From 19b9f1be38fdfb7c5998744234fa8f23a8d496fc Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Thu, 23 Jul 2020 11:17:33 +0100 Subject: [PATCH] Support defining classes in multiple JDK versions. --- classloader/build-include-jdk-misc | 0 classloader/build-release-8 | 0 classloader/build-test-java8 | 0 classloader/build-test-java9 | 0 classloader/pom.xml | 76 +++++++++++++++++++ .../common/classloader/ClassDefiner.java | 41 ++++++++++ .../common/classloader/ClassDefiner.java | 21 +++++ .../common/classloader/ClassDefinerTest.java | 65 ++++++++++++++++ pom.xml | 8 +- 9 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 classloader/build-include-jdk-misc create mode 100644 classloader/build-release-8 create mode 100644 classloader/build-test-java8 create mode 100644 classloader/build-test-java9 create mode 100644 classloader/pom.xml create mode 100644 classloader/src/main/java/io/smallrye/common/classloader/ClassDefiner.java create mode 100644 classloader/src/main/java9/io/smallrye/common/classloader/ClassDefiner.java create mode 100644 classloader/src/test/java/io/smallrye/common/classloader/ClassDefinerTest.java diff --git a/classloader/build-include-jdk-misc b/classloader/build-include-jdk-misc new file mode 100644 index 00000000..e69de29b diff --git a/classloader/build-release-8 b/classloader/build-release-8 new file mode 100644 index 00000000..e69de29b diff --git a/classloader/build-test-java8 b/classloader/build-test-java8 new file mode 100644 index 00000000..e69de29b diff --git a/classloader/build-test-java9 b/classloader/build-test-java9 new file mode 100644 index 00000000..e69de29b diff --git a/classloader/pom.xml b/classloader/pom.xml new file mode 100644 index 00000000..be8ca0c1 --- /dev/null +++ b/classloader/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + io.smallrye.common + smallrye-common-parent + 1.1.1-SNAPSHOT + + + smallrye-common-classloader + SmallRye Common: Classloader + + + + + org.jboss + jdk-misc + ${version.jdk-misc} + provided + true + + + org.junit.jupiter + junit-jupiter + + + org.ow2.asm + asm + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + true + + + + + + + + + coverage + + @{jacocoArgLine} + + + + + org.jacoco + jacoco-maven-plugin + + + report + verify + + report + + + + + + + + + + diff --git a/classloader/src/main/java/io/smallrye/common/classloader/ClassDefiner.java b/classloader/src/main/java/io/smallrye/common/classloader/ClassDefiner.java new file mode 100644 index 00000000..0f35532e --- /dev/null +++ b/classloader/src/main/java/io/smallrye/common/classloader/ClassDefiner.java @@ -0,0 +1,41 @@ +package io.smallrye.common.classloader; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import sun.misc.Unsafe; + +public class ClassDefiner { + private static final Unsafe unsafe; + + static { + unsafe = AccessController.doPrivileged(new PrivilegedAction() { + public Unsafe run() { + try { + final Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } catch (NoSuchFieldException e) { + throw new NoSuchFieldError(e.getMessage()); + } + } + }); + } + + public static Class defineClass(MethodHandles.Lookup lookup, Class parent, String className, byte[] classBytes) { + // The className for Unsafe.defineClass must match the one in classBytes, so we can try to verify the package + // with the parent + String parentPkg = parent.getPackage().getName(); + String classPkg = className.substring(0, className.lastIndexOf('.')); + + if (!parentPkg.equals(classPkg)) { + throw new IllegalArgumentException("Class not in same package as lookup class"); + } + + return unsafe.defineClass(className, classBytes, 0, classBytes.length, parent.getClassLoader(), null); + } +} diff --git a/classloader/src/main/java9/io/smallrye/common/classloader/ClassDefiner.java b/classloader/src/main/java9/io/smallrye/common/classloader/ClassDefiner.java new file mode 100644 index 00000000..e962ab91 --- /dev/null +++ b/classloader/src/main/java9/io/smallrye/common/classloader/ClassDefiner.java @@ -0,0 +1,21 @@ +package io.smallrye.common.classloader; + +import java.lang.invoke.MethodHandles; +import java.security.AccessController; +import java.security.PrivilegedAction; + +public class ClassDefiner { + public static Class defineClass(MethodHandles.Lookup lookup, Class parent, String className, byte[] classBytes) { + return AccessController.doPrivileged(new PrivilegedAction>() { + @Override + public Class run() { + try { + MethodHandles.Lookup privateLookupIn = MethodHandles.privateLookupIn(parent, lookup); + return privateLookupIn.defineClass(classBytes); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } + } + }); + } +} diff --git a/classloader/src/test/java/io/smallrye/common/classloader/ClassDefinerTest.java b/classloader/src/test/java/io/smallrye/common/classloader/ClassDefinerTest.java new file mode 100644 index 00000000..feeb47c3 --- /dev/null +++ b/classloader/src/test/java/io/smallrye/common/classloader/ClassDefinerTest.java @@ -0,0 +1,65 @@ +package io.smallrye.common.classloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ALOAD; +import static org.objectweb.asm.Opcodes.ARETURN; +import static org.objectweb.asm.Opcodes.INVOKESPECIAL; +import static org.objectweb.asm.Opcodes.RETURN; +import static org.objectweb.asm.Opcodes.V1_8; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; + +class ClassDefinerTest { + @Test + void defineClass() throws Exception { + Class helloClass = ClassDefiner.defineClass(MethodHandles.lookup(), ClassDefinerTest.class, + "io.smallrye.common.classloader.TestHello", + getHelloClass("io.smallrye.common.classloader.TestHello")); + assertNotNull(helloClass); + + Object hello = helloClass.getDeclaredConstructor().newInstance(); + Method helloMethod = helloClass.getDeclaredMethod("hello"); + assertEquals("hello", helloMethod.invoke(hello)); + } + + @Test + void notAllowDifferentPackages() { + assertThrows(IllegalArgumentException.class, + () -> ClassDefiner.defineClass(MethodHandles.lookup(), ClassDefinerTest.class, + "io.smallrye.common.something.TestHello", + getHelloClass("io.smallrye.common.something.TestHello"))); + } + + private byte[] getHelloClass(final String name) { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + writer.visit(V1_8, ACC_PUBLIC, name.replace('.', '/'), null, "java/lang/Object", null); + + { + MethodVisitor methodVisitor = writer.visitMethod(ACC_PUBLIC, "", "()V", null, null); + methodVisitor.visitVarInsn(ALOAD, 0); + methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + methodVisitor.visitInsn(RETURN); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + } + + { + MethodVisitor methodVisitor = writer.visitMethod(ACC_PUBLIC, "hello", "()Ljava/lang/String;", null, null); + methodVisitor.visitLdcInsn("hello"); + methodVisitor.visitInsn(ARETURN); + methodVisitor.visitMaxs(1, 1); + methodVisitor.visitEnd(); + writer.visitEnd(); + } + + return writer.toByteArray(); + } +} diff --git a/pom.xml b/pom.xml index cdcba96a..fd5aebc2 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ annotation + classloader constraint cpu expression @@ -78,7 +79,12 @@ pom import - + + org.ow2.asm + asm + 7.0 + test +