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 7b15a546e feat(java): add dedicated exception serializers (#3536)
7b15a546e is described below

commit 7b15a546ead357497d03d8f137f810d223586b36
Author: Shawn Yang <[email protected]>
AuthorDate: Fri Apr 3 20:09:15 2026 +0800

    feat(java): add dedicated exception serializers (#3536)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 .../org/apache/fory/graalvm/ExceptionExample.java  | 110 +++++++
 .../main/java/org/apache/fory/graalvm/Main.java    |   1 +
 .../graalvm_tests/native-image.properties          |   1 +
 .../fory/exception/DeserializationException.java   |   2 +-
 .../org/apache/fory/resolver/ClassResolver.java    |  16 +-
 .../fory/serializer/ExceptionSerializers.java      | 318 +++++++++++++++++++++
 .../java/org/apache/fory/util/GraalvmSupport.java  |   3 +
 .../fory-core/native-image.properties              |   4 +
 .../src/test/java/org/apache/fory/ForyTest.java    |  16 +-
 .../fory/serializer/ExceptionSerializersTest.java  | 162 +++++++++++
 .../fory/graalvm/feature/ForyGraalVMFeature.java   |  15 +-
 11 files changed, 636 insertions(+), 12 deletions(-)

diff --git 
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ExceptionExample.java
 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ExceptionExample.java
new file mode 100644
index 000000000..11bf934b7
--- /dev/null
+++ 
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ExceptionExample.java
@@ -0,0 +1,110 @@
+/*
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.fory.Fory;
+import org.apache.fory.reflect.ReflectionUtils;
+import org.apache.fory.serializer.ExceptionSerializers;
+import org.apache.fory.util.Preconditions;
+
+public class ExceptionExample {
+  private static final Fory FORY =
+      Fory.builder()
+          .withName(ExceptionExample.class.getName())
+          .requireClassRegistration(true)
+          .withRefTracking(false)
+          .build();
+
+  static {
+    FORY.register(CustomException.class);
+  }
+
+  public static void main(String[] args) {
+    testBuiltInException();
+    testCustomException();
+    System.out.println("ExceptionExample succeed");
+  }
+
+  private static void testBuiltInException() {
+    IllegalArgumentException cause = new 
IllegalArgumentException("built-in-cause");
+    IllegalStateException value = new IllegalStateException("built-in", cause);
+    IllegalStateException copy = (IllegalStateException) 
FORY.deserialize(FORY.serialize(value));
+    Preconditions.checkArgument(
+        FORY.getTypeResolver().getSerializerClass(IllegalStateException.class)
+            == ExceptionSerializers.ExceptionSerializer.class);
+    Preconditions.checkArgument(copy.getMessage().equals(value.getMessage()));
+    Preconditions.checkArgument(copy.getCause() != null);
+    Preconditions.checkArgument(copy.getCause().getClass() == 
cause.getClass());
+    
Preconditions.checkArgument(copy.getCause().getMessage().equals(cause.getMessage()));
+    Preconditions.checkArgument(copy.getStackTrace().length == 
value.getStackTrace().length);
+    
Preconditions.checkArgument(copy.getStackTrace()[0].equals(value.getStackTrace()[0]));
+  }
+
+  private static void testCustomException() {
+    CustomException value =
+        new CustomException("custom-native")
+            .withParentCode(42)
+            .withTags(new ArrayList<>(Arrays.asList("left", "right")));
+    value.retryable = true;
+    CustomException copy = (CustomException) 
FORY.deserialize(FORY.serialize(value));
+    Preconditions.checkArgument(
+        FORY.getTypeResolver().getSerializerClass(CustomException.class)
+            == ExceptionSerializers.ExceptionSerializer.class);
+    Preconditions.checkArgument(copy.getMessage().equals(value.getMessage()));
+    Preconditions.checkArgument(copy.parentCode == value.parentCode);
+    Preconditions.checkArgument(copy.retryable == value.retryable);
+    Preconditions.checkArgument(copy.tags.equals(value.tags));
+    Preconditions.checkArgument(copy.getCause() == null);
+    Preconditions.checkArgument(
+        ReflectionUtils.getObjectFieldValue(
+                copy, ReflectionUtils.getField(Throwable.class, "cause"))
+            == copy);
+  }
+
+  public static class ParentException extends RuntimeException {
+    int parentCode;
+    boolean retryable;
+
+    public ParentException(String message) {
+      super(message);
+    }
+  }
+
+  public static class CustomException extends ParentException {
+    List<String> tags;
+
+    public CustomException(String message) {
+      super(message);
+    }
+
+    CustomException withParentCode(int parentCode) {
+      this.parentCode = parentCode;
+      return this;
+    }
+
+    CustomException withTags(List<String> tags) {
+      this.tags = tags;
+      return this;
+    }
+  }
+}
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 d84cf6561..021f5c833 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
@@ -42,6 +42,7 @@ public class Main {
     Benchmark.main(args);
     CollectionExample.main(args);
     ArrayExample.main(args);
+    ExceptionExample.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 c62793701..a39144f2b 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
@@ -34,6 +34,7 @@ Args=-H:+ReportExceptionStackTraces \
   org.apache.fory.graalvm.EnsureSerializerExample$CustomSerializer,\
   org.apache.fory.graalvm.CollectionExample,\
   org.apache.fory.graalvm.ArrayExample,\
+  org.apache.fory.graalvm.ExceptionExample,\
   org.apache.fory.graalvm.AbstractClassExample,\
   org.apache.fory.graalvm.Benchmark,\
   org.apache.fory.graalvm.FeatureTestExample,\
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/exception/DeserializationException.java
 
b/java/fory-core/src/main/java/org/apache/fory/exception/DeserializationException.java
index 1d6099ecb..5cd154c67 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/exception/DeserializationException.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/exception/DeserializationException.java
@@ -24,7 +24,7 @@ import java.util.List;
 /** Exception thrown when a deserialization operation fails. */
 public class DeserializationException extends ForyException {
 
-  private List<Object> readObjects;
+  private transient List<Object> readObjects;
 
   public DeserializationException(String message) {
     super(message);
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 3836d2ad6..6c93a9ad7 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
@@ -103,6 +103,7 @@ import org.apache.fory.serializer.BufferSerializers;
 import org.apache.fory.serializer.CodegenSerializer.LazyInitBeanSerializer;
 import org.apache.fory.serializer.CopyOnlyObjectSerializer;
 import org.apache.fory.serializer.EnumSerializer;
+import org.apache.fory.serializer.ExceptionSerializers;
 import org.apache.fory.serializer.ExternalizableSerializer;
 import org.apache.fory.serializer.ForyCopyableSerializer;
 import org.apache.fory.serializer.JavaSerializer;
@@ -322,6 +323,9 @@ public class ClassResolver extends TypeResolver {
     OptionalSerializers.registerDefaultSerializers(fory);
     CollectionSerializers.registerDefaultSerializers(fory);
     MapSerializers.registerDefaultSerializers(fory);
+    addDefaultSerializer(
+        StackTraceElement[].class,
+        new ArraySerializers.ObjectArraySerializer<>(fory, 
StackTraceElement[].class));
     addDefaultSerializer(Locale.class, new LocaleSerializer(fory));
     addDefaultSerializer(
         SerializedLambda.class, new SerializedLambdaSerializer(fory, 
SerializedLambda.class));
@@ -1213,6 +1217,12 @@ public class ClassResolver extends TypeResolver {
       throw new UnsupportedOperationException(
           String.format("Class %s doesn't support serialization.", cls));
     }
+    if (cls == StackTraceElement.class) {
+      return ExceptionSerializers.StackTraceElementSerializer.class;
+    }
+    if (Throwable.class.isAssignableFrom(cls)) {
+      return ExceptionSerializers.ExceptionSerializer.class;
+    }
     Class<? extends Serializer> serializerClass = 
getSerializerClassFromGraalvmRegistry(cls);
     if (serializerClass != null) {
       return serializerClass;
@@ -1695,12 +1705,6 @@ public class ClassResolver extends TypeResolver {
     } else {
       return extRegistry.typeChecker.checkType(this, cls.getName());
     }
-    // Don't take java Exception as secure in case future JDK introduce 
insecure JDK exception.
-    // if (Exception.class.isAssignableFrom(cls)
-    //     && cls.getName().startsWith("java.")
-    //     && !cls.getName().startsWith("java.sql")) {
-    //   return true;
-    // }
   }
 
   /**
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
new file mode 100644
index 000000000..06039728c
--- /dev/null
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/ExceptionSerializers.java
@@ -0,0 +1,318 @@
+/*
+ * 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.serializer;
+
+import static org.apache.fory.collection.Collections.ofHashSet;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import org.apache.fory.Fory;
+import org.apache.fory.builder.LayerMarkerClassGenerator;
+import org.apache.fory.memory.MemoryBuffer;
+import org.apache.fory.memory.Platform;
+import org.apache.fory.meta.TypeDef;
+import org.apache.fory.reflect.ObjectCreator;
+import org.apache.fory.reflect.ObjectCreators;
+import org.apache.fory.reflect.ReflectionUtils;
+import org.apache.fory.resolver.MetaContext;
+import org.apache.fory.util.GraalvmSupport;
+import org.apache.fory.util.unsafe._JDKAccess;
+
+/** Serializers for {@link Throwable} and {@link StackTraceElement}. */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public final class ExceptionSerializers {
+  private static final Set<Class<?>> THROWABLE_SUPER_CLASSES = 
ofHashSet(Throwable.class);
+
+  private ExceptionSerializers() {}
+
+  public static final class ExceptionSerializer<T extends Throwable> extends 
Serializer<T> {
+    private final ObjectCreator<T> objectCreator;
+    private volatile Serializer[] slotsSerializers;
+    private volatile boolean rebuildSlotsSerializersAtRuntime;
+
+    public ExceptionSerializer(Fory fory, Class<T> type) {
+      super(fory, type);
+      objectCreator = createThrowableObjectCreator(type);
+      slotsSerializers = buildSlotsSerializers(fory, type);
+      // Native-image runtime must rebuild slot serializers once so field 
accessors and
+      // descriptors are created against the runtime heap layout instead of 
reusing
+      // any build-time initialized state.
+      rebuildSlotsSerializersAtRuntime = 
GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+    }
+
+    @Override
+    public void write(MemoryBuffer buffer, T value) {
+      Serializer[] slotsSerializers = getSlotsSerializers();
+      fory.writeRef(buffer, value.getStackTrace());
+      fory.writeRef(buffer, value.getCause());
+      fory.writeStringRef(buffer, value.getMessage());
+      buffer.writeVarUint32(0);
+      for (Serializer slotsSerializer : slotsSerializers) {
+        slotsSerializer.write(buffer, value);
+      }
+    }
+
+    @Override
+    public T read(MemoryBuffer buffer) {
+      Serializer[] slotsSerializers = getSlotsSerializers();
+      T obj = objectCreator.newInstance();
+      refResolver.reference(obj);
+      StackTraceElement[] stackTrace = (StackTraceElement[]) 
fory.readRef(buffer);
+      Throwable cause = (Throwable) fory.readRef(buffer);
+      String detailMessage = fory.readStringRef(buffer);
+      skipExtraFields(buffer);
+      Platform.putObject(obj, ThrowableOffsets.DETAIL_MESSAGE_FIELD_OFFSET, 
detailMessage);
+      Platform.putObject(obj, ThrowableOffsets.CAUSE_FIELD_OFFSET, cause == 
null ? obj : cause);
+      Platform.putObject(obj, ThrowableOffsets.STACK_TRACE_FIELD_OFFSET, 
stackTrace);
+      readAndSetFields(fory, buffer, obj, slotsSerializers);
+      return obj;
+    }
+
+    private Serializer[] getSlotsSerializers() {
+      if (!rebuildSlotsSerializersAtRuntime || 
!GraalvmSupport.isGraalRuntime()) {
+        return slotsSerializers;
+      }
+      synchronized (this) {
+        if (rebuildSlotsSerializersAtRuntime) {
+          slotsSerializers = buildSlotsSerializers(fory, type);
+          rebuildSlotsSerializersAtRuntime = false;
+        }
+        return slotsSerializers;
+      }
+    }
+
+    private void skipExtraFields(MemoryBuffer buffer) {
+      int numExtraFields = buffer.readVarUint32();
+      for (int i = 0; i < numExtraFields; i++) {
+        fory.readString(buffer);
+        fory.readRef(buffer);
+      }
+    }
+  }
+
+  public static final class StackTraceElementSerializer extends 
Serializer<StackTraceElement> {
+    private static final MethodHandles.Lookup LOOKUP =
+        _JDKAccess._trustedLookup(StackTraceElement.class);
+    private static final MethodHandle CLASS_LOADER_NAME_GETTER =
+        getOptionalGetter("getClassLoaderName");
+    private static final MethodHandle MODULE_NAME_GETTER = 
getOptionalGetter("getModuleName");
+    private static final MethodHandle MODULE_VERSION_GETTER = 
getOptionalGetter("getModuleVersion");
+    private static final MethodHandle STACK_TRACE_ELEMENT_CTR_V1 =
+        ReflectionUtils.getCtrHandle(
+            StackTraceElement.class, String.class, String.class, String.class, 
int.class);
+    private static final MethodHandle STACK_TRACE_ELEMENT_CTR_V2 =
+        getOptionalCtr(
+            String.class,
+            String.class,
+            String.class,
+            String.class,
+            String.class,
+            String.class,
+            int.class);
+
+    public StackTraceElementSerializer(Fory fory) {
+      super(fory, StackTraceElement.class, false, true);
+    }
+
+    @Override
+    public void write(MemoryBuffer buffer, StackTraceElement value) {
+      fory.writeStringRef(buffer, invokeStringGetter(CLASS_LOADER_NAME_GETTER, 
value));
+      fory.writeStringRef(buffer, invokeStringGetter(MODULE_NAME_GETTER, 
value));
+      fory.writeStringRef(buffer, invokeStringGetter(MODULE_VERSION_GETTER, 
value));
+      fory.writeStringRef(buffer, value.getClassName());
+      fory.writeStringRef(buffer, value.getMethodName());
+      fory.writeStringRef(buffer, value.getFileName());
+      buffer.writeInt32(value.getLineNumber());
+      buffer.writeVarUint32(0);
+    }
+
+    @Override
+    public StackTraceElement read(MemoryBuffer buffer) {
+      String classLoaderName = fory.readStringRef(buffer);
+      String moduleName = fory.readStringRef(buffer);
+      String moduleVersion = fory.readStringRef(buffer);
+      String declaringClass = fory.readStringRef(buffer);
+      String methodName = fory.readStringRef(buffer);
+      String fileName = fory.readStringRef(buffer);
+      int lineNumber = buffer.readInt32();
+      int numExtraFields = buffer.readVarUint32();
+      for (int i = 0; i < numExtraFields; i++) {
+        fory.readString(buffer);
+        fory.readRef(buffer);
+      }
+      return newStackTraceElement(
+          classLoaderName,
+          moduleName,
+          moduleVersion,
+          declaringClass,
+          methodName,
+          fileName,
+          lineNumber);
+    }
+
+    private static MethodHandle getOptionalGetter(String methodName) {
+      try {
+        return LOOKUP.findVirtual(
+            StackTraceElement.class, methodName, 
MethodType.methodType(String.class));
+      } catch (NoSuchMethodException e) {
+        return null;
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    private static MethodHandle getOptionalCtr(Class<?>... parameterTypes) {
+      try {
+        return LOOKUP.findConstructor(
+            StackTraceElement.class, MethodType.methodType(void.class, 
parameterTypes));
+      } catch (NoSuchMethodException e) {
+        return null;
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    private static String invokeStringGetter(MethodHandle getter, 
StackTraceElement value) {
+      if (getter == null) {
+        return null;
+      }
+      try {
+        return (String) getter.invoke(value);
+      } catch (Throwable t) {
+        throw new RuntimeException(t);
+      }
+    }
+
+    private static StackTraceElement newStackTraceElement(
+        String classLoaderName,
+        String moduleName,
+        String moduleVersion,
+        String declaringClass,
+        String methodName,
+        String fileName,
+        int lineNumber) {
+      try {
+        if (STACK_TRACE_ELEMENT_CTR_V2 != null) {
+          return (StackTraceElement)
+              STACK_TRACE_ELEMENT_CTR_V2.invoke(
+                  classLoaderName,
+                  moduleName,
+                  moduleVersion,
+                  declaringClass,
+                  methodName,
+                  fileName,
+                  lineNumber);
+        }
+        return (StackTraceElement)
+            STACK_TRACE_ELEMENT_CTR_V1.invoke(declaringClass, methodName, 
fileName, lineNumber);
+      } catch (Throwable t) {
+        throw new RuntimeException(t);
+      }
+    }
+  }
+
+  private static <T extends Throwable> ObjectCreator<T> 
createThrowableObjectCreator(
+      Class<T> type) {
+    if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return new ObjectCreators.UnsafeObjectCreator<>(type);
+    }
+    if (ReflectionUtils.getCtrHandle(type, false) != null) {
+      return ObjectCreators.getObjectCreator(type);
+    }
+    return new ObjectCreators.ParentNoArgCtrObjectCreator<>(type);
+  }
+
+  private static <T> Serializer[] buildSlotsSerializers(Fory fory, Class<T> 
type) {
+    List<Serializer> serializers = new ArrayList<>();
+    int layerIndex = 0;
+    while (!THROWABLE_SUPER_CLASSES.contains(type)) {
+      Serializer slotsSerializer;
+      if (fory.getConfig().isCompatible()) {
+        TypeDef layerTypeDef = fory.getTypeResolver().getTypeDef(type, false);
+        Class<?> layerMarkerClass = 
LayerMarkerClassGenerator.getOrCreate(fory, type, layerIndex);
+        slotsSerializer = new MetaSharedLayerSerializer(fory, type, 
layerTypeDef, layerMarkerClass);
+      } else {
+        slotsSerializer = new ObjectSerializer<>(fory, type, false);
+      }
+      serializers.add(slotsSerializer);
+      type = (Class<T>) type.getSuperclass();
+      layerIndex++;
+    }
+    Collections.reverse(serializers);
+    return serializers.toArray(new Serializer[0]);
+  }
+
+  private static void readAndSetFields(
+      Fory fory, MemoryBuffer buffer, Object target, Serializer[] 
slotsSerializers) {
+    for (Serializer slotsSerializer : slotsSerializers) {
+      if (slotsSerializer instanceof MetaSharedLayerSerializer) {
+        MetaSharedLayerSerializer metaSerializer = (MetaSharedLayerSerializer) 
slotsSerializer;
+        if (fory.getConfig().isMetaShareEnabled()) {
+          readAndSkipLayerClassMeta(fory, buffer);
+        }
+        metaSerializer.readAndSetFields(buffer, target);
+      } else {
+        ((ObjectSerializer) slotsSerializer).readAndSetFields(buffer, target);
+      }
+    }
+  }
+
+  private static void readAndSkipLayerClassMeta(Fory fory, MemoryBuffer 
buffer) {
+    MetaContext metaContext = fory.getSerializationContext().getMetaContext();
+    if (metaContext == null) {
+      return;
+    }
+    int indexMarker = buffer.readVarUint32Small14();
+    boolean isRef = (indexMarker & 1) == 1;
+    if (isRef) {
+      return;
+    }
+    long typeDefId = buffer.readInt64();
+    TypeDef.skipTypeDef(buffer, typeDefId);
+    metaContext.readTypeInfos.add(null);
+  }
+
+  private static final class ThrowableOffsets {
+    // Graalvm unsafe offset substitution support: Make the call followed by a 
field store
+    // directly or by a sign extend node followed directly by a field store.
+    private static final long DETAIL_MESSAGE_FIELD_OFFSET;
+    private static final long CAUSE_FIELD_OFFSET;
+    private static final long STACK_TRACE_FIELD_OFFSET;
+
+    static {
+      try {
+        Field detailMessageField = 
Throwable.class.getDeclaredField("detailMessage");
+        DETAIL_MESSAGE_FIELD_OFFSET = 
Platform.UNSAFE.objectFieldOffset(detailMessageField);
+        Field causeField = Throwable.class.getDeclaredField("cause");
+        CAUSE_FIELD_OFFSET = Platform.UNSAFE.objectFieldOffset(causeField);
+        Field stackTraceField = Throwable.class.getDeclaredField("stackTrace");
+        STACK_TRACE_FIELD_OFFSET = 
Platform.UNSAFE.objectFieldOffset(stackTraceField);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+}
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 ad6c976b3..388f06905 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
@@ -34,6 +34,7 @@ import org.apache.fory.serializer.ArraySerializers;
 import org.apache.fory.serializer.BufferSerializers;
 import org.apache.fory.serializer.CodegenSerializer;
 import org.apache.fory.serializer.EnumSerializer;
+import org.apache.fory.serializer.ExceptionSerializers;
 import org.apache.fory.serializer.ExternalizableSerializer;
 import org.apache.fory.serializer.JavaSerializer;
 import org.apache.fory.serializer.JdkProxySerializer;
@@ -106,6 +107,8 @@ public class GraalvmSupport {
     registerDefaultSerializerClass(MapSerializer.class);
     
registerDefaultSerializerClass(CodegenSerializer.LazyInitBeanSerializer.class);
     registerDefaultSerializerClass(ExternalizableSerializer.class);
+    
registerDefaultSerializerClass(ExceptionSerializers.ExceptionSerializer.class);
+    
registerDefaultSerializerClass(ExceptionSerializers.StackTraceElementSerializer.class);
     registerDefaultSerializerClass(ObjectStreamSerializer.class);
     registerDefaultSerializerClass(JavaSerializer.class);
     registerDefaultSerializerClass(ObjectSerializer.class);
diff --git 
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
 
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
index c2a4e70be..0435038e2 100644
--- 
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
+++ 
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
@@ -342,6 +342,10 @@ 
Args=--initialize-at-build-time=org.apache.fory.memory.MemoryBuffer,\
     org.apache.fory.serializer.MetaSharedSerializer,\
     org.apache.fory.serializer.MetaSharedLayerSerializer,\
     org.apache.fory.serializer.EnumSerializer,\
+    org.apache.fory.serializer.ExceptionSerializers,\
+    org.apache.fory.serializer.ExceptionSerializers$ExceptionSerializer,\
+    
org.apache.fory.serializer.ExceptionSerializers$StackTraceElementSerializer,\
+    org.apache.fory.serializer.ExceptionSerializers$ThrowableOffsets,\
     org.apache.fory.serializer.collection.SubListSerializers,\
     
org.apache.fory.serializer.collection.SubListSerializers$SubListViewSerializer,\
     
org.apache.fory.serializer.collection.SubListSerializers$SubListSerializer,\
diff --git a/java/fory-core/src/test/java/org/apache/fory/ForyTest.java 
b/java/fory-core/src/test/java/org/apache/fory/ForyTest.java
index 508b00fa0..fba4505c1 100644
--- a/java/fory-core/src/test/java/org/apache/fory/ForyTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/ForyTest.java
@@ -69,6 +69,7 @@ import org.apache.fory.memory.Platform;
 import org.apache.fory.resolver.MetaContext;
 import org.apache.fory.serializer.ArraySerializersTest;
 import org.apache.fory.serializer.EnumSerializerTest;
+import org.apache.fory.serializer.ExceptionSerializers;
 import org.apache.fory.serializer.ObjectSerializer;
 import org.apache.fory.serializer.Serializer;
 import org.apache.fory.test.bean.BeanA;
@@ -232,8 +233,19 @@ public class ForyTest extends ForyTestBase {
 
   @Test
   public void testSerializeException() {
-    Fory fory = 
Fory.builder().withLanguage(Language.JAVA).withRefTracking(true).build();
-    fory.serialize(new Exception());
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .withRefTracking(true)
+            .requireClassRegistration(true)
+            .build();
+    Exception value = new Exception("test-serialize-exception");
+    Exception copy = serDe(fory, value);
+    Assert.assertEquals(
+        fory.getTypeResolver().getSerializerClass(Exception.class),
+        ExceptionSerializers.ExceptionSerializer.class);
+    Assert.assertEquals(copy.getMessage(), value.getMessage());
+    Assert.assertEquals(copy.getStackTrace()[0], value.getStackTrace()[0]);
   }
 
   @Test(dataProvider = "referenceTrackingConfig")
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
new file mode 100644
index 000000000..2868b3a6b
--- /dev/null
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/ExceptionSerializersTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.serializer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.fory.Fory;
+import org.apache.fory.ForyTestBase;
+import org.apache.fory.config.CompatibleMode;
+import org.apache.fory.reflect.ReflectionUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class ExceptionSerializersTest extends ForyTestBase {
+  @Test(dataProvider = "javaFory")
+  public void testBuiltInThrowableRoundTrip(Fory fory) {
+    IllegalArgumentException cause = new 
IllegalArgumentException("inner-cause");
+    IllegalStateException value = new IllegalStateException("outer-message", 
cause);
+
+    IllegalStateException copy = serDe(fory, value);
+
+    Assert.assertEquals(
+        fory.getTypeResolver().getSerializerClass(value.getClass()),
+        ExceptionSerializers.ExceptionSerializer.class);
+    Assert.assertEquals(copy.getClass(), value.getClass());
+    Assert.assertEquals(copy.getMessage(), value.getMessage());
+    Assert.assertNotNull(copy.getCause());
+    Assert.assertEquals(copy.getCause().getClass(), cause.getClass());
+    Assert.assertEquals(copy.getCause().getMessage(), cause.getMessage());
+    Assert.assertEquals(copy.getStackTrace().length, 
value.getStackTrace().length);
+    Assert.assertEquals(copy.getStackTrace()[0], value.getStackTrace()[0]);
+  }
+
+  @Test(dataProvider = "javaFory")
+  public void testStackTraceElementRoundTrip(Fory fory) {
+    StackTraceElement value = new Exception().getStackTrace()[0];
+
+    StackTraceElement copy = serDe(fory, value);
+
+    Assert.assertEquals(
+        fory.getTypeResolver().getSerializerClass(StackTraceElement.class),
+        ExceptionSerializers.StackTraceElementSerializer.class);
+    Assert.assertEquals(copy, value);
+  }
+
+  @Test
+  public void testThrowableWithoutRefTrackingKeepsSelfCauseField() {
+    Fory fory = builder().withRefTracking(false).withCodegen(false).build();
+    CustomException value =
+        new CustomException("self-cause")
+            .withParentCode(7)
+            .withTags(new ArrayList<>(Arrays.asList("a", "b")));
+
+    CustomException copy = serDe(fory, value);
+
+    Assert.assertNull(copy.getCause());
+    Assert.assertSame(
+        ReflectionUtils.getObjectFieldValue(
+            copy, ReflectionUtils.getField(Throwable.class, "cause")),
+        copy);
+    Assert.assertEquals(copy.parentCode, value.parentCode);
+    Assert.assertEquals(copy.tags, value.tags);
+  }
+
+  @Test
+  public void testBuiltInThrowableWithClassRegistrationRequired() {
+    Fory fory =
+        
builder().requireClassRegistration(true).withRefTracking(false).withCodegen(false).build();
+    IllegalStateException value =
+        new IllegalStateException("registered-built-in", new 
IllegalArgumentException("cause"));
+
+    IllegalStateException copy = serDe(fory, value);
+
+    Assert.assertEquals(copy.getMessage(), value.getMessage());
+    Assert.assertNotNull(copy.getCause());
+    Assert.assertEquals(copy.getCause().getClass(), 
value.getCause().getClass());
+    Assert.assertEquals(copy.getCause().getMessage(), 
value.getCause().getMessage());
+    Assert.assertEquals(copy.getStackTrace()[0], value.getStackTrace()[0]);
+  }
+
+  @Test
+  public void testThrowableCompatibleModeRoundTrip() {
+    Fory fory =
+        builder()
+            .withRefTracking(true)
+            .withCodegen(false)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .build();
+    CustomException cause =
+        new CustomException("cause")
+            .withParentCode(1)
+            .withTags(new ArrayList<>(Arrays.asList("x")));
+    CustomException value =
+        new CustomException("custom", cause)
+            .withParentCode(9)
+            .withTags(new ArrayList<>(Arrays.asList("left", "right")));
+    value.retryable = true;
+
+    CustomException copy = serDe(fory, value);
+
+    Assert.assertEquals(copy.getMessage(), value.getMessage());
+    Assert.assertNotNull(copy.getCause());
+    Assert.assertEquals(copy.getCause().getClass(), cause.getClass());
+    Assert.assertEquals(copy.getCause().getMessage(), cause.getMessage());
+    Assert.assertEquals(copy.parentCode, value.parentCode);
+    Assert.assertEquals(copy.tags, value.tags);
+    Assert.assertEquals(copy.retryable, value.retryable);
+  }
+
+  public static class ParentException extends RuntimeException {
+    int parentCode;
+    boolean retryable;
+
+    public ParentException(String message) {
+      super(message);
+    }
+
+    public ParentException(String message, Throwable cause) {
+      super(message, cause);
+    }
+  }
+
+  public static class CustomException extends ParentException {
+    List<String> tags;
+
+    public CustomException(String message) {
+      super(message);
+    }
+
+    public CustomException(String message, Throwable cause) {
+      super(message, cause);
+    }
+
+    CustomException withParentCode(int parentCode) {
+      this.parentCode = parentCode;
+      return this;
+    }
+
+    CustomException withTags(List<String> tags) {
+      this.tags = tags;
+      return this;
+    }
+  }
+}
diff --git 
a/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java
 
b/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java
index 6d84d3fe0..9aae3424b 100644
--- 
a/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java
+++ 
b/java/fory-graalvm-feature/src/main/java/org/apache/fory/graalvm/feature/ForyGraalVMFeature.java
@@ -107,9 +107,7 @@ public class ForyGraalVMFeature implements Feature {
 
   private void registerClass(Class<?> clazz) {
     RuntimeReflection.register(clazz);
-    registerFields(clazz);
-    registerMethods(clazz);
-    registerConstructors(clazz);
+    registerHierarchyMembers(clazz);
     if (Serializable.class.isAssignableFrom(clazz)) {
       registerSerializableHierarchy(clazz);
     }
@@ -121,6 +119,17 @@ public class ForyGraalVMFeature implements Feature {
     }
   }
 
+  private void registerHierarchyMembers(Class<?> clazz) {
+    for (Class<?> current = clazz;
+        current != null && current != Object.class;
+        current = current.getSuperclass()) {
+      RuntimeReflection.register(current);
+      registerFields(current);
+      registerMethods(current);
+      registerConstructors(current);
+    }
+  }
+
   private void registerSerializerClass(Class<?> clazz) {
     registerConstructors(clazz);
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to