This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 102769518 fix(java): fix abstract enum and abstract array
serialization for GraalVM (#3095)
102769518 is described below
commit 102769518d7f6d175898008fe8f902d579c83dbf
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Dec 29 14:54:12 2025 +0800
fix(java): fix abstract enum and abstract array serialization for GraalVM
(#3095)
## Why?
Fix GitHub issue #2695: Abstract Class Corner Cases in Fory
Serialization
Two problems were identified:
1. **Abstract Object Arrays**: Serializing arrays with abstract
component types (e.g., `AbstractBase[]`) would fail during
`ensureSerializersCompiled()` because abstract types were incorrectly
flagged as non-serializable.
2. **Abstract Enums**: Enums with abstract methods (where each enum
value is an anonymous inner class) would throw `InsecureException`
because the enum value classes (e.g., `AbstractEnum$1`) were not
recognized as secure.
## What does this PR do?
### 1. Fix `TypeResolver.isSerializable()`
- Check for enums before checking if abstract - enums are always
serializable even if they have abstract methods.
### 2. Fix `ClassResolver.isSecure()`
- For enum value classes (anonymous inner classes of abstract enums),
check if the declaring enum class is secure instead of requiring the
inner class itself to be registered.
### 3. Fix `ClassResolver.createSerializer()`
- For enum value classes, reuse the serializer from the declaring enum
class instead of creating a new one. This is more efficient and avoids
polluting the class registry.
### 4. Fix `ClassResolver.ensureSerializersCompiled()`
- For abstract arrays, check if the component type is serializable
before trying to create a serializer.
- For GraalVM builds, also create and cache serializers for enum value
classes when processing abstract enums.
### 5. Add comprehensive tests
- Unit tests in `ClassResolverTest` for abstract enum serialization and
abstract object array serialization.
- GraalVM integration test `AbstractClassExample` that tests abstract
enums, abstract enum arrays, abstract object arrays, and containers with
abstract types.
## Related issues
Closes #2695
## Does this PR introduce any user-facing change?
No user-facing API changes. Users can now serialize:
- Enums with abstract methods (abstract enums)
- Arrays with abstract component types containing concrete instances
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A - Bug fix only
---
.../apache/fory/graalvm/AbstractClassExample.java | 251 +++++++++++++++++++++
.../main/java/org/apache/fory/graalvm/Main.java | 1 +
.../graalvm_tests/native-image.properties | 1 +
.../org/apache/fory/resolver/ClassResolver.java | 41 +++-
.../org/apache/fory/resolver/TypeResolver.java | 4 +
.../apache/fory/resolver/ClassResolverTest.java | 102 +++++++++
6 files changed, 396 insertions(+), 4 deletions(-)
diff --git
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/AbstractClassExample.java
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/AbstractClassExample.java
new file mode 100644
index 000000000..9273bcc31
--- /dev/null
+++
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/AbstractClassExample.java
@@ -0,0 +1,251 @@
+/*
+ * 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.util.Arrays;
+import java.util.Objects;
+import org.apache.fory.Fory;
+import org.apache.fory.util.Preconditions;
+
+/**
+ * Test for abstract class corner cases in GraalVM native image. This tests
the fix for issue #2695:
+ * Abstract enums (enums with abstract methods) and arrays of abstract types.
+ */
+public class AbstractClassExample {
+
+ // Abstract enum with abstract methods - each enum value is an anonymous
inner class
+ public enum AbstractEnum {
+ VALUE1 {
+ @Override
+ public int getValue() {
+ return 1;
+ }
+ },
+ VALUE2 {
+ @Override
+ public int getValue() {
+ return 2;
+ }
+ };
+
+ public abstract int getValue();
+ }
+
+ // Abstract base class for testing abstract object arrays
+ public abstract static class AbstractBase {
+ public int id;
+
+ public abstract String getType();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AbstractBase that = (AbstractBase) o;
+ return id == that.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+ }
+
+ // Concrete implementation 1
+ public static class ConcreteA extends AbstractBase {
+ public String name;
+
+ @Override
+ public String getType() {
+ return "A";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ ConcreteA concreteA = (ConcreteA) o;
+ return Objects.equals(name, concreteA.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), name);
+ }
+ }
+
+ // Concrete implementation 2
+ public static class ConcreteB extends AbstractBase {
+ public long value;
+
+ @Override
+ public String getType() {
+ return "B";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ ConcreteB concreteB = (ConcreteB) o;
+ return value == concreteB.value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), value);
+ }
+ }
+
+ // Container class that holds abstract enum and abstract array
+ public static class Container {
+ public AbstractEnum enumValue;
+ public AbstractEnum[] enumArray;
+ public AbstractBase[] baseArray;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Container container = (Container) o;
+ return enumValue == container.enumValue
+ && Arrays.equals(enumArray, container.enumArray)
+ && Arrays.equals(baseArray, container.baseArray);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(enumValue);
+ result = 31 * result + Arrays.hashCode(enumArray);
+ result = 31 * result + Arrays.hashCode(baseArray);
+ return result;
+ }
+ }
+
+ private static final Fory FORY;
+
+ static {
+ FORY =
+ Fory.builder()
+ .withName(AbstractClassExample.class.getName())
+ .registerGuavaTypes(false)
+ .build();
+ // Register enum type - abstract enums need to be registered
+ // The fix for issue #2695 ensures that registering an abstract enum
+ // also registers its inner classes (the anonymous enum value classes)
+ FORY.register(AbstractEnum.class);
+ // Register concrete types
+ FORY.register(ConcreteA.class);
+ FORY.register(ConcreteB.class);
+ FORY.register(Container.class);
+ // Register array types - the abstract component type should be handled
correctly
+ FORY.register(AbstractBase[].class);
+ FORY.register(AbstractEnum[].class);
+ // Ensure serializers are compiled - this is where the fix for issue #2695
matters
+ FORY.ensureSerializersCompiled();
+ }
+
+ public static void main(String[] args) {
+ FORY.reset();
+
+ // Test abstract enum serialization
+ testAbstractEnum();
+
+ // Test abstract enum array serialization
+ testAbstractEnumArray();
+
+ // Test abstract object array serialization
+ testAbstractObjectArray();
+
+ // Test container with abstract types
+ testContainer();
+
+ System.out.println("AbstractClassExample succeed");
+ }
+
+ private static void testAbstractEnum() {
+ byte[] bytes1 = FORY.serialize(AbstractEnum.VALUE1);
+ AbstractEnum result1 = FORY.deserialize(bytes1, AbstractEnum.class);
+ Preconditions.checkArgument(result1 == AbstractEnum.VALUE1, "VALUE1 should
match");
+ Preconditions.checkArgument(result1.getValue() == 1, "VALUE1.getValue()
should be 1");
+
+ byte[] bytes2 = FORY.serialize(AbstractEnum.VALUE2);
+ AbstractEnum result2 = FORY.deserialize(bytes2, AbstractEnum.class);
+ Preconditions.checkArgument(result2 == AbstractEnum.VALUE2, "VALUE2 should
match");
+ Preconditions.checkArgument(result2.getValue() == 2, "VALUE2.getValue()
should be 2");
+ }
+
+ private static void testAbstractEnumArray() {
+ AbstractEnum[] array = new AbstractEnum[] {AbstractEnum.VALUE1,
AbstractEnum.VALUE2};
+ byte[] bytes = FORY.serialize(array);
+ AbstractEnum[] result = FORY.deserialize(bytes, AbstractEnum[].class);
+ Preconditions.checkArgument(Arrays.equals(array, result), "Enum arrays
should match");
+ Preconditions.checkArgument(result[0].getValue() == 1,
"result[0].getValue() should be 1");
+ Preconditions.checkArgument(result[1].getValue() == 2,
"result[1].getValue() should be 2");
+ }
+
+ private static void testAbstractObjectArray() {
+ ConcreteA a = new ConcreteA();
+ a.id = 1;
+ a.name = "test";
+
+ ConcreteB b = new ConcreteB();
+ b.id = 2;
+ b.value = 100L;
+
+ AbstractBase[] array = new AbstractBase[] {a, b};
+ byte[] bytes = FORY.serialize(array);
+ AbstractBase[] result = FORY.deserialize(bytes, AbstractBase[].class);
+
+ Preconditions.checkArgument(result.length == 2, "Array length should be
2");
+ Preconditions.checkArgument(result[0] instanceof ConcreteA, "result[0]
should be ConcreteA");
+ Preconditions.checkArgument(result[1] instanceof ConcreteB, "result[1]
should be ConcreteB");
+ Preconditions.checkArgument(result[0].equals(a), "result[0] should equal
a");
+ Preconditions.checkArgument(result[1].equals(b), "result[1] should equal
b");
+ Preconditions.checkArgument(
+ "A".equals(result[0].getType()), "result[0].getType() should be 'A'");
+ Preconditions.checkArgument(
+ "B".equals(result[1].getType()), "result[1].getType() should be 'B'");
+ }
+
+ private static void testContainer() {
+ ConcreteA a = new ConcreteA();
+ a.id = 10;
+ a.name = "containerTest";
+
+ ConcreteB b = new ConcreteB();
+ b.id = 20;
+ b.value = 200L;
+
+ Container container = new Container();
+ container.enumValue = AbstractEnum.VALUE1;
+ container.enumArray = new AbstractEnum[] {AbstractEnum.VALUE2,
AbstractEnum.VALUE1};
+ container.baseArray = new AbstractBase[] {a, b};
+
+ byte[] bytes = FORY.serialize(container);
+ Container result = FORY.deserialize(bytes, Container.class);
+
+ Preconditions.checkArgument(container.equals(result), "Container should
match");
+ Preconditions.checkArgument(
+ result.enumValue.getValue() == 1, "container.enumValue.getValue()
should be 1");
+ }
+}
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 74a02e5c4..7a2322b29 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,6 +40,7 @@ public class Main {
Benchmark.main(args);
CollectionExample.main(args);
ArrayExample.main(args);
+ AbstractClassExample.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 f919616b7..7337d3ad4 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,6 +32,7 @@ Args=-H:+ReportExceptionStackTraces \
org.apache.fory.graalvm.EnsureSerializerExample$CustomSerializer,\
org.apache.fory.graalvm.CollectionExample,\
org.apache.fory.graalvm.ArrayExample,\
+ org.apache.fory.graalvm.AbstractClassExample,\
org.apache.fory.graalvm.Benchmark,\
org.apache.fory.graalvm.FeatureTestExample,\
org.apache.fory.graalvm.FeatureTestExample$PrivateConstructorClass,\
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 a65282cdc..5ae366a31 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
@@ -1384,6 +1384,15 @@ public class ClassResolver extends TypeResolver {
}
}
+ // For enum value classes (anonymous inner classes of abstract enums),
+ // reuse the serializer from the declaring enum class
+ if (!cls.isEnum() && Enum.class.isAssignableFrom(cls) && cls !=
Enum.class) {
+ Class<?> enclosingClass = cls.getEnclosingClass();
+ if (enclosingClass != null && enclosingClass.isEnum()) {
+ return getSerializer(enclosingClass);
+ }
+ }
+
if (extRegistry.serializerFactory != null) {
Serializer serializer =
extRegistry.serializerFactory.createSerializer(fory, cls);
if (serializer != null) {
@@ -1470,6 +1479,14 @@ public class ClassResolver extends TypeResolver {
if (cls.isArray()) {
return isSecure(TypeUtils.getArrayComponent(cls));
}
+ // For enum value classes (anonymous inner classes of abstract enums),
+ // check if the declaring enum class is secure
+ if (!cls.isEnum() && Enum.class.isAssignableFrom(cls) && cls !=
Enum.class) {
+ Class<?> enclosingClass = cls.getEnclosingClass();
+ if (enclosingClass != null && enclosingClass.isEnum()) {
+ return isSecure(enclosingClass);
+ }
+ }
if (fory.getConfig().requireClassRegistration()) {
return Functions.isLambda(cls)
|| ReflectionUtils.isJdkProxy(cls)
@@ -1958,20 +1975,36 @@ public class ClassResolver extends TypeResolver {
createSerializer0(cls);
}
if (cls.isArray()) {
- // Also create serializer for the component type
- createSerializer0(TypeUtils.getArrayComponent(cls));
+ // Also create serializer for the component type if it's
serializable
+ Class<?> componentType = TypeUtils.getArrayComponent(cls);
+ if (isSerializable(componentType)) {
+ createSerializer0(componentType);
+ }
}
}
// Always ensure array class serializers and their component type
serializers
// are registered in GraalVM registry, since ObjectArraySerializer
needs
// the component type serializer at construction time
if (cls.isArray() && GraalvmSupport.isGraalBuildtime()) {
- // First ensure component type serializer is registered
+ // First ensure component type serializer is registered if it's
serializable
Class<?> componentType = TypeUtils.getArrayComponent(cls);
- createSerializer0(componentType);
+ if (isSerializable(componentType)) {
+ createSerializer0(componentType);
+ }
// Then register the array serializer
createSerializer0(cls);
}
+ // For abstract enums, also create and store serializers for enum
value classes
+ // so they are available at GraalVM runtime
+ if (cls.isEnum() && GraalvmSupport.isGraalBuildtime()) {
+ for (Object enumConstant : cls.getEnumConstants()) {
+ Class<?> enumValueClass = enumConstant.getClass();
+ if (enumValueClass != cls) {
+ // Get serializer for the enum value class (will reuse the
enum's serializer)
+ getSerializer(enumValueClass);
+ }
+ }
+ }
});
if (GraalvmSupport.isGraalBuildtime()) {
classInfoCache = NIL_CLASS_INFO;
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 24ccfc8a2..5e905533f 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
@@ -481,6 +481,10 @@ public abstract class TypeResolver {
public abstract ClassDef getTypeDef(Class<?> cls, boolean resolveParent);
public final boolean isSerializable(Class<?> cls) {
+ // Enums are always serializable, even if abstract (enums with abstract
methods)
+ if (cls.isEnum()) {
+ return true;
+ }
if (ReflectionUtils.isAbstract(cls) || cls.isInterface()) {
return false;
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
b/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
index 1328536e6..6c088cfdd 100644
---
a/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
@@ -564,4 +564,106 @@ public class ClassResolverTest extends ForyTestBase {
fory.getClassResolver().getSerializer(abs2Test.getClass()).getClass(),
AbstractCustomSerializer.class);
}
+
+ // Test enum with abstract methods (which makes the enum class abstract)
+ enum AbstractEnum {
+ VALUE1 {
+ @Override
+ public int getValue() {
+ return 1;
+ }
+ },
+ VALUE2 {
+ @Override
+ public int getValue() {
+ return 2;
+ }
+ };
+
+ public abstract int getValue();
+ }
+
+ @Test
+ public void testAbstractEnumIsSerializable() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ ClassResolver classResolver = fory.getClassResolver();
+ // Abstract enums should be serializable
+ Assert.assertTrue(classResolver.isSerializable(AbstractEnum.class));
+ // The concrete enum value classes should also be serializable
+
Assert.assertTrue(classResolver.isSerializable(AbstractEnum.VALUE1.getClass()));
+
Assert.assertTrue(classResolver.isSerializable(AbstractEnum.VALUE2.getClass()));
+ }
+
+ @Test
+ public void testAbstractEnumSerialization() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ // Serialize and deserialize abstract enum values
+ Assert.assertEquals(AbstractEnum.VALUE1, serDe(fory, AbstractEnum.VALUE1));
+ Assert.assertEquals(AbstractEnum.VALUE2, serDe(fory, AbstractEnum.VALUE2));
+ Assert.assertEquals(1, ((AbstractEnum) serDe(fory,
AbstractEnum.VALUE1)).getValue());
+ Assert.assertEquals(2, ((AbstractEnum) serDe(fory,
AbstractEnum.VALUE2)).getValue());
+ }
+
+ @Test
+ public void testAbstractObjectArraySerialization() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ // Create an array of abstract type with concrete instances
+ AbsTest[] array = new AbsTest[2];
+ SubAbsTest item1 = new SubAbsTest();
+ item1.setF1(10);
+ item1.f2 = 100L;
+ Sub2AbsTest item2 = new Sub2AbsTest();
+ item2.setF1(20);
+ item2.f2 = 200L;
+ item2.f3 = "test";
+ array[0] = item1;
+ array[1] = item2;
+
+ AbsTest[] result = serDe(fory, array);
+ Assert.assertEquals(result.length, 2);
+ Assert.assertEquals(result[0].getF1(), 10);
+ Assert.assertEquals(((SubAbsTest) result[0]).f2, 100L);
+ Assert.assertEquals(result[1].getF1(), 20);
+ Assert.assertEquals(((Sub2AbsTest) result[1]).f2, 200L);
+ Assert.assertEquals(((Sub2AbsTest) result[1]).f3, "test");
+ }
+
+ @Test
+ public void testAbstractObjectArrayWithRegistration() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(true).build();
+ // Register the concrete types but not the abstract type
+ fory.register(SubAbsTest.class);
+ fory.register(Sub2AbsTest.class);
+ fory.register(AbsTest[].class);
+
+ AbsTest[] array = new AbsTest[2];
+ SubAbsTest item1 = new SubAbsTest();
+ item1.setF1(10);
+ item1.f2 = 100L;
+ Sub2AbsTest item2 = new Sub2AbsTest();
+ item2.setF1(20);
+ item2.f2 = 200L;
+ item2.f3 = "test";
+ array[0] = item1;
+ array[1] = item2;
+
+ AbsTest[] result = serDe(fory, array);
+ Assert.assertEquals(result.length, 2);
+ Assert.assertEquals(result[0].getF1(), 10);
+ Assert.assertEquals(result[1].getF1(), 20);
+ }
+
+ @Test
+ public void testAbstractEnumArraySerialization() {
+ Fory fory =
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+ // Create an array of abstract enum type
+ AbstractEnum[] array = new AbstractEnum[] {AbstractEnum.VALUE1,
AbstractEnum.VALUE2};
+
+ AbstractEnum[] result = serDe(fory, array);
+ Assert.assertEquals(result.length, 2);
+ Assert.assertEquals(result[0], AbstractEnum.VALUE1);
+ Assert.assertEquals(result[1], AbstractEnum.VALUE2);
+ Assert.assertEquals(result[0].getValue(), 1);
+ Assert.assertEquals(result[1].getValue(), 2);
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]