From 18a8c7a783339ff73d08572cb0728a382eed1ec4 Mon Sep 17 00:00:00 2001 From: Shawn Yang Date: Tue, 20 Feb 2024 14:59:44 +0800 Subject: [PATCH] feat(java): support jdk proxy serialization for graalvm (#1379) support jdk proxy serialization for graalvm, see more in #1364 Closes #1378 --- integration_tests/graalvm_tests/pom.xml | 2 + .../java/org/apache/fury/graalvm/Main.java | 1 + .../org/apache/fury/graalvm/ProxyExample.java | 56 +++++++++++++++ .../graalvm_tests/native-image.properties | 1 + .../META-INF/native-image/proxy-config.json | 7 ++ .../apache/fury/resolver/ClassResolver.java | 5 +- .../fury/serializer/ArraySerializers.java | 1 + .../fury/serializer/JdkProxySerializer.java | 17 ++--- .../apache/fury/serializer/Serializers.java | 14 ---- .../org/apache/fury/util/ReflectionUtils.java | 72 ++++++++++--------- 10 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/ProxyExample.java create mode 100644 integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/proxy-config.json diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml index c9dbc5242b..92f7dc2a78 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -172,6 +172,8 @@ + -H:+UnlockExperimentalVMOptions + -H:DynamicProxyConfigurationFiles=src/main/resources/META-INF/native-image/proxy-config.json diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/Main.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/Main.java index 5325f283c9..d5b403aef1 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/Main.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/Main.java @@ -28,6 +28,7 @@ public static void main(String[] args) throws Throwable { RecordExample.main(args); RecordExample2.main(args); ThreadSafeExample.main(args); + ProxyExample.main(args); Benchmark.main(args); } } diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/ProxyExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/ProxyExample.java new file mode 100644 index 0000000000..43afc8bcd8 --- /dev/null +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fury/graalvm/ProxyExample.java @@ -0,0 +1,56 @@ +/* + * 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.graalvm; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.function.Function; +import org.apache.fury.Fury; +import org.apache.fury.util.Preconditions; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class ProxyExample { + private static class TestInvocationHandler implements InvocationHandler, Serializable { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return 1; + } + } + + static Fury fury; + + static { + fury = Fury.builder().requireClassRegistration(true).build(); + // register and generate serializer code. + fury.register(TestInvocationHandler.class, true); + } + + public static void main(String[] args) { + Function function = + (Function) + Proxy.newProxyInstance( + fury.getClassLoader(), new Class[] {Function.class}, new TestInvocationHandler()); + Function deserializedFunction = (Function) fury.deserialize(fury.serialize(function)); + Preconditions.checkArgument(deserializedFunction.apply(null).equals(1)); + System.out.println("Proxy tests pass"); + } +} diff --git a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fury/graalvm_tests/native-image.properties b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fury/graalvm_tests/native-image.properties index 631ca2e66e..c4f4dc89d6 100644 --- a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fury/graalvm_tests/native-image.properties +++ b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fury/graalvm_tests/native-image.properties @@ -22,4 +22,5 @@ Args = -H:+ReportExceptionStackTraces \ org.apache.fury.graalvm.record.RecordExample,\ org.apache.fury.graalvm.record.RecordExample2,\ org.apache.fury.graalvm.ThreadSafeExample,\ + org.apache.fury.graalvm.ProxyExample,\ org.apache.fury.graalvm.Benchmark diff --git a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/proxy-config.json b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/proxy-config.json new file mode 100644 index 0000000000..305ed56fea --- /dev/null +++ b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/proxy-config.json @@ -0,0 +1,7 @@ +[ + { + "interfaces": [ + "java.util.function.Function" + ] + } +] diff --git a/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java b/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java index 1d376dfd0f..a65291f3a0 100644 --- a/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java +++ b/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java @@ -307,8 +307,8 @@ private void addDefaultSerializers() { // primitive types will be boxed. addDefaultSerializer(String.class, new StringSerializer(fury)); PrimitiveSerializers.registerDefaultSerializers(fury); - ArraySerializers.registerDefaultSerializers(fury); Serializers.registerDefaultSerializers(fury); + ArraySerializers.registerDefaultSerializers(fury); TimeSerializers.registerDefaultSerializers(fury); OptionalSerializers.registerDefaultSerializers(fury); CollectionSerializers.registerDefaultSerializers(fury); @@ -1820,6 +1820,9 @@ private Class getSerializerClassFromGraalvmRegistry(Class< } } if (GraalvmSupport.isGraalRuntime()) { + if (Functions.isLambda(cls) || ReflectionUtils.isJdkProxy(cls)) { + return null; + } throw new RuntimeException(String.format("Class %s is not registered", cls)); } return null; diff --git a/java/fury-core/src/main/java/org/apache/fury/serializer/ArraySerializers.java b/java/fury-core/src/main/java/org/apache/fury/serializer/ArraySerializers.java index 27f33fff31..5696fec5dd 100644 --- a/java/fury-core/src/main/java/org/apache/fury/serializer/ArraySerializers.java +++ b/java/fury-core/src/main/java/org/apache/fury/serializer/ArraySerializers.java @@ -661,6 +661,7 @@ public String[] xread(MemoryBuffer buffer) { public static void registerDefaultSerializers(Fury fury) { fury.registerSerializer(Object[].class, new ObjectArraySerializer<>(fury, Object[].class)); + fury.registerSerializer(Class[].class, new ObjectArraySerializer<>(fury, Class[].class)); fury.registerSerializer(byte[].class, new ByteArraySerializer(fury)); fury.registerSerializer(char[].class, new CharArraySerializer(fury)); fury.registerSerializer(short[].class, new ShortArraySerializer(fury)); diff --git a/java/fury-core/src/main/java/org/apache/fury/serializer/JdkProxySerializer.java b/java/fury-core/src/main/java/org/apache/fury/serializer/JdkProxySerializer.java index 914d11b54c..5befb551ce 100644 --- a/java/fury-core/src/main/java/org/apache/fury/serializer/JdkProxySerializer.java +++ b/java/fury-core/src/main/java/org/apache/fury/serializer/JdkProxySerializer.java @@ -19,12 +19,12 @@ package org.apache.fury.serializer; +import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import org.apache.fury.Fury; import org.apache.fury.memory.MemoryBuffer; import org.apache.fury.resolver.RefResolver; -import org.apache.fury.util.GraalvmSupport; import org.apache.fury.util.Platform; import org.apache.fury.util.Preconditions; import org.apache.fury.util.ReflectionUtils; @@ -34,21 +34,12 @@ public class JdkProxySerializer extends Serializer { // Make offset compatible with graalvm native image. + private static final Field FIELD; private static final long PROXY_HANDLER_FIELD_OFFSET; static { - if (GraalvmSupport.isGraalBuildtime()) { - try { - // Make offset compatible with graalvm native image. - PROXY_HANDLER_FIELD_OFFSET = Platform.objectFieldOffset(Proxy.class.getDeclaredField("h")); - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } else { - // not all JVM implementations use 'h' as internal InvocationHandler name - PROXY_HANDLER_FIELD_OFFSET = - ReflectionUtils.getFieldOffset(Proxy.class, InvocationHandler.class); - } + FIELD = ReflectionUtils.getField(Proxy.class, InvocationHandler.class); + PROXY_HANDLER_FIELD_OFFSET = Platform.objectFieldOffset(FIELD); } private static final InvocationHandler STUB_HANDLER = diff --git a/java/fury-core/src/main/java/org/apache/fury/serializer/Serializers.java b/java/fury-core/src/main/java/org/apache/fury/serializer/Serializers.java index 9a0020fe15..19829c97aa 100644 --- a/java/fury-core/src/main/java/org/apache/fury/serializer/Serializers.java +++ b/java/fury-core/src/main/java/org/apache/fury/serializer/Serializers.java @@ -33,7 +33,6 @@ import java.net.URI; import java.nio.charset.Charset; import java.util.Currency; -import java.util.IdentityHashMap; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -47,7 +46,6 @@ import org.apache.fury.memory.MemoryBuffer; import org.apache.fury.resolver.ClassResolver; import org.apache.fury.type.Type; -import org.apache.fury.type.TypeUtils; import org.apache.fury.util.GraalvmSupport; import org.apache.fury.util.Platform; import org.apache.fury.util.Preconditions; @@ -532,20 +530,8 @@ public UUID read(MemoryBuffer buffer) { } public static final class ClassSerializer extends Serializer { - private static final byte USE_CLASS_ID = 0; - private static final byte USE_CLASSNAME = 1; - private static final byte PRIMITIVE_FLAG = 2; - private final IdentityHashMap, Byte> primitivesMap = new IdentityHashMap<>(); - private final Class[] id2PrimitiveClasses = new Class[9]; - public ClassSerializer(Fury fury) { super(fury, Class.class); - byte count = 0; - for (Class primitiveType : TypeUtils.getSortedPrimitiveClasses()) { - primitivesMap.put(primitiveType, count); - id2PrimitiveClasses[count] = primitiveType; - count++; - } } @Override diff --git a/java/fury-core/src/main/java/org/apache/fury/util/ReflectionUtils.java b/java/fury-core/src/main/java/org/apache/fury/util/ReflectionUtils.java index f45fef4b9b..8144a0ba42 100644 --- a/java/fury-core/src/main/java/org/apache/fury/util/ReflectionUtils.java +++ b/java/fury-core/src/main/java/org/apache/fury/util/ReflectionUtils.java @@ -281,6 +281,44 @@ public static Field getField(Class cls, String fieldName) { return field; } + /** + * Gets the only field in the class matching the required type. + * + * @param clazz the class in which the field should be declared + * @param fieldType the required type of the field + * @return the field with specified type + * @throws IllegalStateException if there are multiple fields of the required type + * @throws IllegalArgumentException if there are no fields of the required type + */ + public static Field getField(Class clazz, Class fieldType) { + Field f = null; + Class cls = clazz; + while (cls != null) { + for (Field fi : cls.getDeclaredFields()) { + if (fieldType.equals(fi.getType())) { + if (f != null) { + throw new IllegalStateException( + "Found multiple field s matching " + + fieldType + + " in " + + cls + + ": " + + f + + " and " + + fi); + } + f = fi; + } + } + cls = cls.getSuperclass(); + } + if (f == null) { + throw new IllegalArgumentException( + "Found no field matching " + fieldType.getName() + " in " + clazz + "!"); + } + return f; + } + public static Field getFieldNullable(Class cls, String fieldName) { Class clazz = cls; do { @@ -348,40 +386,6 @@ public static long getFieldOffset(Class cls, String fieldName) { return getFieldOffset(field); } - /** - * Gets the offset of the only field in the class matching the required type. - * - * @param clazz the class in which the field should be declared - * @param fieldType the required type of the field - * @return offset of the field - * @throws IllegalStateException if there are multiple fields of the required type - * @throws IllegalArgumentException if there are no fields of the required type - */ - public static long getFieldOffset(Class clazz, Class fieldType) { - Field f = null; - for (Field fi : clazz.getDeclaredFields()) { - if (fieldType.equals(fi.getType())) { - if (f != null) { - throw new IllegalStateException( - "Found multiple field s matching " - + fieldType - + " in " - + clazz - + ": " - + f - + " and " - + fi); - } - f = fi; - } - } - if (f == null) { - throw new IllegalArgumentException( - "Found no field matching " + fieldType.getName() + " in " + clazz + "!"); - } - return Platform.objectFieldOffset(f); - } - public static long getFieldOffsetChecked(Class cls, String fieldName) { long offset = getFieldOffset(cls, fieldName); Preconditions.checkArgument(offset != -1);