Fix (Bungee): Java 16 compatibility (#2433)

This has been tested on the following:
 - AdoptOpenJDK Java 1.8.0_282
 - GraalVM CE 21.0.0 OpenJDK 11.0.10
 - AdoptOpenJDK Java 15.0.2
 - AdoptOpenJDK Java 16 (also tested with BungeeCord b1556)
 - Amazon Corretto OpenJDK 16.0.0.36.1

... with Waterfall b406 on Linux 5.10.28.
This commit is contained in:
Mariell Hoversholm 2021-04-13 11:42:32 +02:00 committed by GitHub
parent 7300a69817
commit 458279d111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 230 additions and 19 deletions

View File

@ -1,4 +1,11 @@
dependencies { dependencies {
implementation(project(":viaversion-common")) implementation(project(":viaversion-common"))
implementation(project(":java-compat"))
compileOnly("net.md-5", "bungeecord-api", Versions.bungee) compileOnly("net.md-5", "bungeecord-api", Versions.bungee)
} }
configure<JavaPluginConvention> {
// This is necessary to allow compilation for Java 8 while still including
// newer Java versions in the code.
disableAutoTargetJvm()
}

View File

@ -25,15 +25,52 @@ import it.unimi.dsi.fastutil.ints.IntSortedSet;
import us.myles.ViaVersion.api.Via; import us.myles.ViaVersion.api.Via;
import us.myles.ViaVersion.api.platform.ViaInjector; import us.myles.ViaVersion.api.platform.ViaInjector;
import us.myles.ViaVersion.bungee.handlers.BungeeChannelInitializer; import us.myles.ViaVersion.bungee.handlers.BungeeChannelInitializer;
import us.myles.ViaVersion.compatibility.FieldModifierAccessor;
import us.myles.ViaVersion.compatibility.JavaVersionIdentifier;
import us.myles.ViaVersion.compatibility.jre16.Jre16FieldModifierAccessor;
import us.myles.ViaVersion.compatibility.jre8.Jre8FieldModifierAccessor;
import us.myles.ViaVersion.compatibility.jre9.Jre9FieldModifierAccessor;
import us.myles.ViaVersion.util.ReflectionUtil; import us.myles.ViaVersion.util.ReflectionUtil;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.List; import java.util.List;
public class BungeeViaInjector implements ViaInjector { public class BungeeViaInjector implements ViaInjector {
private final FieldModifierAccessor fieldModifierAccessor;
public BungeeViaInjector() {
FieldModifierAccessor fieldModifierAccessor = null;
try {
if (JavaVersionIdentifier.IS_JAVA_16) {
fieldModifierAccessor = new Jre16FieldModifierAccessor();
} else if (JavaVersionIdentifier.IS_JAVA_9) {
fieldModifierAccessor = new Jre9FieldModifierAccessor();
}
} catch (final ReflectiveOperationException outer) {
try {
fieldModifierAccessor = new Jre8FieldModifierAccessor();
Via.getPlatform().getLogger().warning("Had to fall back to the Java 8 field modifier accessor.");
outer.printStackTrace();
} catch (final ReflectiveOperationException inner) {
inner.addSuppressed(outer);
throw new IllegalStateException("Cannot create a modifier accessor", inner);
}
}
try {
if (fieldModifierAccessor == null) {
fieldModifierAccessor = new Jre8FieldModifierAccessor();
}
} catch (final ReflectiveOperationException ex) {
throw new IllegalStateException("Cannot create a modifier accessor", ex);
}
// Must be non-null by now.
this.fieldModifierAccessor = fieldModifierAccessor;
}
@Override @Override
public void inject() throws Exception { public void inject() throws Exception {
try { try {
@ -42,26 +79,9 @@ public class BungeeViaInjector implements ViaInjector {
field.setAccessible(true); field.setAccessible(true);
// Remove the final modifier (unless removed by a fork) // Remove the final modifier (unless removed by a fork)
//TODO Fix Java 16 compatibility
int modifiers = field.getModifiers(); int modifiers = field.getModifiers();
if (Modifier.isFinal(modifiers)) { if (Modifier.isFinal(modifiers)) {
try { this.fieldModifierAccessor.setModifiers(field, modifiers & ~Modifier.FINAL);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, modifiers & ~Modifier.FINAL);
} catch (NoSuchFieldException e) {
// Java 12 compatibility *this is fine*
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
for (Field classField : fields) {
if ("modifiers".equals(classField.getName())) {
classField.setAccessible(true);
classField.set(field, modifiers & ~Modifier.FINAL);
break;
}
}
}
} }
BungeeChannelInitializer newInit = new BungeeChannelInitializer((ChannelInitializer<Channel>) field.get(null)); BungeeChannelInitializer newInit = new BungeeChannelInitializer((ChannelInitializer<Channel>) field.get(null));

View File

@ -0,0 +1,12 @@
dependencies {
api(project(":java-compat:java-compat-common"))
api(project(":java-compat:java-compat-8"))
api(project(":java-compat:java-compat-9"))
api(project(":java-compat:java-compat-16"))
}
configure<JavaPluginConvention> {
// This is necessary to allow compilation for Java 8 while still including
// newer Java versions in the code.
disableAutoTargetJvm()
}

View File

@ -0,0 +1,10 @@
dependencies {
api(project(":java-compat:java-compat-common"))
}
configure<JavaPluginConvention> {
// This is for Java 16, but the minimum required for this
// is actually just Java 9!
sourceCompatibility = JavaVersion.VERSION_1_9
targetCompatibility = JavaVersion.VERSION_1_9
}

View File

@ -0,0 +1,35 @@
package us.myles.ViaVersion.compatibility.jre16;
import us.myles.ViaVersion.compatibility.FieldModifierAccessor;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.util.Objects;
@SuppressWarnings({
"java:S1191", // SonarLint/-Qube/-Cloud: We (sadly) need Unsafe for the Java 16 impl.
"java:S3011", // ^: We need to circumvent the access restrictions of fields.
})
public final class Jre16FieldModifierAccessor implements FieldModifierAccessor {
private final VarHandle modifiersHandle;
public Jre16FieldModifierAccessor() throws ReflectiveOperationException {
final Field theUnsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
final sun.misc.Unsafe unsafe = (sun.misc.Unsafe) theUnsafeField.get(null);
final Field trustedLookup = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
final MethodHandles.Lookup lookup = (MethodHandles.Lookup) unsafe.getObject(
unsafe.staticFieldBase(trustedLookup), unsafe.staticFieldOffset(trustedLookup));
this.modifiersHandle = lookup.findVarHandle(Field.class, "modifiers", int.class);
}
@Override
public void setModifiers(final Field field, final int modifiers) {
Objects.requireNonNull(field, "field must not be null");
this.modifiersHandle.set(field, modifiers);
}
}

View File

@ -0,0 +1,3 @@
dependencies {
api(project(":java-compat:java-compat-common"))
}

View File

@ -0,0 +1,23 @@
package us.myles.ViaVersion.compatibility.jre8;
import us.myles.ViaVersion.compatibility.FieldModifierAccessor;
import java.lang.reflect.Field;
import java.util.Objects;
@SuppressWarnings("java:S3011") // SonarLint/-Qube/-Cloud: we are intentionally bypassing the setter.
public final class Jre8FieldModifierAccessor implements FieldModifierAccessor {
private final Field modifiersField;
public Jre8FieldModifierAccessor() throws ReflectiveOperationException {
this.modifiersField = Field.class.getDeclaredField("modifiers");
this.modifiersField.setAccessible(true);
}
@Override
public void setModifiers(final Field field, final int modifiers) throws ReflectiveOperationException {
Objects.requireNonNull(field, "field must not be null");
this.modifiersField.setInt(field, modifiers);
}
}

View File

@ -0,0 +1,8 @@
dependencies {
api(project(":java-compat:java-compat-common"))
}
configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_1_9
targetCompatibility = JavaVersion.VERSION_1_9
}

View File

@ -0,0 +1,24 @@
package us.myles.ViaVersion.compatibility.jre9;
import us.myles.ViaVersion.compatibility.FieldModifierAccessor;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.util.Objects;
public final class Jre9FieldModifierAccessor implements FieldModifierAccessor {
private final VarHandle modifiersHandle;
public Jre9FieldModifierAccessor() throws ReflectiveOperationException {
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
this.modifiersHandle = lookup.findVarHandle(Field.class, "modifiers", int.class);
}
@Override
public void setModifiers(final Field field, final int modifiers) {
Objects.requireNonNull(field, "field must not be null");
this.modifiersHandle.set(field, modifiers);
}
}

View File

@ -0,0 +1,26 @@
package us.myles.ViaVersion.compatibility;
import java.lang.reflect.Field;
/**
* Exposes a way to access the modifiers of a {@link Field} mutably.
* <p>
* <i>Note:</i> This is <b>explicitly</b> an implementation detail. Do not rely on this within plugins and any
* non-ViaVersion code.
* </p>
*/
public interface FieldModifierAccessor {
/**
* Sets the modifiers of a field.
* <p>
* <i>Note:</i> This does not set the accessibility of the field. If you need to read or mutate it, you must handle
* that yourself.
* </p>
*
* @param field the field to set the modifiers of. Will throw if {@code null}.
* @param modifiers the modifiers to set on the given {@code field}.
* @throws ReflectiveOperationException if the reflective operation fails this method is implemented with fails.
*/
void setModifiers(final Field field, final int modifiers)
throws ReflectiveOperationException;
}

View File

@ -0,0 +1,41 @@
package us.myles.ViaVersion.compatibility;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.stream.Stream;
public enum JavaVersionIdentifier {
;
public static final boolean IS_JAVA_9;
public static final boolean IS_JAVA_16;
static {
// Optional<T>#stream()Stream<T> is marked `@since 9`.
IS_JAVA_9 = doesMethodExist(Optional.class, "stream");
// Stream<T>#toList()List<T> is marked `@since 16`.
IS_JAVA_16 = doesMethodExist(Stream.class, "toList");
}
/**
* Checks if the given name of a {@link Method} exists on the given {@link Class} without comparing parameters or
* other parts of the descriptor. The method must be public and declared on the given class.
* <p>
* <i>Note:</i> This should only check for stable methods that are expected to stay permanently.
* </p>
*
* @param clazz the type to get the given {@code method} on.
* @param method the name to find.
* @return whether the given method exists.
*/
private static boolean doesMethodExist(final Class<?> clazz, final String method) {
for (final Method reflect : clazz.getMethods()) {
if (reflect.getName().equals(method)) {
return true;
}
}
return false;
}
}

View File

@ -1,6 +1,8 @@
rootProject.name = "viaversion-parent" rootProject.name = "viaversion-parent"
include("adventure") include("adventure")
include("java-compat", "java-compat:java-compat-common", "java-compat:java-compat-8",
"java-compat:java-compat-9", "java-compat:java-compat-16")
setupViaSubproject("api") setupViaSubproject("api")
setupViaSubproject("common") setupViaSubproject("common")