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]

Reply via email to