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.foryfory-parent
- 0.13.0
+ 0.13.0-SNAPSHOT../../java4.0.0
@@ -35,7 +35,6 @@
17UTF-80.9.28
- 5.8.1org.apache.fory.graalvm.Mainmain
@@ -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-forkpackage
-
- test-native
-
- test
-
- test
- false
@@ -184,17 +187,13 @@
java-agent
- exec
+ javatest
- 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.foryfory-parent
- 0.13.0
+ 0.13.0-SNAPSHOT4.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 extends Serializer>> 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 extends Serializer> getGraalvmSerializerClass(Serializer serializer) {
@@ -610,8 +591,9 @@ final Class extends Serializer> getGraalvmSerializerClass(Serializer serialize
}
final Class extends Serializer> 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 extends Serializer> getSerializerClassFromGraalvmRegistry(Class>
private Class extends Serializer> 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