From 50058ec2c52132c23c43d0b58b8049318c0187e0 Mon Sep 17 00:00:00 2001 From: Shawn Yang Date: Sun, 24 Mar 2024 20:04:18 +0800 Subject: [PATCH] feat(java): implement define_class insteadof using javaassist (#1422) This PR implements `defineClass` insteadof using javaassist to reduce the dependency --- java/fury-core/pom.xml | 9 --- .../apache/fury/util/ClassLoaderUtils.java | 4 +- .../apache/fury/util/unsafe/DefineClass.java | 76 +++++++++++++++++++ .../apache/fury/util/unsafe/_JDKAccess.java | 37 +++++++++ .../org/apache/fury/util/unsafe/_Lookup.java | 43 +++++++++++ .../fury/util/ClassLoaderUtilsTest.java | 17 ++--- .../fury/util/unsafe/DefineClassTest.java | 72 ++++++++++++++++++ java/pom.xml | 13 ++-- 8 files changed, 244 insertions(+), 27 deletions(-) create mode 100644 java/fury-core/src/main/java/org/apache/fury/util/unsafe/DefineClass.java create mode 100644 java/fury-core/src/test/java/org/apache/fury/util/unsafe/DefineClassTest.java diff --git a/java/fury-core/pom.xml b/java/fury-core/pom.xml index 7d8caf1d1e..d46fb31668 100644 --- a/java/fury-core/pom.xml +++ b/java/fury-core/pom.xml @@ -50,10 +50,6 @@ org.codehaus.janino janino - - org.javassist - javassist - org.apache.fury fury-test-core @@ -89,7 +85,6 @@ org.codehaus.janino - org.javassist @@ -97,10 +92,6 @@ org.codehaus org.apache.fury.shaded.org.codehaus - - javassist - org.apache.fury.shaded.javassist - diff --git a/java/fury-core/src/main/java/org/apache/fury/util/ClassLoaderUtils.java b/java/fury-core/src/main/java/org/apache/fury/util/ClassLoaderUtils.java index 30e916c8a5..3dcc200d18 100644 --- a/java/fury-core/src/main/java/org/apache/fury/util/ClassLoaderUtils.java +++ b/java/fury-core/src/main/java/org/apache/fury/util/ClassLoaderUtils.java @@ -33,7 +33,7 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import javassist.util.proxy.DefineClassHelper; +import org.apache.fury.util.unsafe.DefineClass; import org.slf4j.Logger; /** ClassLoader utility for defining class and loading class by strategies. */ @@ -254,7 +254,7 @@ public static Class tryDefineClassesInClassLoader( if (classLoader instanceof ByteArrayClassLoader) { return ((ByteArrayClassLoader) classLoader).defineClassPublic(className, bytecode, domain); } - return DefineClassHelper.toClass(className, neighbor, classLoader, domain, bytecode); + return DefineClass.defineClass(className, neighbor, classLoader, domain, bytecode); } catch (Exception | LinkageError e) { LOG.debug("Unable define class {} in classloader {}.", className, classLoader, e); return null; diff --git a/java/fury-core/src/main/java/org/apache/fury/util/unsafe/DefineClass.java b/java/fury-core/src/main/java/org/apache/fury/util/unsafe/DefineClass.java new file mode 100644 index 0000000000..e2fe03e2f6 --- /dev/null +++ b/java/fury-core/src/main/java/org/apache/fury/util/unsafe/DefineClass.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fury.util.unsafe; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.security.ProtectionDomain; +import org.apache.fury.annotation.Internal; +import org.apache.fury.util.Platform; +import org.apache.fury.util.Preconditions; + +/** A class to define bytecode as a class. */ +@Internal +public class DefineClass { + private static volatile MethodHandle classloaderDefineClassHandle; + + public static Class defineClass( + String className, + Class neighbor, + ClassLoader loader, + ProtectionDomain domain, + byte[] bytecodes) { + Preconditions.checkNotNull(loader); + Preconditions.checkArgument(Platform.JAVA_VERSION >= 8); + if (neighbor != null && Platform.JAVA_VERSION >= 9) { + // classes in bytecode must be in same package as lookup class. + MethodHandles.Lookup lookup = MethodHandles.lookup(); + _JDKAccess.addReads(_JDKAccess.getModule(DefineClass.class), _JDKAccess.getModule(neighbor)); + lookup = _Lookup.privateLookupIn(neighbor, lookup); + return _Lookup.defineClass(lookup, bytecodes); + } + if (classloaderDefineClassHandle == null) { + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(ClassLoader.class); + try { + classloaderDefineClassHandle = + lookup.findVirtual( + ClassLoader.class, + "defineClass", + MethodType.methodType( + Class.class, + String.class, + byte[].class, + int.class, + int.class, + ProtectionDomain.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + try { + return (Class) + classloaderDefineClassHandle.invokeWithArguments( + loader, className, bytecodes, 0, bytecodes.length, domain); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } +} diff --git a/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_JDKAccess.java b/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_JDKAccess.java index e2680bb52d..e7d3b0de89 100644 --- a/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_JDKAccess.java +++ b/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_JDKAccess.java @@ -26,6 +26,7 @@ import java.lang.invoke.MethodHandles.Lookup; import java.lang.invoke.MethodType; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -39,6 +40,7 @@ import org.apache.fury.collection.Tuple2; import org.apache.fury.type.TypeUtils; import org.apache.fury.util.GraalvmSupport; +import org.apache.fury.util.Platform; import org.apache.fury.util.Preconditions; import org.apache.fury.util.Utils; import org.apache.fury.util.function.ToByteFunction; @@ -303,4 +305,39 @@ public static Object makeGetterFunction( throw new IllegalStateException(e); } } + + private static volatile Method getModuleMethod; + + public static Object getModule(Class cls) { + Preconditions.checkArgument(Platform.JAVA_VERSION >= 9); + if (getModuleMethod == null) { + try { + getModuleMethod = Class.class.getDeclaredMethod("getModule"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + try { + return getModuleMethod.invoke(cls); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + // caller sensitive, must use MethodHandle to walk around the check. + private static volatile MethodHandle addReadsHandle; + + public static Object addReads(Object thisModule, Object otherModule) { + Preconditions.checkArgument(Platform.JAVA_VERSION >= 9); + try { + if (addReadsHandle == null) { + Class cls = Class.forName("java.lang.Module"); + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(cls); + addReadsHandle = lookup.findVirtual(cls, "addReads", MethodType.methodType(cls, cls)); + } + return addReadsHandle.invoke(thisModule, otherModule); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } } diff --git a/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_Lookup.java b/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_Lookup.java index adbadd8c8d..16ddedb1fe 100644 --- a/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_Lookup.java +++ b/java/fury-core/src/main/java/org/apache/fury/util/unsafe/_Lookup.java @@ -25,6 +25,9 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.ProtectionDomain; // CHECKSTYLE.OFF:TypeName class _Lookup { @@ -105,4 +108,44 @@ private static MethodHandles.Lookup getLookupByReflection(Class cls) { return null; } } + + private static volatile Method PRIVATE_LOOKUP_IN = null; + + public static Lookup privateLookupIn(Class targetClass, Lookup caller) { + try { + // This doesn't have side effect, it's ok to read and assign it in multi-threaded way. + if (PRIVATE_LOOKUP_IN == null) { + Method m = + MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, Lookup.class); + m.setAccessible(true); + PRIVATE_LOOKUP_IN = m; + } + return (Lookup) PRIVATE_LOOKUP_IN.invoke(null, targetClass, caller); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private static volatile Method DEFINE_CLASS = null; + + /** + * Creates and links a class or interface from {@code bytes} with the same class loader and in the + * same runtime package and {@linkplain java.security.ProtectionDomain protection domain} as this + * lookup's {@linkplain Lookup#lookupClass() lookup class} as if calling {@link + * ClassLoader#defineClass(String,byte[],int,int, ProtectionDomain) ClassLoader::defineClass}. + * Note that classes in bytecode must be in same package as lookup class. + */ + public static Class defineClass(Lookup lookup, byte[] bytes) { + try { + // This doesn't have side effect, it's ok to read and assign it in multi-threaded way. + if (DEFINE_CLASS == null) { + Method m = Lookup.class.getDeclaredMethod("defineClass", byte[].class); + m.setAccessible(true); + DEFINE_CLASS = m; + } + return (Class) DEFINE_CLASS.invoke(lookup, (Object) bytes); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } } diff --git a/java/fury-core/src/test/java/org/apache/fury/util/ClassLoaderUtilsTest.java b/java/fury-core/src/test/java/org/apache/fury/util/ClassLoaderUtilsTest.java index 1540461157..39dd4864b5 100644 --- a/java/fury-core/src/test/java/org/apache/fury/util/ClassLoaderUtilsTest.java +++ b/java/fury-core/src/test/java/org/apache/fury/util/ClassLoaderUtilsTest.java @@ -78,18 +78,15 @@ public void testTryDefineClassesInClassLoader(String pkg) { .values() .iterator() .next(); - if (Platform.JAVA_VERSION >= 17) { + if (ClassLoaderUtils.class.getPackage().getName().equals(pkg)) { Class cls = ClassLoaderUtils.tryDefineClassesInClassLoader( - pkg + "." + classname, null, getClass().getClassLoader(), bytes); - Assert.assertNull(cls); - cls = - ClassLoaderUtils.tryDefineClassesInClassLoader( - pkg + "." + classname, getClass(), getClass().getClassLoader(), bytes); - if (ClassLoaderUtils.class.getPackage().getName().equals(pkg)) { - Assert.assertNotNull(cls); - Assert.assertEquals(cls.getSimpleName(), classname); - } + pkg + "." + classname, + ClassLoaderUtils.class, + ClassLoaderUtils.class.getClassLoader(), + bytes); + Assert.assertNotNull(cls); + Assert.assertEquals(cls.getSimpleName(), classname); } else { Class cls = ClassLoaderUtils.tryDefineClassesInClassLoader( diff --git a/java/fury-core/src/test/java/org/apache/fury/util/unsafe/DefineClassTest.java b/java/fury-core/src/test/java/org/apache/fury/util/unsafe/DefineClassTest.java new file mode 100644 index 0000000000..70d5ccd6aa --- /dev/null +++ b/java/fury-core/src/test/java/org/apache/fury/util/unsafe/DefineClassTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fury.util.unsafe; + +import java.util.Collections; +import org.apache.fury.codegen.CompileUnit; +import org.apache.fury.codegen.JaninoUtils; +import org.apache.fury.util.ClassLoaderUtils; +import org.apache.fury.util.Platform; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class DefineClassTest { + + @Test + public void testDefineClass() throws ClassNotFoundException { + String pkg = DefineClassTest.class.getPackage().getName(); + CompileUnit unit = + new CompileUnit( + pkg, + "A", + ("package " + + pkg + + ";\n" + + "public class A {\n" + + " public static String hello() { return \"HELLO\"; }\n" + + "}")); + byte[] bytecodes = + JaninoUtils.toBytecode(Thread.currentThread().getContextClassLoader(), unit) + .values() + .iterator() + .next(); + String className = pkg + ".A"; + ClassLoaderUtils.ByteArrayClassLoader loader = + new ClassLoaderUtils.ByteArrayClassLoader(Collections.singletonMap(className, bytecodes)); + loader.loadClass(className); + + loader = + new ClassLoaderUtils.ByteArrayClassLoader(Collections.singletonMap(className, bytecodes)); + DefineClass.defineClass(className, DefineClassTest.class, loader, null, bytecodes); + Class clz = loader.loadClass(className); + if (Platform.JAVA_VERSION >= 9) { + Assert.assertEquals(clz.getClassLoader(), DefineClassTest.class.getClassLoader()); + Assert.assertThrows( + Exception.class, + () -> + DefineClass.defineClass( + className, null, DefineClassTest.class.getClassLoader(), null, bytecodes)); + } else { + Assert.assertEquals(clz.getClassLoader(), loader); + DefineClass.defineClass( + className, null, DefineClassTest.class.getClassLoader(), null, bytecodes); + } + } +} diff --git a/java/pom.xml b/java/pom.xml index ba83466732..a231df390b 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -35,7 +35,9 @@ pom 0.5.0-SNAPSHOT Fury Project Parent POM - A blazing fast multi-language serialization framework powered by jit and zero-copy. + + Apache Fury™ is a blazing fast multi-language serialization framework powered by jit and zero-copy. + https://github.com/apache/incubator-fury @@ -90,11 +92,6 @@ janino ${janino.version} - - org.javassist - javassist - 3.28.0-GA - commons-codec @@ -161,6 +158,10 @@ none + + Copyright © 2023-2024, The Apache Software Foundation. Apache Fury™, Fury™, and Apache + are either registered trademarks or trademarks of the Apache Software Foundation. +