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