You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/09/20 02:50:25 UTC
(fory) branch main updated: fix(java): guard LambdaGetterJsonAccessor and LambdaSetterJsonAccessor with GraalvmSupport.isGraalBuildTime() (#4060)
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 05109af54 fix(java): guard LambdaGetterJsonAccessor and LambdaSetterJsonAccessor with GraalvmSupport.isGraalBuildTime() (#4060)
05109af54 is described below
commit 05109af54677f7fcd1109542f61a0101cf20b20b
Author: Zheng Feng <zh...@gmail.com>
AuthorDate: Sun Sep 20 10:50:20 2026 +0800
fix(java): guard LambdaGetterJsonAccessor and LambdaSetterJsonAccessor with GraalvmSupport.isGraalBuildTime() (#4060)
## Description
Prevent LambdaMetafactory from creating runtime classes during GraalVM
Native Image compilation. Classes cannot be defined at runtime by
default in AOT compilation.
## Fix
Add check to both and to ensure lambda classes are only created during
GraalVM build time (via the Native Image Feature), not at runtime.
Fixes #4059
---------
Co-authored-by: chaokunyang <sh...@gmail.com>
---
.github/workflows/ci.yml | 6 +--
.../org/apache/fory/graalvm/ForyJsonExample.java | 49 ++++++++++++++++++++
.../apache/fory/json/meta/JsonFieldAccessor.java | 32 +++++++++++--
.../org/apache/fory/json/JsonPropertyTest.java | 54 ++++++++++++++++++++++
4 files changed, 134 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c656ce58d..6e7c2139a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -976,7 +976,7 @@ jobs:
restore-keys: |
${{ runner.os }}-maven-
- name: Cache Cargo dependencies
- uses: swatinem/rust-cache@v2
+ uses: Swatinem/rust-cache@v2
with:
workspaces: integration_tests/grpc_tests/rust
cache-bin: "false"
@@ -1273,7 +1273,7 @@ jobs:
restore-keys: |
${{ runner.os }}-maven-
- name: Cache Cargo dependencies
- uses: swatinem/rust-cache@v2
+ uses: Swatinem/rust-cache@v2
with:
workspaces: rust
cache-bin: "false"
@@ -1302,7 +1302,7 @@ jobs:
restore-keys: |
${{ runner.os }}-maven-
- name: Cache Cargo dependencies
- uses: swatinem/rust-cache@v2
+ uses: Swatinem/rust-cache@v2
with:
workspaces: rust
cache-bin: "false"
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
index 4e307a36f..1cbd7fad3 100644
--- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
@@ -73,6 +73,7 @@ import org.apache.fory.json.annotation.JsonValue;
import org.apache.fory.json.codec.JsonValueCodec;
import org.apache.fory.json.codec.MapKeyCodec;
import org.apache.fory.json.codec.ObjectCodec;
+import org.apache.fory.json.meta.JsonFieldAccessor;
import org.apache.fory.json.reader.Latin1JsonReader;
import org.apache.fory.json.reader.Utf16JsonReader;
import org.apache.fory.json.reader.Utf8JsonReader;
@@ -98,6 +99,7 @@ public final class ForyJsonExample {
testHostedCodegenConfigurations();
}
testModels();
+ testMethodAccessors();
testConfigurations();
testInclusion();
testCodecs();
@@ -227,6 +229,53 @@ public final class ForyJsonExample {
Preconditions.checkArgument(validated.validatorInvoked());
}
+ private static void testMethodAccessors() {
+ try {
+ // This bean deliberately has no JsonType annotation or hosted codec. Its accessors must be
+ // created after image startup, rather than reused from the Feature's build-time cache.
+ AccessorBean bean = new AccessorBean();
+ JsonFieldAccessor intGetter =
+ JsonFieldAccessor.forGetter(AccessorBean.class.getMethod("getValue"));
+ JsonFieldAccessor intSetter =
+ JsonFieldAccessor.forSetter(AccessorBean.class.getMethod("setValue", int.class));
+ JsonFieldAccessor objectGetter =
+ JsonFieldAccessor.forGetter(AccessorBean.class.getMethod("getName"));
+ JsonFieldAccessor objectSetter =
+ JsonFieldAccessor.forSetter(AccessorBean.class.getMethod("setName", String.class));
+ intSetter.putInt(bean, 123456);
+ objectSetter.putObject(bean, "runtime");
+ Preconditions.checkArgument(intGetter.getInt(bean) == 123456);
+ Preconditions.checkArgument(objectGetter.getObject(bean).equals("runtime"));
+ intSetter.putObject(bean, 654321);
+ objectSetter.putObject(bean, null);
+ Preconditions.checkArgument(intGetter.getObject(bean).equals(654321));
+ Preconditions.checkArgument(objectGetter.getObject(bean) == null);
+ } catch (NoSuchMethodException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public static final class AccessorBean {
+ private int value;
+ private String name;
+
+ public int getValue() {
+ return value;
+ }
+
+ public void setValue(int value) {
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
private static void testPrimitiveProperties(ForyJson json) {
PrimitiveProperties value = new PrimitiveProperties();
value.setBooleanValue(true);
diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java
index d2c94ca82..a93a1f01e 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java
@@ -58,13 +58,17 @@ import org.apache.fory.util.function.ToShortFunction;
*
* <p>Field members use typed access so primitive paths do not box. Ordinary JVM fields delegate to
* Fory core's {@link FieldAccessor}; JDK 25 Native Image fields use typed reflection when module
- * access permits it. Method members cache a trusted {@code MethodHandle} on the JVM, use reflection
- * on Android, and use lambdas prepared by the Fory JSON Native Image Feature. Generated codecs
+ * access permits it. Method members use typed lambdas on the JVM, reflection on Android, and
+ * lambdas prepared by the Fory JSON Native Image Feature on JDK 25+. Native runtime cache misses
+ * use {@code MethodHandle}s because new lambda classes cannot be defined there. Generated codecs
* consume the original field or method metadata and emit direct expressions.
*/
public abstract class JsonFieldAccessor {
private static final boolean USE_JDK25_NATIVE_ACCESS =
GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && JdkVersion.MAJOR_VERSION >= 25;
+ private static final boolean USE_METHOD_LAMBDAS =
+ !AndroidSupport.IS_ANDROID
+ && (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE || USE_JDK25_NATIVE_ACCESS);
// The Feature calls the ordinary factories during analysis, so Native Image retains the same
// accessor instances later returned while runtime configurations build interpreted codecs.
private static final ClassValueCache<ConcurrentMap<Member, JsonFieldAccessor>> NATIVE_ACCESSORS =
@@ -201,18 +205,38 @@ public abstract class JsonFieldAccessor {
private static JsonFieldAccessor newGetterAccessor(Member member) {
Method getter = (Method) member;
- return USE_JDK25_NATIVE_ACCESS
+ // Scala Unit getters return void. MethodHandle invocation adapts their result to null, whereas
+ // LambdaMetafactory cannot adapt a void method to a value-returning function interface.
+ return getter.getReturnType() != void.class && canUseMethodLambda(getter)
? LambdaGetterJsonAccessor.create(getter)
: new GetterJsonAccessor(getter);
}
private static JsonFieldAccessor newSetterAccessor(Member member) {
Method setter = (Method) member;
- return USE_JDK25_NATIVE_ACCESS && setter.getParameterCount() == 1
+ return setter.getParameterCount() == 1 && canUseMethodLambda(setter)
? LambdaSetterJsonAccessor.create(setter)
: new SetterJsonAccessor(setter);
}
+ private static boolean canUseMethodLambda(Method method) {
+ if (!USE_METHOD_LAMBDAS || GraalvmSupport.isGraalRuntime()) {
+ return false;
+ }
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+ return true;
+ }
+ // Lambda classes are defined in the declaring loader. Bootstrap and isolated model loaders
+ // cannot implement Fory's primitive function interfaces, so retain MethodHandle access there.
+ ClassLoader loader = method.getDeclaringClass().getClassLoader();
+ try {
+ return Class.forName(JsonFieldAccessor.class.getName(), false, loader)
+ == JsonFieldAccessor.class;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+
private static final class FieldJsonAccessor extends JsonFieldAccessor {
private final FieldAccessor accessor;
diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java
index ab70faffe..4d80d3c27 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java
@@ -22,6 +22,7 @@ package org.apache.fory.json;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
+import java.text.DecimalFormat;
import org.apache.fory.json.annotation.JsonProperty;
import org.apache.fory.json.data.BeanProperties.BooleanBean;
import org.apache.fory.json.data.BeanProperties.ConflictingTypesBean;
@@ -36,6 +37,7 @@ import org.apache.fory.json.data.BeanProperties.MixedBean;
import org.apache.fory.json.data.BeanProperties.OverloadedSetterBean;
import org.apache.fory.json.data.BeanProperties.SetterBean;
import org.apache.fory.json.data.BeanProperties.SetterOnlyBean;
+import org.apache.fory.json.meta.JsonFieldAccessor;
import org.testng.annotations.Factory;
import org.testng.annotations.Test;
@@ -45,6 +47,58 @@ public class JsonPropertyTest extends ForyJsonTestModels {
super(codegen);
}
+ @Test
+ public void typedMethodAccessors() throws Exception {
+ JsonFieldAccessor getter = JsonFieldAccessor.forGetter(GetterBean.class.getMethod("getId"));
+ JsonFieldAccessor setter =
+ JsonFieldAccessor.forSetter(SetterBean.class.getMethod("setId", int.class));
+ // The typed operations must be implemented directly, without the base Object boxing path.
+ assertEquals(
+ getter.getClass().getMethod("getInt", Object.class).getDeclaringClass(), getter.getClass());
+ assertEquals(
+ setter.getClass().getMethod("putInt", Object.class, int.class).getDeclaringClass(),
+ setter.getClass());
+ assertEquals(getter.getInt(new GetterBean()), 17);
+ assertEquals(getter.getObject(new GetterBean()), 17);
+ SetterBean value = new SetterBean();
+ setter.putInt(value, 123456);
+ assertEquals(SetterBean.id(value), 123457);
+ setter.putObject(value, 654321);
+ assertEquals(SetterBean.id(value), 654322);
+ assertEquals(SetterBean.setterCalls(value), 2);
+ }
+
+ @Test
+ public void bootstrapMethodAccessors() throws Exception {
+ JsonFieldAccessor getter = JsonFieldAccessor.forGetter(Character.class.getMethod("charValue"));
+ assertEquals(getter.getChar('x'), 'x');
+ JsonFieldAccessor setter =
+ JsonFieldAccessor.forSetter(
+ DecimalFormat.class.getMethod("setGroupingUsed", boolean.class));
+ DecimalFormat format = new DecimalFormat();
+ setter.putBoolean(format, false);
+ assertEquals(format.isGroupingUsed(), false);
+ setter.putBoolean(format, true);
+ assertEquals(format.isGroupingUsed(), true);
+ }
+
+ @Test
+ public void voidGetter() throws Exception {
+ VoidProperty value = new VoidProperty();
+ JsonFieldAccessor getter =
+ JsonFieldAccessor.forGetter(VoidProperty.class.getMethod("getValue"));
+ assertEquals(getter.getObject(value), null);
+ assertEquals(value.calls, 1);
+ }
+
+ public static final class VoidProperty {
+ int calls;
+
+ public void getValue() {
+ calls++;
+ }
+ }
+
@Test
public void writePrivateGetters() {
ForyJson json = newJson();
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org