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]