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 0c04a845a fix(java): serialize suppressed exceptions (#3663)
0c04a845a is described below

commit 0c04a845aac3d3f5123979716577a7dad63cc44f
Author: Shawn Yang <[email protected]>
AuthorDate: Fri May 8 22:30:51 2026 +0800

    fix(java): serialize suppressed exceptions (#3663)
    
    ## 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`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## 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  |  8 +++
 .../org/apache/fory/resolver/TypeResolver.java     |  2 +-
 .../fory/serializer/ExceptionSerializers.java      | 25 ++++++-
 .../fory/resolver/GraalvmRuntimeArrayTest.java     | 82 ++++++++++++++++++++++
 .../fory/serializer/ExceptionSerializersTest.java  | 45 ++++++++++++
 5 files changed, 159 insertions(+), 3 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
index 11bf934b7..8acf5aee1 100644
--- 
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
@@ -48,6 +48,7 @@ public class ExceptionExample {
   private static void testBuiltInException() {
     IllegalArgumentException cause = new 
IllegalArgumentException("built-in-cause");
     IllegalStateException value = new IllegalStateException("built-in", cause);
+    value.addSuppressed(new RuntimeException("built-in-suppressed"));
     IllegalStateException copy = (IllegalStateException) 
FORY.deserialize(FORY.serialize(value));
     Preconditions.checkArgument(
         FORY.getTypeResolver().getSerializerClass(IllegalStateException.class)
@@ -58,6 +59,9 @@ public class ExceptionExample {
     
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]));
+    Preconditions.checkArgument(copy.getSuppressed().length == 1);
+    Preconditions.checkArgument(copy.getSuppressed()[0].getClass() == 
RuntimeException.class);
+    
Preconditions.checkArgument(copy.getSuppressed()[0].getMessage().equals("built-in-suppressed"));
   }
 
   private static void testCustomException() {
@@ -66,6 +70,7 @@ public class ExceptionExample {
             .withParentCode(42)
             .withTags(new ArrayList<>(Arrays.asList("left", "right")));
     value.retryable = true;
+    value.addSuppressed(new IllegalStateException("custom-suppressed"));
     CustomException copy = (CustomException) 
FORY.deserialize(FORY.serialize(value));
     Preconditions.checkArgument(
         FORY.getTypeResolver().getSerializerClass(CustomException.class)
@@ -75,6 +80,9 @@ public class ExceptionExample {
     Preconditions.checkArgument(copy.retryable == value.retryable);
     Preconditions.checkArgument(copy.tags.equals(value.tags));
     Preconditions.checkArgument(copy.getCause() == null);
+    Preconditions.checkArgument(copy.getSuppressed().length == 1);
+    Preconditions.checkArgument(copy.getSuppressed()[0].getClass() == 
IllegalStateException.class);
+    
Preconditions.checkArgument(copy.getSuppressed()[0].getMessage().equals("custom-suppressed"));
     Preconditions.checkArgument(
         ReflectionUtils.getObjectFieldValue(
                 copy, ReflectionUtils.getField(Throwable.class, "cause"))
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 4c690c9a0..27463aafe 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
@@ -1802,7 +1802,7 @@ public abstract class TypeResolver {
       }
     }
     if (GraalvmSupport.isGraalRuntime()) {
-      if (Functions.isLambda(cls) || ReflectionUtils.isJdkProxy(cls)) {
+      if (cls.isArray() || Functions.isLambda(cls) || 
ReflectionUtils.isJdkProxy(cls)) {
         return null;
       }
       throw new RuntimeException(String.format("Class %s is not registered", 
cls));
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
index 5bbb5c822..07e5df779 100644
--- 
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
@@ -73,11 +73,16 @@ public final class ExceptionSerializers {
 
     @Override
     public void write(WriteContext writeContext, T value) {
-      MemoryBuffer buffer = writeContext.getBuffer();
       Serializer[] slotsSerializers = getSlotsSerializers();
       writeContext.writeRef(value.getStackTrace());
       writeContext.writeRef(value.getCause());
       writeContext.writeStringRef(value.getMessage());
+      Throwable[] suppressedExceptions = value.getSuppressed();
+      MemoryBuffer buffer = writeContext.getBuffer();
+      buffer.writeVarUInt32(suppressedExceptions.length);
+      for (Throwable suppressedException : suppressedExceptions) {
+        writeContext.writeRef(suppressedException);
+      }
       buffer.writeVarUInt32(0);
       for (Serializer slotsSerializer : slotsSerializers) {
         slotsSerializer.write(writeContext, value);
@@ -86,17 +91,19 @@ public final class ExceptionSerializers {
 
     @Override
     public T read(ReadContext readContext) {
-      MemoryBuffer buffer = readContext.getBuffer();
       Serializer[] slotsSerializers = getSlotsSerializers();
       T obj = objectCreator.newInstance();
       readContext.reference(obj);
       StackTraceElement[] stackTrace = (StackTraceElement[]) 
readContext.readRef();
       Throwable cause = (Throwable) readContext.readRef();
       String detailMessage = readContext.readStringRef();
+      List<Throwable> suppressedExceptions = 
readSuppressedExceptions(readContext);
       skipExtraFields(readContext);
       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);
+      Platform.putObject(
+          obj, ThrowableOffsets.SUPPRESSED_EXCEPTIONS_FIELD_OFFSET, 
suppressedExceptions);
       readAndSetFields(readContext, obj, slotsSerializers, config);
       return obj;
     }
@@ -315,12 +322,23 @@ public final class ExceptionSerializers {
     metaReadContext.readTypeInfos.add(null);
   }
 
+  private static List<Throwable> readSuppressedExceptions(ReadContext 
readContext) {
+    MemoryBuffer buffer = readContext.getBuffer();
+    int numSuppressedExceptions = buffer.readVarUInt32();
+    List<Throwable> suppressedExceptionsList = new 
ArrayList<>(numSuppressedExceptions);
+    for (int i = 0; i < numSuppressedExceptions; i++) {
+      suppressedExceptionsList.add((Throwable) readContext.readRef());
+    }
+    return suppressedExceptionsList;
+  }
+
   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;
+    private static final long SUPPRESSED_EXCEPTIONS_FIELD_OFFSET;
 
     static {
       try {
@@ -330,6 +348,9 @@ public final class ExceptionSerializers {
         CAUSE_FIELD_OFFSET = Platform.UNSAFE.objectFieldOffset(causeField);
         Field stackTraceField = Throwable.class.getDeclaredField("stackTrace");
         STACK_TRACE_FIELD_OFFSET = 
Platform.UNSAFE.objectFieldOffset(stackTraceField);
+        Field suppressedExceptionsField = 
Throwable.class.getDeclaredField("suppressedExceptions");
+        SUPPRESSED_EXCEPTIONS_FIELD_OFFSET =
+            Platform.UNSAFE.objectFieldOffset(suppressedExceptionsField);
       } catch (Exception e) {
         throw new RuntimeException(e);
       }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java
new file mode 100644
index 000000000..449c6dc68
--- /dev/null
+++ 
b/java/fory-core/src/test/java/org/apache/fory/resolver/GraalvmRuntimeArrayTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.resolver;
+
+import static org.testng.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.fory.Fory;
+import org.apache.fory.serializer.ArraySerializers;
+import org.testng.annotations.Test;
+
+public class GraalvmRuntimeArrayTest {
+  @Test
+  public void testGraalvmRuntimeFallsBackForUnregisteredArrayClass() throws 
Exception {
+    String javaBin =
+        System.getProperty("java.home") + File.separator + "bin" + 
File.separator + "java";
+    Process process =
+        new ProcessBuilder(
+                javaBin,
+                "-Dorg.graalvm.nativeimage.imagecode=runtime",
+                "-cp",
+                System.getProperty("java.class.path"),
+                GraalvmRuntimeArrayMain.class.getName())
+            .redirectErrorStream(true)
+            .start();
+    String output = readFully(process.getInputStream());
+    assertEquals(process.waitFor(), 0, output);
+  }
+
+  private static String readFully(InputStream inputStream) throws IOException {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    byte[] buffer = new byte[1024];
+    int read;
+    while ((read = inputStream.read(buffer)) != -1) {
+      outputStream.write(buffer, 0, read);
+    }
+    return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
+  }
+
+  public static final class GraalvmRuntimeArrayMain {
+    public static void main(String[] args) {
+      Fory fory =
+          Fory.builder()
+              .withCodegen(false)
+              .requireClassRegistration(true)
+              .suppressClassRegistrationWarnings(true)
+              .build();
+      Class<?> serializerClass = 
fory.getTypeResolver().getSerializerClass(Throwable[].class);
+      if (serializerClass != ArraySerializers.ObjectArraySerializer.class) {
+        throw new AssertionError("Unexpected serializer class: " + 
serializerClass);
+      }
+      Throwable[] value = {new RuntimeException("array-element")};
+      Throwable[] copy = (Throwable[]) fory.deserialize(fory.serialize(value));
+      if (copy.length != 1
+          || copy[0].getClass() != RuntimeException.class
+          || !"array-element".equals(copy[0].getMessage())) {
+        throw new AssertionError("Unexpected round-trip result");
+      }
+    }
+  }
+}
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
index 66886ac67..2288837ea 100644
--- 
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
@@ -33,6 +33,8 @@ public class ExceptionSerializersTest extends ForyTestBase {
   public void testBuiltInThrowableRoundTrip(Fory fory) {
     IllegalArgumentException cause = new 
IllegalArgumentException("inner-cause");
     IllegalStateException value = new IllegalStateException("outer-message", 
cause);
+    value.addSuppressed(new RuntimeException("suppressed-1"));
+    value.addSuppressed(new IllegalArgumentException("suppressed-2"));
 
     IllegalStateException copy = serDe(fory, value);
 
@@ -46,6 +48,11 @@ public class ExceptionSerializersTest extends ForyTestBase {
     Assert.assertEquals(copy.getCause().getMessage(), cause.getMessage());
     Assert.assertEquals(copy.getStackTrace().length, 
value.getStackTrace().length);
     Assert.assertEquals(copy.getStackTrace()[0], value.getStackTrace()[0]);
+    Assert.assertEquals(copy.getSuppressed().length, 
value.getSuppressed().length);
+    Assert.assertEquals(copy.getSuppressed()[0].getClass(), 
RuntimeException.class);
+    Assert.assertEquals(copy.getSuppressed()[0].getMessage(), "suppressed-1");
+    Assert.assertEquals(copy.getSuppressed()[1].getClass(), 
IllegalArgumentException.class);
+    Assert.assertEquals(copy.getSuppressed()[1].getMessage(), "suppressed-2");
   }
 
   @Test(dataProvider = "javaFory")
@@ -71,6 +78,7 @@ public class ExceptionSerializersTest extends ForyTestBase {
     CustomException copy = serDe(fory, value);
 
     Assert.assertNull(copy.getCause());
+    Assert.assertEquals(copy.getSuppressed().length, 0);
     Assert.assertSame(
         ReflectionUtils.getObjectFieldValue(
             copy, ReflectionUtils.getField(Throwable.class, "cause")),
@@ -85,6 +93,7 @@ public class ExceptionSerializersTest extends ForyTestBase {
         
builder().requireClassRegistration(true).withRefTracking(false).withCodegen(false).build();
     IllegalStateException value =
         new IllegalStateException("registered-built-in", new 
IllegalArgumentException("cause"));
+    value.addSuppressed(new RuntimeException("registered-suppressed"));
 
     IllegalStateException copy = serDe(fory, value);
 
@@ -93,6 +102,22 @@ public class ExceptionSerializersTest extends ForyTestBase {
     Assert.assertEquals(copy.getCause().getClass(), 
value.getCause().getClass());
     Assert.assertEquals(copy.getCause().getMessage(), 
value.getCause().getMessage());
     Assert.assertEquals(copy.getStackTrace()[0], value.getStackTrace()[0]);
+    Assert.assertEquals(copy.getSuppressed().length, 1);
+    Assert.assertEquals(copy.getSuppressed()[0].getClass(), 
RuntimeException.class);
+    Assert.assertEquals(copy.getSuppressed()[0].getMessage(), 
"registered-suppressed");
+  }
+
+  @Test
+  public void testTryWithResourcesSuppressedRoundTrip() {
+    Fory fory = builder().withRefTracking(true).withCodegen(false).build();
+    RuntimeException value = buildTryWithResourcesException();
+
+    RuntimeException copy = serDe(fory, value);
+
+    Assert.assertEquals(copy.getMessage(), "main-failure");
+    Assert.assertEquals(copy.getSuppressed().length, 1);
+    Assert.assertEquals(copy.getSuppressed()[0].getClass(), 
IllegalStateException.class);
+    Assert.assertEquals(copy.getSuppressed()[0].getMessage(), "close-failure");
   }
 
   @Test
@@ -107,6 +132,7 @@ public class ExceptionSerializersTest extends ForyTestBase {
             .withParentCode(9)
             .withTags(new ArrayList<>(Arrays.asList("left", "right")));
     value.retryable = true;
+    value.addSuppressed(new IllegalStateException("suppressed-custom"));
 
     CustomException copy = serDe(fory, value);
 
@@ -117,6 +143,25 @@ public class ExceptionSerializersTest extends ForyTestBase 
{
     Assert.assertEquals(copy.parentCode, value.parentCode);
     Assert.assertEquals(copy.tags, value.tags);
     Assert.assertEquals(copy.retryable, value.retryable);
+    Assert.assertEquals(copy.getSuppressed().length, 1);
+    Assert.assertEquals(copy.getSuppressed()[0].getMessage(), 
"suppressed-custom");
+  }
+
+  private static RuntimeException buildTryWithResourcesException() {
+    try {
+      try (FailingCloseable ignored = new FailingCloseable()) {
+        throw new RuntimeException("main-failure");
+      }
+    } catch (RuntimeException e) {
+      return e;
+    }
+  }
+
+  private static final class FailingCloseable implements AutoCloseable {
+    @Override
+    public void close() {
+      throw new IllegalStateException("close-failure");
+    }
   }
 
   public static class ParentException extends RuntimeException {


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

Reply via email to