diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml index 1957253d21..acf2195897 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -25,7 +25,7 @@ org.apache.fory fory-parent - 0.13.0 + 0.13.0-SNAPSHOT ../../java 4.0.0 @@ -35,7 +35,6 @@ 17 UTF-8 0.9.28 - 5.8.1 org.apache.fory.graalvm.Main main @@ -59,6 +58,16 @@ fory-core ${project.version} + + org.apache.fory + fory-graalvm-feature + ${project.version} + + + org.testng + testng + test + @@ -149,20 +158,14 @@ ${native.maven.plugin.version} true + build-native - build + compile-no-fork package - - test-native - - test - - test - false @@ -184,17 +187,13 @@ java-agent - exec + java test - java - ${project.build.directory} - - -classpath - - ${mainClass} - + ${mainClass} + true + false diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java new file mode 100644 index 0000000000..4ce7f6075d --- /dev/null +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/FeatureTestExample.java @@ -0,0 +1,133 @@ +/* + * 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.fory.graalvm; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import org.apache.fory.Fory; +import org.apache.fory.builder.Generated; +import org.apache.fory.config.Language; +import org.apache.fory.util.GraalvmSupport; + +public class FeatureTestExample { + + public static class PrivateConstructorClass { + private String value; + + private PrivateConstructorClass() { + // Private constructor + } + + public PrivateConstructorClass(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + // Test interface for proxy + public interface TestInterface { + String getValue(); + + void setValue(String value); + } + + // Simple invocation handler + public static class TestInvocationHandler implements InvocationHandler { + private String value; + + public TestInvocationHandler(String value) { + this.value = value; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("getValue".equals(method.getName())) { + return value; + } else if ("setValue".equals(method.getName())) { + value = (String) args[0]; + return null; + } + return null; + } + } + + public static void main(String[] args) { + System.out.println("Testing Fory GraalVM Feature..."); + + Fory fory = + Fory.builder().withLanguage(Language.JAVA).withRefTracking(true).withCodegen(true).build(); + + fory.register(PrivateConstructorClass.class); + fory.register(TestInvocationHandler.class); + + // Register proxy interface + GraalvmSupport.registerProxySupport(TestInterface.class); + + try { + // Test 1: Serialize/deserialize class with private constructor + PrivateConstructorClass original = new PrivateConstructorClass("test-value"); + byte[] serialized = fory.serialize(original); + PrivateConstructorClass deserialized = (PrivateConstructorClass) fory.deserialize(serialized); + + // Assert that the serializer is generated + if (!(fory.getClassResolver().getSerializer(PrivateConstructorClass.class) + instanceof Generated)) { + throw new RuntimeException( + "Expected Generated serializer for PrivateConstructorClass but got: " + + fory.getClassResolver().getSerializer(PrivateConstructorClass.class).getClass()); + } + + if (!"test-value".equals(deserialized.getValue())) { + throw new RuntimeException("Private constructor class test failed"); + } + System.out.println("Private constructor class serialization test passed"); + + // Test 2: Serialize/deserialize proxy object + TestInterface proxy = + (TestInterface) + Proxy.newProxyInstance( + TestInterface.class.getClassLoader(), + new Class[] {TestInterface.class}, + new TestInvocationHandler("proxy-value")); + + byte[] proxySerialised = fory.serialize(proxy); + TestInterface deserializedProxy = (TestInterface) fory.deserialize(proxySerialised); + + if (!"proxy-value".equals(deserializedProxy.getValue())) { + throw new RuntimeException("Proxy test failed"); + } + System.out.println("Proxy serialization test passed"); + + System.out.println("All GraalVM Feature tests passed!"); + + } catch (Exception e) { + System.err.println("GraalVM Feature test failed: " + e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java index d1e1591b83..74a02e5c45 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java @@ -40,5 +40,6 @@ public static void main(String[] args) throws Throwable { Benchmark.main(args); CollectionExample.main(args); ArrayExample.main(args); + FeatureTestExample.main(args); } } diff --git a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties index 60369d6826..53e708a917 100644 --- a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties +++ b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties @@ -32,5 +32,4 @@ Args=-H:+ReportExceptionStackTraces \ org.apache.fory.graalvm.EnsureSerializerExample$CustomSerializer,\ org.apache.fory.graalvm.CollectionExample,\ org.apache.fory.graalvm.ArrayExample,\ - org.apache.fory.graalvm.Benchmark,\ - org.apache.fory.graalvm.Main + org.apache.fory.graalvm.Benchmark diff --git a/integration_tests/graalvm_tests/src/test/java/org/apache/fory/graalvm/GraalvmRegistrationTest.java b/integration_tests/graalvm_tests/src/test/java/org/apache/fory/graalvm/GraalvmRegistrationTest.java new file mode 100644 index 0000000000..167d6685ab --- /dev/null +++ b/integration_tests/graalvm_tests/src/test/java/org/apache/fory/graalvm/GraalvmRegistrationTest.java @@ -0,0 +1,69 @@ +/* + * 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.fory.graalvm; + +import org.apache.fory.Fory; +import org.apache.fory.config.Language; +import org.apache.fory.util.GraalvmSupport; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class GraalvmRegistrationTest { + + static class GraalvmRegistrationBean { + int value; + } + + @Test + public void testEnsureSerializersCompiledRegistersClassesForGraalvm() { + Fory fory = Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(true).build(); + GraalvmSupport.clearRegistrations(); + Assert.assertFalse( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + + fory.register(GraalvmRegistrationBean.class); + + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + Assert.assertTrue( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + + GraalvmSupport.clearRegistrations(); + Assert.assertFalse( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + + fory.ensureSerializersCompiled(); + + Assert.assertTrue( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + } else { + Assert.assertFalse( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + + GraalvmSupport.clearRegistrations(); + Assert.assertFalse( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + + fory.ensureSerializersCompiled(); + + Assert.assertFalse( + GraalvmSupport.getRegisteredClasses().contains(GraalvmRegistrationBean.class)); + } + } +} diff --git a/integration_tests/graalvm_tests/src/test/java/org/apache/fory/util/GraalvmSupportRecordTest.java b/integration_tests/graalvm_tests/src/test/java/org/apache/fory/util/GraalvmSupportRecordTest.java new file mode 100644 index 0000000000..9ce8db59a2 --- /dev/null +++ b/integration_tests/graalvm_tests/src/test/java/org/apache/fory/util/GraalvmSupportRecordTest.java @@ -0,0 +1,112 @@ +/* + * 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.fory.util; + +import org.apache.fory.util.record.RecordUtils; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class GraalvmSupportRecordTest { + + public static class RegularClass { + public int intField; + public String stringField; + + public RegularClass() {} + + public RegularClass(int intField, String stringField) { + this.intField = intField; + this.stringField = stringField; + } + } + + private static class PrivateClass { + @SuppressWarnings("unused") + private final int value; + + @SuppressWarnings("unused") + public PrivateClass(int value) { + this.value = value; + } + } + + @Test + public void testIsRecordConstructorPublicAccessible_WithNonRecord() { + boolean result = GraalvmSupport.isRecordConstructorPublicAccessible(RegularClass.class); + Assert.assertFalse(result, "Non-Record class should return false"); + } + + @Test + public void testIsRecordConstructorPublicAccessible_WithObject() { + boolean result = GraalvmSupport.isRecordConstructorPublicAccessible(Object.class); + Assert.assertFalse(result, "Object class should return false"); + } + + @Test + public void testIsRecordConstructorPublicAccessible_WithString() { + boolean result = GraalvmSupport.isRecordConstructorPublicAccessible(String.class); + Assert.assertFalse(result, "String class should return false"); + } + + @Test + public void testObjectCreators_ReflectiveInstantiationFlag_WithRegularClass() { + boolean result = GraalvmSupport.needReflectionRegisterForCreation(RegularClass.class); + Assert.assertFalse( + result, + "RegularClass with no-arg constructor does not require reflective instantiation registration"); + } + + @Test + public void testBackwardCompatibility_OnOlderJDK() { + boolean result = GraalvmSupport.isRecordConstructorPublicAccessible(RegularClass.class); + Assert.assertFalse(result, "Should return false for non-Record classes on any JDK version"); + + try { + GraalvmSupport.isRecordConstructorPublicAccessible(Object.class); + GraalvmSupport.isRecordConstructorPublicAccessible(String.class); + GraalvmSupport.isRecordConstructorPublicAccessible(Integer.class); + Assert.assertTrue(true, "Method should not throw exceptions on older JDK versions"); + } catch (Exception e) { + Assert.fail("Method should not throw exceptions on older JDK versions", e); + } + } + + @Test + public void testObjectCreators_BackwardCompatibility() { + Assert.assertFalse(GraalvmSupport.needReflectionRegisterForCreation(RegularClass.class)); + Assert.assertFalse(GraalvmSupport.needReflectionRegisterForCreation(Object.class)); + Assert.assertFalse(GraalvmSupport.needReflectionRegisterForCreation(String.class)); + + Assert.assertTrue(GraalvmSupport.needReflectionRegisterForCreation(PrivateClass.class)); + + Assert.assertFalse(GraalvmSupport.needReflectionRegisterForCreation(Runnable.class)); + } + + @Test + public void testRecordUtilsIntegration() { + Assert.assertFalse(RecordUtils.isRecord(RegularClass.class)); + Assert.assertFalse(RecordUtils.isRecord(Object.class)); + Assert.assertFalse(RecordUtils.isRecord(String.class)); + + Assert.assertFalse(GraalvmSupport.isRecordConstructorPublicAccessible(RegularClass.class)); + Assert.assertFalse(GraalvmSupport.isRecordConstructorPublicAccessible(Object.class)); + Assert.assertFalse(GraalvmSupport.isRecordConstructorPublicAccessible(String.class)); + } +} diff --git a/java/fory-core/pom.xml b/java/fory-core/pom.xml index 10fd8138a1..3b8a084db4 100644 --- a/java/fory-core/pom.xml +++ b/java/fory-core/pom.xml @@ -25,7 +25,7 @@ org.apache.fory fory-parent - 0.13.0 + 0.13.0-SNAPSHOT 4.0.0 diff --git a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java index 9b61dbb98d..7cc0fbe36c 100644 --- a/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java +++ b/java/fory-core/src/main/java/org/apache/fory/reflect/ObjectCreators.java @@ -82,11 +82,11 @@ private static ObjectCreator creategetObjectCreator(Class type) { return new RecordObjectCreator<>(type); } Constructor noArgConstructor = ReflectionUtils.getNoArgConstructor(type); - if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && Platform.JAVA_VERSION >= 25) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { if (noArgConstructor != null) { return new DeclaredNoArgCtrObjectCreator<>(type); } else { - return new ParentNoArgCtrObjectCreator<>(type); + return new UnsafeObjectCreator<>(type); } } if (noArgConstructor == null) { diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java index 62fda33225..d31118121e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java @@ -431,6 +431,7 @@ public void register(Class cls, int classId) { registeredId2ClassInfo[id] = classInfo; extRegistry.registeredClasses.put(cls.getName(), cls); extRegistry.classIdGenerator++; + GraalvmSupport.registerClassForGraalvm(cls, fory.getConfig().getConfigHash()); } public void register(String className, int classId) { @@ -476,6 +477,7 @@ public void register(Class cls, String namespace, String name) { compositeNameBytes2ClassInfo.put( new TypeNameBytes(nsBytes.hashCode, nameBytes.hashCode), classInfo); extRegistry.registeredClasses.put(fullname, cls); + GraalvmSupport.registerClassForGraalvm(cls, fory.getConfig().getConfigHash()); } private void checkRegistration(Class cls, short classId, String name) { @@ -1792,10 +1794,13 @@ public void ensureSerializersCompiled() { try { fory.getJITContext().lock(); Serializers.newSerializer(fory, LambdaSerializer.STUB_LAMBDA_CLASS, LambdaSerializer.class); - Serializers.newSerializer( - fory, JdkProxySerializer.SUBT_PROXY.getClass(), JdkProxySerializer.class); + if (!GraalvmSupport.isGraalRuntime()) { + Serializers.newSerializer( + fory, JdkProxySerializer.SUBT_PROXY.getClass(), JdkProxySerializer.class); + } classInfoMap.forEach( (cls, classInfo) -> { + GraalvmSupport.registerClassForGraalvm(cls, fory.getConfig().getConfigHash()); if (classInfo.serializer == null) { if (isSerializable(classInfo.cls)) { createSerializer0(cls); diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index 4db04ecf95..436c1dc7ff 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -26,9 +26,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Type; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -36,7 +34,6 @@ import java.util.Set; import java.util.SortedMap; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import org.apache.fory.Fory; import org.apache.fory.annotation.Internal; @@ -572,34 +569,18 @@ public void setTypeChecker(TypeChecker typeChecker) { extRegistry.typeChecker = typeChecker; } - private static final ConcurrentMap GRAALVM_REGISTRY = - new ConcurrentHashMap<>(); - // CHECKSTYLE.OFF:MethodName public static void _addGraalvmClassRegistry(int foryConfigHash, ClassResolver classResolver) { // CHECKSTYLE.ON:MethodName if (GraalvmSupport.isGraalBuildtime()) { - GraalvmClassRegistry registry = - GRAALVM_REGISTRY.computeIfAbsent(foryConfigHash, k -> new GraalvmClassRegistry()); + GraalvmSupport.GraalvmClassRegistry registry = + GraalvmSupport.getGraalvmClassRegistry(foryConfigHash); registry.resolvers.add(classResolver); } } - static class GraalvmClassRegistry { - final List resolvers; - final Map, Class> serializerClassMap; - final Map> deserializerClassMap; - - private GraalvmClassRegistry() { - resolvers = Collections.synchronizedList(new ArrayList<>()); - serializerClassMap = new ConcurrentHashMap<>(); - deserializerClassMap = new ConcurrentHashMap<>(); - } - } - - final GraalvmClassRegistry getGraalvmClassRegistry() { - return GRAALVM_REGISTRY.computeIfAbsent( - fory.getConfig().getConfigHash(), k -> new GraalvmClassRegistry()); + final GraalvmSupport.GraalvmClassRegistry getGraalvmClassRegistry() { + return GraalvmSupport.getGraalvmClassRegistry(fory.getConfig().getConfigHash()); } final Class getGraalvmSerializerClass(Serializer serializer) { @@ -610,8 +591,9 @@ final Class getGraalvmSerializerClass(Serializer serialize } final Class getSerializerClassFromGraalvmRegistry(Class cls) { - GraalvmClassRegistry registry = getGraalvmClassRegistry(); - List classResolvers = registry.resolvers; + GraalvmSupport.GraalvmClassRegistry registry = getGraalvmClassRegistry(); + @SuppressWarnings("unchecked") + List classResolvers = (List) (List) registry.resolvers; if (classResolvers.isEmpty()) { return null; } @@ -639,8 +621,9 @@ final Class getSerializerClassFromGraalvmRegistry(Class private Class getMetaSharedDeserializerClassFromGraalvmRegistry( Class cls, ClassDef classDef) { - GraalvmClassRegistry registry = getGraalvmClassRegistry(); - List classResolvers = registry.resolvers; + GraalvmSupport.GraalvmClassRegistry registry = getGraalvmClassRegistry(); + @SuppressWarnings("unchecked") + List classResolvers = (List) (List) registry.resolvers; if (classResolvers.isEmpty()) { return null; } diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java index 34ac519183..33f0be0e8b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java @@ -97,6 +97,7 @@ import org.apache.fory.type.Generics; import org.apache.fory.type.TypeUtils; import org.apache.fory.type.Types; +import org.apache.fory.util.GraalvmSupport; import org.apache.fory.util.Preconditions; @SuppressWarnings({"unchecked", "rawtypes"}) @@ -166,6 +167,7 @@ public void register(Class type, int userTypeId) { ClassInfo classInfo = classInfoMap.get(type); if (type.isArray()) { buildClassInfo(type); + GraalvmSupport.registerClassForGraalvm(type, fory.getConfig().getConfigHash()); return; } Serializer serializer = null; @@ -255,6 +257,7 @@ private void register( String qualifiedName = qualifiedName(namespace, typeName); qualifiedType2ClassInfo.put(qualifiedName, classInfo); extRegistry.registeredClasses.put(qualifiedName, type); + GraalvmSupport.registerClassForGraalvm(type, fory.getConfig().getConfigHash()); if (serializer == null) { if (type.isEnum()) { classInfo.serializer = new EnumSerializer(fory, (Class) type); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java index 9c2bc7418c..38522d8dcb 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/JdkProxySerializer.java @@ -28,6 +28,7 @@ import org.apache.fory.memory.Platform; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.RefResolver; +import org.apache.fory.util.GraalvmSupport; import org.apache.fory.util.Preconditions; /** Serializer for jdk {@link Proxy}. */ @@ -62,7 +63,11 @@ private interface StubInterface { public JdkProxySerializer(Fory fory, Class cls) { super(fory, cls); if (cls != ReplaceStub.class) { - Preconditions.checkArgument(ReflectionUtils.isJdkProxy(cls), "Require a jdk proxy class"); + // Skip proxy class validation in GraalVM native image runtime to avoid issues with proxy + // detection + if (!GraalvmSupport.isGraalRuntime()) { + Preconditions.checkArgument(ReflectionUtils.isJdkProxy(cls), "Require a jdk proxy class"); + } } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java index 48d56e11dd..09d6d22e7a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java @@ -55,6 +55,8 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.Platform; +import org.apache.fory.reflect.ObjectCreator; +import org.apache.fory.reflect.ObjectCreators; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.resolver.ClassInfo; import org.apache.fory.resolver.FieldResolver; @@ -80,10 +82,64 @@ public class ObjectStreamSerializer extends AbstractObjectSerializer { private static final Logger LOG = LoggerFactory.getLogger(ObjectStreamSerializer.class); - private final SlotsInfo[] slotsInfos; + private final SlotInfo[] slotsInfos; + + /** + * Interface for slot information used in ObjectStreamSerializer. This allows both full SlotsInfo + * and minimal MinimalSlotsInfo implementations. + */ + /** + * Interface for slot information used in ObjectStreamSerializer. This allows both full SlotsInfo + * and minimal MinimalSlotsInfo implementations. + */ + private interface SlotInfo { + Class getCls(); + + StreamClassInfo getStreamClassInfo(); + + CompatibleSerializerBase getSlotsSerializer(); + + ForyObjectOutputStream getObjectOutputStream(); + + ForyObjectInputStream getObjectInputStream(); + + ObjectArray getFieldPool(); + + ObjectIntMap getFieldIndexMap(); + + FieldResolver getPutFieldsResolver(); + + CompatibleSerializer getCompatibleStreamSerializer(); + } + + /** + * Safe wrapper for ObjectStreamClass.lookup that handles GraalVM native image limitations. In + * GraalVM native image, ObjectStreamClass.lookup may fail for certain classes like Throwable due + * to missing SerializationConstructorAccessor. This method catches such errors and returns null, + * allowing the serializer to use alternative approaches like Unsafe.allocateInstance. + */ + private static ObjectStreamClass safeObjectStreamClassLookup(Class type) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + try { + return ObjectStreamClass.lookup(type); + } catch (Throwable e) { + // In GraalVM native image, ObjectStreamClass.lookup may fail for certain classes + // due to missing SerializationConstructorAccessor. We catch this and return null + // to allow fallback to Unsafe-based object creation. + LOG.warn( + "ObjectStreamClass.lookup failed for {} in GraalVM native image: {}", + type.getName(), + e.getMessage()); + return null; + } + } else { + // In regular JVM, use normal lookup + return ObjectStreamClass.lookup(type); + } + } public ObjectStreamSerializer(Fory fory, Class type) { - super(fory, type); + super(fory, type, createObjectCreatorForGraalVM(type)); if (!Serializable.class.isAssignableFrom(type)) { throw new IllegalArgumentException( String.format("Class %s should implement %s.", type, Serializable.class)); @@ -96,34 +152,62 @@ public ObjectStreamSerializer(Fory fory, Class type) { Externalizable.class.getName()); // stream serializer may be data serializer of ReplaceResolver serializer. fory.getClassResolver().setSerializerIfAbsent(type, this); - List slotsInfoList = new ArrayList<>(); + List slotsInfoList = new ArrayList<>(); Class end = type; // locate closest non-serializable superclass while (end != null && Serializable.class.isAssignableFrom(end)) { end = end.getSuperclass(); } while (type != end) { - slotsInfoList.add(new SlotsInfo(fory, type)); + try { + slotsInfoList.add(new SlotsInfo(fory, type)); + } catch (Exception e) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + LOG.warn( + "Failed to create SlotsInfo for {} in GraalVM native image, " + + "using minimal serialization support: {}", + type.getName(), + e.getMessage()); + // Create a minimal SlotsInfo that can work with Unsafe + slotsInfoList.add(new MinimalSlotsInfo(fory, type)); + } else { + throw e; + } + } type = type.getSuperclass(); } Collections.reverse(slotsInfoList); - slotsInfos = slotsInfoList.toArray(new SlotsInfo[0]); + slotsInfos = slotsInfoList.toArray(new SlotInfo[0]); + } + + /** + * Creates an appropriate ObjectCreator for GraalVM native image environment. In GraalVM, we + * prefer UnsafeObjectCreator to avoid serialization constructor issues. + */ + private static ObjectCreator createObjectCreatorForGraalVM(Class type) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + // In GraalVM native image, use Unsafe to avoid serialization constructor issues + return new ObjectCreators.UnsafeObjectCreator<>(type); + } else { + // In regular JVM, use the standard object creator + return ObjectCreators.getObjectCreator(type); + } } @Override public void write(MemoryBuffer buffer, Object value) { buffer.writeInt16((short) slotsInfos.length); try { - for (SlotsInfo slotsInfo : slotsInfos) { + for (SlotInfo slotsInfo : slotsInfos) { // create a classinfo to avoid null class bytes when class id is a // replacement id. - classResolver.writeClassInternal(buffer, slotsInfo.classInfo.getCls()); - StreamClassInfo streamClassInfo = slotsInfo.streamClassInfo; + classResolver.writeClassInternal(buffer, slotsInfo.getCls()); + StreamClassInfo streamClassInfo = slotsInfo.getStreamClassInfo(); Method writeObjectMethod = streamClassInfo.writeObjectMethod; if (writeObjectMethod == null) { - slotsInfo.slotsSerializer.write(buffer, value); + slotsInfo.getSlotsSerializer().write(buffer, value); } else { - ForyObjectOutputStream objectOutputStream = slotsInfo.objectOutputStream; + ForyObjectOutputStream objectOutputStream = slotsInfo.getObjectOutputStream(); Object oldObject = objectOutputStream.targetObject; MemoryBuffer oldBuffer = objectOutputStream.buffer; ForyObjectOutputStream.PutFieldImpl oldPutField = objectOutputStream.curPut; @@ -161,9 +245,9 @@ public Object read(MemoryBuffer buffer) { TreeMap callbacks = new TreeMap<>(Collections.reverseOrder()); for (int i = 0; i < numClasses; i++) { Class currentClass = classResolver.readClassInternal(buffer); - SlotsInfo slotsInfo = slotsInfos[slotIndex++]; - StreamClassInfo streamClassInfo = slotsInfo.streamClassInfo; - while (currentClass != slotsInfo.cls) { + SlotInfo slotsInfo = slotsInfos[slotIndex++]; + StreamClassInfo streamClassInfo = slotsInfo.getStreamClassInfo(); + while (currentClass != slotsInfo.getCls()) { // the receiver's version extends classes that are not extended by the sender's version. Method readObjectNoData = streamClassInfo.readObjectNoData; if (readObjectNoData != null) { @@ -177,14 +261,14 @@ public Object read(MemoryBuffer buffer) { } Method readObjectMethod = streamClassInfo.readObjectMethod; if (readObjectMethod == null) { - slotsInfo.slotsSerializer.readAndSetFields(buffer, obj); + slotsInfo.getSlotsSerializer().readAndSetFields(buffer, obj); } else { - ForyObjectInputStream objectInputStream = slotsInfo.objectInputStream; + ForyObjectInputStream objectInputStream = slotsInfo.getObjectInputStream(); MemoryBuffer oldBuffer = objectInputStream.buffer; Object oldObject = objectInputStream.targetObject; ForyObjectInputStream.GetFieldImpl oldGetField = objectInputStream.getField; ForyObjectInputStream.GetFieldImpl getField = - (ForyObjectInputStream.GetFieldImpl) slotsInfo.getFieldPool.popOrNull(); + (ForyObjectInputStream.GetFieldImpl) slotsInfo.getFieldPool().popOrNull(); if (getField == null) { getField = new ForyObjectInputStream.GetFieldImpl(slotsInfo); } @@ -205,7 +289,7 @@ public Object read(MemoryBuffer buffer) { objectInputStream.buffer = oldBuffer; objectInputStream.targetObject = oldObject; objectInputStream.getField = oldGetField; - slotsInfo.getFieldPool.add(getField); + slotsInfo.getFieldPool().add(getField); objectInputStream.callbacks = null; Arrays.fill(getField.vals, ForyObjectInputStream.NO_VALUE_STUB); } @@ -239,6 +323,10 @@ private static void throwSerializationException(Class type, Exception e) { e); } + /** + * Information about a class's stream methods (writeObject, readObject, readObjectNoData) and + * their optimized MethodHandle equivalents for fast invocation. + */ private static class StreamClassInfo { private final Method writeObjectMethod; private final Method readObjectMethod; @@ -249,7 +337,7 @@ private static class StreamClassInfo { private StreamClassInfo(Class type) { // ObjectStreamClass.lookup has cache inside, invocation cost won't be big. - ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(type); + ObjectStreamClass objectStreamClass = safeObjectStreamClassLookup(type); // In JDK17, set private jdk method accessible will fail by default, use ObjectStreamClass // instead, since it set accessible. writeObjectMethod = @@ -290,7 +378,12 @@ protected StreamClassInfo computeValue(Class type) { } }; - private static class SlotsInfo { + /** + * Full implementation of SlotInfo for handling object stream serialization. This class manages + * all the details of serializing and deserializing a single class in the class hierarchy using + * Java's ObjectInputStream/ObjectOutputStream protocol. + */ + private static class SlotsInfo implements SlotInfo { private final Class cls; private final ClassInfo classInfo; private final StreamClassInfo streamClassInfo; @@ -306,7 +399,7 @@ private static class SlotsInfo { public SlotsInfo(Fory fory, Class type) { this.cls = type; classInfo = fory.getClassResolver().newClassInfo(type, null, NO_CLASS_ID); - ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(type); + ObjectStreamClass objectStreamClass = safeObjectStreamClassLookup(type); streamClassInfo = STREAM_CLASS_INFO_CACHE.get(type); // `putFields/writeFields` will convert to fields value to be written by // `CompatibleSerializer`, @@ -345,8 +438,15 @@ public SlotsInfo(Fory fory, Class type) { } fieldIndexMap = new ObjectIntMap<>(4, 0.4f); List allFields = new ArrayList<>(); - for (ObjectStreamField serialField : objectStreamClass.getFields()) { - allFields.add(new ClassField(serialField.getName(), serialField.getType(), cls)); + if (objectStreamClass != null) { + for (ObjectStreamField serialField : objectStreamClass.getFields()) { + allFields.add(new ClassField(serialField.getName(), serialField.getType(), cls)); + } + } else { + // Fallback to field resolver when ObjectStreamClass is not available in GraalVM + for (FieldResolver.FieldInfo fieldInfo : fieldResolver.getAllFieldsList()) { + allFields.add(new ClassField(fieldInfo.getName(), fieldInfo.getField().getType(), cls)); + } } if (streamClassInfo.writeObjectMethod != null || streamClassInfo.readObjectMethod != null) { putFieldsResolver = new FieldResolver(fory, cls, true, allFields, new HashSet<>()); @@ -382,12 +482,143 @@ public SlotsInfo(Fory fory, Class type) { getFieldPool = new ObjectArray(); } + @Override + public Class getCls() { + return cls; + } + + @Override + public StreamClassInfo getStreamClassInfo() { + return streamClassInfo; + } + + @Override + public CompatibleSerializerBase getSlotsSerializer() { + return slotsSerializer; + } + + @Override + public ForyObjectOutputStream getObjectOutputStream() { + return objectOutputStream; + } + + @Override + public ForyObjectInputStream getObjectInputStream() { + return objectInputStream; + } + + @Override + public ObjectArray getFieldPool() { + return getFieldPool; + } + + @Override + public ObjectIntMap getFieldIndexMap() { + return fieldIndexMap; + } + + @Override + public FieldResolver getPutFieldsResolver() { + return putFieldsResolver; + } + + @Override + public CompatibleSerializer getCompatibleStreamSerializer() { + return compatibleStreamSerializer; + } + @Override public String toString() { return "SlotsInfo{" + "cls=" + cls + '}'; } } + /** + * Minimal SlotsInfo implementation for GraalVM native image when ObjectStreamClass.lookup fails. + * This provides basic serialization support using Unsafe-based object creation. + */ + private static class MinimalSlotsInfo implements SlotInfo { + private final Class cls; + private final ClassInfo classInfo; + private final StreamClassInfo streamClassInfo; + private CompatibleSerializerBase slotsSerializer; + private final ObjectIntMap fieldIndexMap; + private final FieldResolver putFieldsResolver; + private final CompatibleSerializer compatibleStreamSerializer; + private final ForyObjectOutputStream objectOutputStream; + private final ForyObjectInputStream objectInputStream; + private final ObjectArray getFieldPool; + + public MinimalSlotsInfo(Fory fory, Class type) { + // Initialize with minimal required fields + this.cls = type; + this.classInfo = fory.getClassResolver().newClassInfo(type, null, NO_CLASS_ID); + this.streamClassInfo = null; // Skip problematic ObjectStreamClass lookup + + // Create a basic CompatibleSerializer for field handling + FieldResolver fieldResolver = FieldResolver.of(fory, type, false, true); + this.slotsSerializer = new CompatibleSerializer(fory, type, fieldResolver); + + // Initialize other fields with safe defaults + this.fieldIndexMap = new ObjectIntMap<>(4, 0.4f); + this.putFieldsResolver = null; + this.compatibleStreamSerializer = null; + this.objectOutputStream = null; + this.objectInputStream = null; + this.getFieldPool = new ObjectArray(); + } + + @Override + public Class getCls() { + return cls; + } + + @Override + public StreamClassInfo getStreamClassInfo() { + return streamClassInfo; + } + + @Override + public CompatibleSerializerBase getSlotsSerializer() { + return slotsSerializer; + } + + @Override + public ForyObjectOutputStream getObjectOutputStream() { + return objectOutputStream; + } + + @Override + public ForyObjectInputStream getObjectInputStream() { + return objectInputStream; + } + + @Override + public ObjectArray getFieldPool() { + return getFieldPool; + } + + @Override + public ObjectIntMap getFieldIndexMap() { + return fieldIndexMap; + } + + @Override + public FieldResolver getPutFieldsResolver() { + return putFieldsResolver; + } + + @Override + public CompatibleSerializer getCompatibleStreamSerializer() { + return compatibleStreamSerializer; + } + + @Override + public String toString() { + return "MinimalSlotsInfo{" + "cls=" + cls + '}'; + } + } + /** * Implement serialization for object output with `writeObject/readObject` defined by java * serialization output spec. @@ -398,15 +629,15 @@ public String toString() { private static class ForyObjectOutputStream extends ObjectOutputStream { private final Fory fory; private final boolean compressInt; - private final SlotsInfo slotsInfo; + private final SlotInfo slotsInfo; private MemoryBuffer buffer; private Object targetObject; private boolean fieldsWritten; - protected ForyObjectOutputStream(SlotsInfo slotsInfo) throws IOException { + protected ForyObjectOutputStream(SlotInfo slotsInfo) throws IOException { super(); this.slotsInfo = slotsInfo; - this.fory = slotsInfo.slotsSerializer.fory; + this.fory = slotsInfo.getSlotsSerializer().fory; this.compressInt = fory.compressInt(); } @@ -428,21 +659,20 @@ public void writeUnshared(Object obj) throws IOException { * href="https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/input.html#the-objectinputstream.getfield-class">ObjectInputStream.GetField * @see ConcurrentHashMap */ - // See `defaultReadObject` in ConcurrentHashMap#readObject skip fields written by - // `writeFields()`. private class PutFieldImpl extends PutField { private final Object[] vals; PutFieldImpl() { - vals = new Object[slotsInfo.putFieldsResolver.getNumFields()]; + vals = new Object[slotsInfo.getPutFieldsResolver().getNumFields()]; } private void putValue(String name, Object val) { - int index = slotsInfo.fieldIndexMap.get(name, -1); + int index = slotsInfo.getFieldIndexMap().get(name, -1); if (index == -1) { throw new IllegalArgumentException( String.format( - "Field name %s not exist in class %s", name, slotsInfo.slotsSerializer.type)); + "Field name %s not exist in class %s", + name, slotsInfo.getSlotsSerializer().type)); } vals[index] = val; } @@ -495,7 +725,7 @@ public void put(String name, Object val) { @Deprecated @Override public void write(ObjectOutput out) throws IOException { - Class cls = slotsInfo.slotsSerializer.type; + Class cls = slotsInfo.getSlotsSerializer().getType(); throwUnsupportedEncodingException(cls); } } @@ -524,7 +754,7 @@ public void writeFields() throws IOException { if (curPut == null) { throw new NotActiveException("no current PutField object"); } - slotsInfo.compatibleStreamSerializer.writeFieldsValues(buffer, curPut.vals); + slotsInfo.getCompatibleStreamSerializer().writeFieldsValues(buffer, curPut.vals); Arrays.fill(curPut.vals, null); putFieldsCache.add(curPut); this.curPut = null; @@ -536,13 +766,13 @@ public void defaultWriteObject() throws IOException, NotActiveException { if (fieldsWritten) { throw new NotActiveException("not in writeObject invocation or fields already written"); } - slotsInfo.slotsSerializer.write(buffer, targetObject); + slotsInfo.getSlotsSerializer().write(buffer, targetObject); fieldsWritten = true; } @Override public void reset() throws IOException { - Class cls = slotsInfo.slotsSerializer.getType(); + Class cls = slotsInfo.getSlotsSerializer().getType(); // Fory won't invoke this method, throw exception if the user invokes it. throwUnsupportedEncodingException(cls); } @@ -659,7 +889,7 @@ public void writeUTF(String s) throws IOException { @Override public void useProtocolVersion(int version) throws IOException { - Class cls = slotsInfo.cls; + Class cls = slotsInfo.getCls(); throwUnsupportedEncodingException(cls); } @@ -683,15 +913,15 @@ public void close() throws IOException {} private static class ForyObjectInputStream extends ObjectInputStream { private final Fory fory; private final boolean compressInt; - private final SlotsInfo slotsInfo; + private final SlotInfo slotsInfo; private MemoryBuffer buffer; private Object targetObject; private GetFieldImpl getField; private boolean fieldsRead; private TreeMap callbacks; - protected ForyObjectInputStream(SlotsInfo slotsInfo) throws IOException { - this.fory = slotsInfo.slotsSerializer.fory; + protected ForyObjectInputStream(SlotInfo slotsInfo) throws IOException { + this.fory = slotsInfo.getSlotsSerializer().fory; this.compressInt = fory.compressInt(); this.slotsInfo = slotsInfo; } @@ -708,24 +938,28 @@ public Object readUnshared() { private static final Object NO_VALUE_STUB = new Object(); + /** + * Implementation of ObjectInputStream.GetField for reading fields that may not exist in the + * current class version. + */ private static class GetFieldImpl extends GetField { - private final SlotsInfo slotsInfo; + private final SlotInfo slotsInfo; private final Object[] vals; - GetFieldImpl(SlotsInfo slotsInfo) { + GetFieldImpl(SlotInfo slotsInfo) { this.slotsInfo = slotsInfo; - vals = new Object[slotsInfo.putFieldsResolver.getNumFields()]; + vals = new Object[slotsInfo.getPutFieldsResolver().getNumFields()]; Arrays.fill(vals, NO_VALUE_STUB); } @Override public ObjectStreamClass getObjectStreamClass() { - return ObjectStreamClass.lookup(slotsInfo.cls); + return safeObjectStreamClassLookup(slotsInfo.getCls()); } @Override public boolean defaulted(String name) throws IOException { - int index = slotsInfo.fieldIndexMap.get(name, -1); + int index = slotsInfo.getFieldIndexMap().get(name, -1); checkFieldExists(name, index); return vals[index] == NO_VALUE_STUB; } @@ -812,7 +1046,7 @@ public Object get(String name, Object val) throws IOException { } private Object getFieldValue(String name) { - int index = slotsInfo.fieldIndexMap.get(name, -1); + int index = slotsInfo.getFieldIndexMap().get(name, -1); checkFieldExists(name, index); return vals[index]; } @@ -821,7 +1055,8 @@ private void checkFieldExists(String name, int index) { if (index == -1) { throw new IllegalArgumentException( String.format( - "Field name %s not exist in class %s", name, slotsInfo.slotsSerializer.type)); + "Field name %s not exist in class %s", + name, slotsInfo.getSlotsSerializer().getType())); } } } @@ -833,7 +1068,7 @@ public GetField readFields() throws IOException { if (fieldsRead) { throw new NotActiveException("not in readObject invocation or fields already read"); } - slotsInfo.compatibleStreamSerializer.readFields(buffer, getField.vals); + slotsInfo.getCompatibleStreamSerializer().readFields(buffer, getField.vals); fieldsRead = true; return getField; } @@ -843,7 +1078,7 @@ public void defaultReadObject() throws IOException, ClassNotFoundException { if (fieldsRead) { throw new NotActiveException("not in readObject invocation or fields already read"); } - slotsInfo.slotsSerializer.readAndSetFields(buffer, targetObject); + slotsInfo.getSlotsSerializer().readAndSetFields(buffer, targetObject); fieldsRead = true; } diff --git a/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java b/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java index 7bfe2b00b1..942fbabc3a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/GraalvmSupport.java @@ -20,11 +20,19 @@ package org.apache.fory.util; import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.apache.fory.Fory; import org.apache.fory.exception.ForyException; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.serializer.Serializer; +import org.apache.fory.util.record.RecordUtils; /** A helper for Graalvm native image support. */ public class GraalvmSupport { @@ -37,6 +45,9 @@ public class GraalvmSupport { private static final String GRAAL_IMAGE_BUILDTIME = "buildtime"; private static final String GRAAL_IMAGE_RUNTIME = "runtime"; + private static final Map GRAALVM_REGISTRY = + new ConcurrentHashMap<>(); + static { String imageCode = System.getProperty(GRAAL_IMAGE_CODE_KEY); IN_GRAALVM_NATIVE_IMAGE = imageCode != null; @@ -54,6 +65,103 @@ public static boolean isGraalRuntime() { && GRAAL_IMAGE_RUNTIME.equals(System.getProperty(GRAAL_IMAGE_CODE_KEY)); } + /** + * Returns all classes registered for GraalVM native image compilation across all configurations. + */ + public static Set> getRegisteredClasses() { + return getAllRegisteredClasses(); + } + + /** Returns all proxy interfaces registered for GraalVM native image compilation. */ + public static Set> getProxyInterfaces() { + return getAllProxyInterfaces(); + } + + /** Clears all GraalVM native image registrations. Primarily for testing purposes. */ + public static void clearRegistrations() { + clearGraalvmRegistrations(); + } + + /** + * Get all registered classes across all GraalVM registries for native image compilation. + * + * @return unmodifiable set of all registered classes + */ + public static Set> getAllRegisteredClasses() { + Set> allClasses = ConcurrentHashMap.newKeySet(); + for (GraalvmClassRegistry registry : GRAALVM_REGISTRY.values()) { + allClasses.addAll(registry.registeredClasses); + } + return Collections.unmodifiableSet(allClasses); + } + + /** + * Get all registered proxy interfaces across all GraalVM registries for native image compilation. + * + * @return unmodifiable set of all registered proxy interfaces + */ + public static Set> getAllProxyInterfaces() { + Set> allInterfaces = ConcurrentHashMap.newKeySet(); + for (GraalvmClassRegistry registry : GRAALVM_REGISTRY.values()) { + allInterfaces.addAll(registry.proxyInterfaces); + } + return Collections.unmodifiableSet(allInterfaces); + } + + /** + * Register a class in the GraalVM registry for native image compilation. + * + * @param cls the class to register + * @param configHash the configuration hash for the Fory instance + */ + public static void registerClassForGraalvm(Class cls, int configHash) { + if (!IN_GRAALVM_NATIVE_IMAGE) { + return; + } + GraalvmClassRegistry registry = + GRAALVM_REGISTRY.computeIfAbsent(configHash, k -> new GraalvmClassRegistry()); + registry.registeredClasses.add(cls); + } + + /** + * Register a proxy interface in the GraalVM registry for native image compilation. + * + * @param proxyInterface the proxy interface to register + * @param configHash the configuration hash for the Fory instance + */ + public static void registerProxyInterface(Class proxyInterface, int configHash) { + if (!IN_GRAALVM_NATIVE_IMAGE) { + return; + } + if (proxyInterface == null) { + throw new NullPointerException("Proxy interface must not be null"); + } + if (!proxyInterface.isInterface()) { + throw new IllegalArgumentException( + "Proxy type must be an interface: " + proxyInterface.getName()); + } + GraalvmClassRegistry registry = + GRAALVM_REGISTRY.computeIfAbsent(configHash, k -> new GraalvmClassRegistry()); + registry.proxyInterfaces.add(proxyInterface); + } + + /** + * Register proxy support for GraalVM native image compilation. + * + * @param proxyInterface the proxy interface to register + */ + public static void registerProxySupport(Class proxyInterface) { + registerProxyInterface(proxyInterface, 0); + } + + /** Clear all GraalVM registrations. This is primarily for testing purposes. */ + public static void clearGraalvmRegistrations() { + for (GraalvmClassRegistry registry : GRAALVM_REGISTRY.values()) { + registry.registeredClasses.clear(); + registry.proxyInterfaces.clear(); + } + } + public static class GraalvmSerializerHolder extends Serializer { private final Class serializerClass; private Serializer serializer; @@ -96,4 +204,93 @@ private Serializer getSerializer() { public static ForyException throwNoArgCtrException(Class type) { throw new ForyException("Please provide a no-arg constructor for " + type); } + + public static boolean isRecordConstructorPublicAccessible(Class type) { + if (!RecordUtils.isRecord(type)) { + return false; + } + + try { + Constructor[] constructors = type.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + if (Modifier.isPublic(constructor.getModifiers())) { + Class[] paramTypes = constructor.getParameterTypes(); + boolean allParamsPublic = true; + for (Class paramType : paramTypes) { + if (!Modifier.isPublic(paramType.getModifiers())) { + allParamsPublic = false; + break; + } + } + if (allParamsPublic) { + return true; + } + } + } + } catch (Exception e) { + return false; + } + return false; + } + + /** + * Checks whether a class requires reflective instantiation handling in GraalVM. + * + *

Returns true when the class does not expose an accessible no-arg constructor and therefore + * needs reflective registration for instantiation during native image builds. + * + * @param type the class to check + * @return true if reflective instantiation handling is required, false otherwise + */ + public static boolean needReflectionRegisterForCreation(Class type) { + if (type.isInterface() + || Modifier.isAbstract(type.getModifiers()) + || type.isArray() + || type.isEnum() + || type.isAnonymousClass() + || type.isLocalClass()) { + return false; + } + Constructor[] constructors = type.getDeclaredConstructors(); + if (constructors.length == 0) { + return true; + } + for (Constructor constructor : constructors) { + if (constructor.getParameterCount() == 0) { + return false; + } + } + if (RecordUtils.isRecord(type)) { + return !isRecordConstructorPublicAccessible(type); + } + return true; + } + + /** + * Get the GraalVM class registry for a specific configuration hash. Package-private method for + * use by TypeResolver and ClassResolver. + */ + public static GraalvmClassRegistry getGraalvmClassRegistry(int configHash) { + if (!IN_GRAALVM_NATIVE_IMAGE) { + return new GraalvmClassRegistry(); + } + return GRAALVM_REGISTRY.computeIfAbsent(configHash, k -> new GraalvmClassRegistry()); + } + + /** GraalVM class registry. */ + public static class GraalvmClassRegistry { + public final List resolvers; + public final Map, Class> serializerClassMap; + public final Map> deserializerClassMap; + public final Set> registeredClasses; + public final Set> proxyInterfaces; + + private GraalvmClassRegistry() { + resolvers = Collections.synchronizedList(new ArrayList<>()); + serializerClassMap = new ConcurrentHashMap<>(); + deserializerClassMap = new ConcurrentHashMap<>(); + registeredClasses = ConcurrentHashMap.newKeySet(); + proxyInterfaces = ConcurrentHashMap.newKeySet(); + } + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java b/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java index 0fd3f7b87f..40aac69e37 100644 --- a/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java +++ b/java/fory-core/src/main/java/org/apache/fory/util/function/Functions.java @@ -71,8 +71,25 @@ public static List extractCapturedVariables( public static Object makeGetterFunction(Class cls, String methodName) { try { - return makeGetterFunction(cls.getDeclaredMethod(methodName)); + Method method = cls.getDeclaredMethod(methodName); + return makeGetterFunction(method); } catch (NoSuchMethodException e) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + // In GraalVM native image, getDeclaredMethod may fail for Record accessor methods + // Try using getMethods() which works for public methods without reflection config + try { + for (Method method : cls.getMethods()) { + if (method.getName().equals(methodName) && method.getParameterCount() == 0) { + return makeGetterFunction(method); + } + } + throw new NoSuchMethodException( + "No public no-arg method found: " + cls.getName() + "." + methodName + "()"); + } catch (NoSuchMethodException ex) { + throw new RuntimeException( + "Failed to create getter for " + cls.getName() + "." + methodName, ex); + } + } throw new RuntimeException(e); } } diff --git a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties index e65de39acd..b9fa3bf5a0 100644 --- a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties +++ b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties @@ -488,7 +488,10 @@ Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\ org.apache.fory.util.ClassLoaderUtils,\ org.apache.fory.util.DelayedRef,\ org.apache.fory.util.function.Functions,\ + org.apache.fory.graalvm.FeatureTestExample,\ + org.apache.fory.graalvm.Main,\ org.apache.fory.util.GraalvmSupport,\ + org.apache.fory.util.GraalvmSupport$GraalvmClassRegistry,\ org.apache.fory.util.GraalvmSupport$GraalvmSerializerHolder,\ org.apache.fory.util.LoaderBinding$1,\ org.apache.fory.util.LoaderBinding$StagingType,\ diff --git a/java/fory-graalvm-feature/pom.xml b/java/fory-graalvm-feature/pom.xml new file mode 100644 index 0000000000..bf6fe74014 --- /dev/null +++ b/java/fory-graalvm-feature/pom.xml @@ -0,0 +1,91 @@ + + + + + fory-parent + org.apache.fory + 0.13.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + fory-graalvm-feature + Fory GraalVM Feature + + + 1.8 + 1.8 + ${basedir}/.. + + + + + org.apache.fory + fory-core + ${project.version} + compile + + + org.graalvm.sdk + graal-sdk + 23.0.0 + provided + + + org.testng + testng + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.apache.fory.graalvm.feature + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + diff --git a/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java b/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java new file mode 100644 index 0000000000..7ccaa29a20 --- /dev/null +++ b/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java @@ -0,0 +1,95 @@ +/* + * 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.fory.graalvm.feature; + +import java.lang.reflect.Field; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.fory.util.GraalvmSupport; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + +/** + * GraalVM Feature for Apache Fory serialization framework. + * + *

This feature automatically registers necessary metadata for GraalVM native image compilation + * to ensure Fory serialization works correctly at runtime. It handles: + * + *

    + *
  • Registering classes that require reflective instantiation (no accessible no-arg + * constructor) + *
  • Registering field reflection access for serialization + *
  • Registering proxy interfaces for dynamic proxy creation + *
+ */ +public class ForyGraalVMFeature implements Feature { + + private final Set> processedClasses = ConcurrentHashMap.newKeySet(); + private final Set> processedProxyInterfaces = ConcurrentHashMap.newKeySet(); + + @Override + public void duringAnalysis(DuringAnalysisAccess access) { + boolean changed = false; + + for (Class clazz : GraalvmSupport.getRegisteredClasses()) { + if (processedClasses.add(clazz)) { + handleForyClass(clazz); + changed = true; + } + } + + for (Class proxyInterface : GraalvmSupport.getProxyInterfaces()) { + if (processedProxyInterfaces.add(proxyInterface)) { + RuntimeReflection.register(proxyInterface); + RuntimeReflection.register(proxyInterface.getMethods()); + changed = true; + } + } + + if (changed) { + access.requireAnalysisIteration(); + } + } + + public String getDescription() { + return "Fory GraalVM Feature: Registers classes for serialization and proxy support."; + } + + private void handleForyClass(Class clazz) { + if (GraalvmSupport.needReflectionRegisterForCreation(clazz)) { + try { + RuntimeReflection.registerForReflectiveInstantiation(clazz); + for (Field field : clazz.getDeclaredFields()) { + RuntimeReflection.register(field); + } + } catch (Exception e) { + throw new RuntimeException( + String.format( + "Failed to register class '%s' for GraalVM Native Image. " + + "This class lacks an accessible no-arg constructor. " + + "Please ensure fory-graalvm-feature is included in your native-image build.", + clazz.getName()), + e); + } + } + + RuntimeReflection.register(clazz); + } +} diff --git a/java/fory-graalvm-feature/src/main/resources/META-INF/services/org.graalvm.nativeimage.hosted.Feature b/java/fory-graalvm-feature/src/main/resources/META-INF/services/org.graalvm.nativeimage.hosted.Feature new file mode 100644 index 0000000000..72cf9f8397 --- /dev/null +++ b/java/fory-graalvm-feature/src/main/resources/META-INF/services/org.graalvm.nativeimage.hosted.Feature @@ -0,0 +1 @@ +org.apache.fory.graalvm.feature.ForyGraalVMFeature \ No newline at end of file diff --git a/java/fory-graalvm-feature/src/test/java/org/apache/fory/graalvm/feature/ForyGraalVMFeatureTest.java b/java/fory-graalvm-feature/src/test/java/org/apache/fory/graalvm/feature/ForyGraalVMFeatureTest.java new file mode 100644 index 0000000000..c64f3487a4 --- /dev/null +++ b/java/fory-graalvm-feature/src/test/java/org/apache/fory/graalvm/feature/ForyGraalVMFeatureTest.java @@ -0,0 +1,167 @@ +/* + * 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.fory.graalvm.feature; + +import static org.testng.Assert.*; + +import org.apache.fory.Fory; +import org.apache.fory.util.GraalvmSupport; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class ForyGraalVMFeatureTest { + + private ForyGraalVMFeature feature; + + public static class PublicNoArgConstructorClass { + private String field1; + private int field2; + } + + public static class ProtectedNoArgConstructorClass { + protected ProtectedNoArgConstructorClass() {} + } + + public static class PrivateParameterizedConstructorClass { + private String data; + + private PrivateParameterizedConstructorClass(String data) { + this.data = data; + } + } + + public interface SampleProxyInterface { + void execute(); + } + + public static class NonInterfaceProxy {} + + public enum SampleEnum { + VALUE + } + + @BeforeMethod + public void setUp() { + GraalvmSupport.clearRegistrations(); + feature = new ForyGraalVMFeature(); + } + + @AfterMethod + public void tearDown() { + GraalvmSupport.clearRegistrations(); + } + + @Test + public void testGetDescription() { + String description = feature.getDescription(); + assertEquals( + "Fory GraalVM Feature: Registers classes for serialization and proxy support.", + description); + } + + @Test + public void testObjectCreatorsDetection() { + assertTrue( + GraalvmSupport.needReflectionRegisterForCreation( + PrivateParameterizedConstructorClass.class), + "Class without no-arg constructor requires reflective instantiation registration"); + + assertFalse( + GraalvmSupport.needReflectionRegisterForCreation(PublicNoArgConstructorClass.class), + "Public no-arg constructor does not require reflective instantiation registration"); + + assertFalse( + GraalvmSupport.needReflectionRegisterForCreation(ProtectedNoArgConstructorClass.class), + "Protected no-arg constructor does not require reflective instantiation registration"); + + assertFalse( + GraalvmSupport.needReflectionRegisterForCreation(SampleEnum.class), + "Enums do not require reflective instantiation registration"); + } + + @Test + public void testForyStaticMethods() { + // Test that Fory static methods are accessible + assertNotNull(GraalvmSupport.getRegisteredClasses(), "Registered classes should not be null"); + + assertNotNull(GraalvmSupport.getProxyInterfaces(), "Proxy interfaces should not be null"); + } + + @Test + public void testFeatureInstantiation() { + assertNotNull(feature, "Feature should be instantiated"); + assertNotNull(feature.getDescription(), "Feature description should not be null"); + } + + @Test + public void testAddProxyInterfaceRejectsNull() { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + try { + GraalvmSupport.registerProxySupport(null); + fail("Null proxy interface should throw NullPointerException"); + } catch (NullPointerException expected) { + // expected + } + } else { + GraalvmSupport.registerProxySupport(null); + } + } + + @Test + public void testAddProxyInterfaceRejectsNonInterface() { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + try { + GraalvmSupport.registerProxySupport(NonInterfaceProxy.class); + fail("Non-interface proxy type should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expected + } + } else { + GraalvmSupport.registerProxySupport(NonInterfaceProxy.class); + } + } + + @Test + public void testClearRegistrationsResetsState() { + Fory builderInstance = Fory.builder().build(); + GraalvmSupport.clearRegistrations(); + builderInstance.register(PublicNoArgConstructorClass.class); + GraalvmSupport.registerProxySupport(SampleProxyInterface.class); + + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + assertFalse(GraalvmSupport.getRegisteredClasses().isEmpty()); + assertFalse(GraalvmSupport.getProxyInterfaces().isEmpty()); + + GraalvmSupport.clearRegistrations(); + + assertTrue(GraalvmSupport.getRegisteredClasses().isEmpty()); + assertTrue(GraalvmSupport.getProxyInterfaces().isEmpty()); + } else { + assertTrue(GraalvmSupport.getRegisteredClasses().isEmpty()); + assertTrue(GraalvmSupport.getProxyInterfaces().isEmpty()); + + GraalvmSupport.clearRegistrations(); + + assertTrue(GraalvmSupport.getRegisteredClasses().isEmpty()); + assertTrue(GraalvmSupport.getProxyInterfaces().isEmpty()); + } + } +} diff --git a/java/fory-simd/pom.xml b/java/fory-simd/pom.xml index 4f90782d42..a410045a06 100644 --- a/java/fory-simd/pom.xml +++ b/java/fory-simd/pom.xml @@ -25,7 +25,7 @@ org.apache.fory fory-parent - 0.13.0 + 0.13.0-SNAPSHOT 4.0.0 @@ -83,6 +83,7 @@ org.apache.maven.plugins maven-javadoc-plugin + false --add-modules=jdk.incubator.vector diff --git a/java/pom.xml b/java/pom.xml index c05b20c559..9d11435d1c 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -33,7 +33,7 @@ org.apache.fory fory-parent pom - 0.13.0 + 0.13.0-SNAPSHOT Fory Project Parent POM Apache Fory™ is a blazingly fast multi-language serialization framework powered by jit and zero-copy. @@ -67,6 +67,9 @@ 1.8 1.8 + UTF-8 + UTF-8 + false 32.1.2-jre 3.1.12 1.13 @@ -93,6 +96,7 @@ fory-simd + fory-graalvm-feature @@ -177,26 +181,6 @@ - - org.apache.maven.plugins - maven-javadoc-plugin - 3.1.0 - - - attach-javadocs - - jar - - - - - none - - Copyright © 2023-2025, The Apache Software Foundation. Apache Fory™, Fory™, and Apache - are either registered trademarks or trademarks of the Apache Software Foundation. - - - org.apache.maven.plugins maven-shade-plugin @@ -279,6 +263,32 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + + attach-javadocs + + jar + + + + + none + UTF-8 + UTF-8 + UTF-8 + false + false + false + + Copyright © 2023-2025, The Apache Software Foundation. Apache Fory™, Fory™, and Apache + are either registered trademarks or trademarks of the Apache Software Foundation. + + +