Skip to content

Commit

Permalink
feat(java): implement define_class insteadof using javaassist (#1422)
Browse files Browse the repository at this point in the history
This PR implements `defineClass` insteadof using javaassist to reduce
the dependency
  • Loading branch information
chaokunyang authored Mar 24, 2024
1 parent ab8f480 commit 50058ec
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 27 deletions.
9 changes: 0 additions & 9 deletions java/fury-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</dependency>
<dependency>
<groupId>org.apache.fury</groupId>
<artifactId>fury-test-core</artifactId>
Expand Down Expand Up @@ -89,18 +85,13 @@
<artifactSet>
<includes>
<include>org.codehaus.janino</include>
<include>org.javassist</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>org.codehaus</pattern>
<shadedPattern>org.apache.fury.shaded.org.codehaus</shadedPattern>
</relocation>
<relocation>
<pattern>javassist</pattern>
<shadedPattern>org.apache.fury.shaded.javassist</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
13 changes: 7 additions & 6 deletions java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
<packaging>pom</packaging>
<version>0.5.0-SNAPSHOT</version>
<name>Fury Project Parent POM</name>
<description>A blazing fast multi-language serialization framework powered by jit and zero-copy.</description>
<description>
Apache Fury™ is a blazing fast multi-language serialization framework powered by jit and zero-copy.
</description>
<url>https://github.com/apache/incubator-fury</url>

<licenses>
Expand Down Expand Up @@ -90,11 +92,6 @@
<artifactId>janino</artifactId>
<version>${janino.version}</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<!-- Cxeb68d52e-5509 -->
<dependency>
<groupId>commons-codec</groupId>
Expand Down Expand Up @@ -161,6 +158,10 @@
</executions>
<configuration>
<doclint>none</doclint>
<bottom>
Copyright © 2023-2024, The Apache Software Foundation. Apache Fury™, Fury™, and Apache
are either registered trademarks or trademarks of the Apache Software Foundation.
</bottom>
</configuration>
</plugin>
<plugin>
Expand Down

0 comments on commit 50058ec

Please sign in to comment.