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 adcfd61d5 feat(java): direct static varhandle field accessors (#3778)
adcfd61d5 is described below

commit adcfd61d530b7914a26b16a62d6dab395ded2faf
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Jun 23 14:16:11 2026 +0530

    feat(java): direct static varhandle field accessors (#3778)
    
    ## Why?
    
    ## What does this PR do?
    
    non static VarHandle could not be compiled into direct field access,
    this pr generate static final varhandle for jdk25 instead.
    
    ## Related issues
    
    None.
    
    ## AI Contribution Checklist
    
    
    ## Does this PR introduce any user-facing change?
    
    No. This changes internal Java serializer accessor generation and
    runtime codegen behavior only.
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    ```
    Benchmark                              (bufferType)   (objectType)  
(references)   Mode  Cnt         Score         Error  Units
    UserTypeSerializeSuite.fory_serialize         array  MEDIA_CONTENT         
false  thrpt    5  15921306.512 ± 1695382.132  ops/s
    ```
---
 .agents/languages/java.md                          |  14 +-
 .github/workflows/ci.yml                           |  29 +++-
 .../java/org/apache/fory/benchmark/data/Image.java |  60 +++++++-
 .../java/org/apache/fory/benchmark/data/Media.java | 120 +++++++++++++--
 .../apache/fory/benchmark/data/MediaContent.java   |  42 ++++--
 .../fory/benchmark/state/FlatBuffersState.java     |  90 ++++++------
 .../fory/benchmark/state/ProtoBuffersState.java    |  86 +++++------
 .../apache/fory/benchmark/util/MsgpackUtil.java    |  98 ++++++-------
 integration_tests/jpms_tests/run_jlink_smoke.sh    |   6 +-
 .../integration_tests/JpmsFieldAccessorTest.java   |  17 ++-
 .../java/org/apache/fory/builder/CodecBuilder.java | 163 +++++++++++++--------
 .../fory/reflect/InstanceFieldAccessors.java       |  13 +-
 .../fory/builder/VarHandleCodegenSupport.java      | 120 +++++++++++++++
 .../fory/reflect/InstanceFieldAccessors.java       |  13 +-
 .../fory/builder/ObjectCodecBuilderTest.java       | 127 ++++++++++++++++
 15 files changed, 731 insertions(+), 267 deletions(-)

diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 55a3d0691..3f4b16512 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -176,8 +176,8 @@ Load this file when changing anything under `java/` or when 
Java drives a cross-
   trusted-lookup initialization or cold setup, not inside string hot paths.
 - `FieldAccessor` owns field-accessor dispatch. `RecordFieldAccessors` owns 
record field access,
   and `InstanceFieldAccessors` owns non-record instance field access. Do not 
reintroduce a
-  `FieldAccessorFactory` layer. `InstanceFieldAccessors` is public only so 
generated serializers
-  can name its concrete nested accessor type; treat it as internal owner code, 
not user API.
+  `FieldAccessorFactory` layer. Treat `InstanceFieldAccessors` as 
package-owned implementation
+  code, not user API and not generated-serializer API.
 - Android non-record reflection field access belongs inside the root 
`InstanceFieldAccessors`
   owner. Do not keep a standalone `ReflectionFieldAccessor`; Java25 never 
needs that path, and
   record reflection fallback remains record-owned in `RecordFieldAccessors`. 
Keep `sun.misc.Unsafe`
@@ -196,11 +196,11 @@ Load this file when changing anything under `java/` or 
when Java drives a cross-
   hot-path try/catch blocks and do not call `FieldAccessor.checkObj`; 
VarHandle validates null and
   receiver type itself. Root Unsafe offset access may keep a debug-only 
`assert` receiver check
   because Unsafe does not validate the target object; do not add production 
receiver checks.
-- JDK25+ generated serializers should store field accessors as concrete
-  `InstanceFieldAccessors.InstanceAccessor` static final fields, initialized 
once through
-  `FieldAccessor.createAccessor(...)` and a static-init cast. This keeps 
platform dispatch out of
-  generated read/write hot paths and avoids `FieldAccessor` virtual dispatch 
on final/private field
-  get/set calls.
+- JDK25+ generated serializers should store per-field `static final VarHandle` 
fields and call
+  `VarHandleCodegenSupport` typed static helpers directly. Do not use 
`InstanceAccessor` wrappers,
+  hidden generated accessors, `MethodHandle` bridges, boxed primitive 
VarHandle calls, or dynamic
+  handle containers in generated read/write hot paths. Final field writes must 
use this path
+  regardless of public, protected, package, or private visibility.
 - `DefineClass#defineHiddenNestmate` belongs in the root `DefineClass` owner. 
Do not add a Java25
   overlay only to call `Lookup#defineHiddenClass` directly, and do not move it 
to `java9` because
   `Lookup#defineClass` defines normal package classes, not hidden nestmates. 
Root code must avoid
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d618e65cc..44290afb0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1071,10 +1071,31 @@ jobs:
         with:
           bazelisk-cache: true
           bazelisk-version: "1.x"
+      - name: Compute Bazel C++ toolchain cache key
+        id: bazel-cpp-toolchain
+        shell: bash
+        run: |
+          {
+            echo "runner-os=${RUNNER_OS}"
+            echo "runner-arch=${RUNNER_ARCH}"
+            if [[ "${RUNNER_OS}" == "macOS" ]]; then
+              xcode-select -p
+              xcrun --sdk macosx --show-sdk-path
+              xcrun --find clang++
+              xcodebuild -version
+            else
+              command -v gcc || true
+              gcc --version || true
+              command -v g++ || true
+              g++ --version || true
+            fi
+          } > bazel-cpp-toolchain-key.txt
+          echo "key=$(shasum -a 256 bazel-cpp-toolchain-key.txt | cut -d' ' 
-f1)" >> "$GITHUB_OUTPUT"
       - name: Cache Bazel repository cache
         uses: actions/cache@v4
         with:
           # setup-bazel uses ~/.bazel on GitHub-hosted runners.
+          # local_config_cc records absolute host compiler and SDK include 
roots.
           path: |
             ~/.bazel/external
             ~/.cache/bazel/_bazel_*/*/external
@@ -1082,20 +1103,22 @@ jobs:
             ~/Library/Caches/bazel/external
             C:\users\runneradmin\.bazel\external
             C:\users\runneradmin\_bazel_runneradmin\*/external
-          key: bazel-repo-${{ runner.os }}-${{ runner.arch }}-py311-${{ 
hashFiles('WORKSPACE', '.bazelrc', 'bazel/**') }}
+          key: bazel-repo-${{ runner.os }}-${{ runner.arch }}-py311-${{ 
steps.bazel-cpp-toolchain.outputs.key }}-${{ hashFiles('MODULE.bazel', 
'MODULE.bazel.lock', 'WORKSPACE', '.bazelversion', '.bazelrc', 'bazel/**') }}
           restore-keys: |
-            bazel-repo-${{ runner.os }}-${{ runner.arch }}-py311-
+            bazel-repo-${{ runner.os }}-${{ runner.arch }}-py311-${{ 
steps.bazel-cpp-toolchain.outputs.key }}-
       - name: Cache Bazel build outputs
         uses: actions/cache@v4
         with:
           # setup-bazel uses ~/.bazel on GitHub-hosted runners.
+          # Keep host toolchain identity in the key so depfiles generated with
+          # one Xcode/SDK path are not restored under another macOS image.
           path: |
             ~/.bazel
             ~/.cache/bazel
             ~/Library/Caches/bazel
             C:\users\runneradmin\.bazel
             C:\users\runneradmin\_bazel_runneradmin
-          key: bazel-build-cpp-${{ runner.os }}-${{ runner.arch }}-${{ 
hashFiles('cpp/**', 'BUILD', 'WORKSPACE') }}
+          key: bazel-build-cpp-${{ runner.os }}-${{ runner.arch }}-${{ 
steps.bazel-cpp-toolchain.outputs.key }}-${{ hashFiles('cpp/**', 'BUILD', 
'WORKSPACE', 'MODULE.bazel', 'MODULE.bazel.lock', '.bazelversion', '.bazelrc', 
'bazel/**') }}
       - name: Run C++ CI with Bazel
         run: python ./ci/run_ci.py cpp
       - name: Upload Bazel Test Logs
diff --git 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Image.java 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Image.java
index 4b35f6900..e445449f7 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Image.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Image.java
@@ -22,12 +22,12 @@ package org.apache.fory.benchmark.data;
 import java.io.Serializable;
 
 public class Image implements Serializable {
-  public String uri;
-  public String title; // Can be null.
-  public int width;
-  public int height;
-  public Size size;
-  public Media media; // Can be null.
+  private String uri;
+  private String title; // Can be null.
+  private int width;
+  private int height;
+  private Size size;
+  private Media media; // Can be null.
 
   public Image() {}
 
@@ -40,6 +40,54 @@ public class Image implements Serializable {
     this.media = media;
   }
 
+  public String getUri() {
+    return uri;
+  }
+
+  public void setUri(String uri) {
+    this.uri = uri;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public int getWidth() {
+    return width;
+  }
+
+  public void setWidth(int width) {
+    this.width = width;
+  }
+
+  public int getHeight() {
+    return height;
+  }
+
+  public void setHeight(int height) {
+    this.height = height;
+  }
+
+  public Size getSize() {
+    return size;
+  }
+
+  public void setSize(Size size) {
+    this.size = size;
+  }
+
+  public Media getMedia() {
+    return media;
+  }
+
+  public void setMedia(Media media) {
+    this.media = media;
+  }
+
   public boolean equals(Object o) {
     if (this == o) {
       return true;
diff --git 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Media.java 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Media.java
index 9f119c939..8e167551a 100644
--- a/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Media.java
+++ b/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/Media.java
@@ -22,18 +22,18 @@ package org.apache.fory.benchmark.data;
 import java.util.List;
 
 public class Media implements java.io.Serializable {
-  public String uri;
-  public String title; // Can be null.
-  public int width;
-  public int height;
-  public String format;
-  public long duration;
-  public long size;
-  public int bitrate;
-  public boolean hasBitrate;
-  public List<String> persons;
-  public Player player;
-  public String copyright; // Can be null.
+  private String uri;
+  private String title; // Can be null.
+  private int width;
+  private int height;
+  private String format;
+  private long duration;
+  private long size;
+  private int bitrate;
+  private boolean hasBitrate;
+  private List<String> persons;
+  private Player player;
+  private String copyright; // Can be null.
 
   public Media() {}
 
@@ -64,6 +64,102 @@ public class Media implements java.io.Serializable {
     this.copyright = copyright;
   }
 
+  public String getUri() {
+    return uri;
+  }
+
+  public void setUri(String uri) {
+    this.uri = uri;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public int getWidth() {
+    return width;
+  }
+
+  public void setWidth(int width) {
+    this.width = width;
+  }
+
+  public int getHeight() {
+    return height;
+  }
+
+  public void setHeight(int height) {
+    this.height = height;
+  }
+
+  public String getFormat() {
+    return format;
+  }
+
+  public void setFormat(String format) {
+    this.format = format;
+  }
+
+  public long getDuration() {
+    return duration;
+  }
+
+  public void setDuration(long duration) {
+    this.duration = duration;
+  }
+
+  public long getSize() {
+    return size;
+  }
+
+  public void setSize(long size) {
+    this.size = size;
+  }
+
+  public int getBitrate() {
+    return bitrate;
+  }
+
+  public void setBitrate(int bitrate) {
+    this.bitrate = bitrate;
+  }
+
+  public boolean isHasBitrate() {
+    return hasBitrate;
+  }
+
+  public void setHasBitrate(boolean hasBitrate) {
+    this.hasBitrate = hasBitrate;
+  }
+
+  public List<String> getPersons() {
+    return persons;
+  }
+
+  public void setPersons(List<String> persons) {
+    this.persons = persons;
+  }
+
+  public Player getPlayer() {
+    return player;
+  }
+
+  public void setPlayer(Player player) {
+    this.player = player;
+  }
+
+  public String getCopyright() {
+    return copyright;
+  }
+
+  public void setCopyright(String copyright) {
+    this.copyright = copyright;
+  }
+
   public boolean equals(Object o) {
     if (this == o) {
       return true;
diff --git 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/MediaContent.java
 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/MediaContent.java
index d8839a04e..0d3a44eaf 100644
--- 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/MediaContent.java
+++ 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/data/MediaContent.java
@@ -23,8 +23,8 @@ import java.util.ArrayList;
 import java.util.List;
 
 public class MediaContent implements java.io.Serializable {
-  public Media media;
-  public List<Image> images;
+  private Media media;
+  private List<Image> images;
 
   public MediaContent() {}
 
@@ -33,6 +33,22 @@ public class MediaContent implements java.io.Serializable {
     this.images = images;
   }
 
+  public Media getMedia() {
+    return media;
+  }
+
+  public void setMedia(Media media) {
+    this.media = media;
+  }
+
+  public List<Image> getImages() {
+    return images;
+  }
+
+  public void setImages(List<Image> images) {
+    this.images = images;
+  }
+
   public boolean equals(Object o) {
     if (this == o) {
       return true;
@@ -67,17 +83,17 @@ public class MediaContent implements java.io.Serializable {
 
   public MediaContent populate(boolean circularReference) {
     media = new Media();
-    media.uri = "http://javaone.com/keynote.ogg";;
-    media.width = 641;
-    media.height = 481;
-    media.format = "video/theora\u1234";
-    media.duration = 18000001;
-    media.size = 58982401;
-    media.persons = new ArrayList();
-    media.persons.add("Bill Gates, Jr.");
-    media.persons.add("Steven Jobs");
-    media.player = Media.Player.FLASH;
-    media.copyright = "Copyright (c) 2009, Scooby Dooby Doo";
+    media.setUri("http://javaone.com/keynote.ogg";);
+    media.setWidth(641);
+    media.setHeight(481);
+    media.setFormat("video/theora\u1234");
+    media.setDuration(18000001);
+    media.setSize(58982401);
+    media.setPersons(new ArrayList<>());
+    media.getPersons().add("Bill Gates, Jr.");
+    media.getPersons().add("Steven Jobs");
+    media.setPlayer(Media.Player.FLASH);
+    media.setCopyright("Copyright (c) 2009, Scooby Dooby Doo");
     images = new ArrayList();
     Media media = circularReference ? this.media : null;
     images.add(
diff --git 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/FlatBuffersState.java
 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/FlatBuffersState.java
index 492e6521e..6d0e478ef 100644
--- 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/FlatBuffersState.java
+++ 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/FlatBuffersState.java
@@ -288,10 +288,10 @@ public class FlatBuffersState {
 
   public static FlatBufferBuilder serializeMediaContent(
       MediaContent mediaContent, FlatBufferBuilder builder) {
-    int mediaOffset = serializeMediaContent(mediaContent.media, builder);
-    int[] imageOffsets = new int[mediaContent.images.size()];
+    int mediaOffset = serializeMediaContent(mediaContent.getMedia(), builder);
+    int[] imageOffsets = new int[mediaContent.getImages().size()];
     for (int i = 0; i < imageOffsets.length; i++) {
-      imageOffsets[i] = serializeImage(mediaContent.images.get(i), builder);
+      imageOffsets[i] = serializeImage(mediaContent.getImages().get(i), 
builder);
     }
     int imagesOffset = FBSMediaContent.createImagesVector(builder, 
imageOffsets);
     FBSMediaContent.startFBSMediaContent(builder);
@@ -302,94 +302,94 @@ public class FlatBuffersState {
   }
 
   private static int serializeMediaContent(Media media, FlatBufferBuilder 
builder) {
-    int uriOffset = builder.createString(media.uri);
+    int uriOffset = builder.createString(media.getUri());
     int titleOffset = 0;
-    if (media.title != null) {
-      titleOffset = builder.createString(media.title);
+    if (media.getTitle() != null) {
+      titleOffset = builder.createString(media.getTitle());
     }
-    int formatOffset = builder.createString(media.format);
-    int[] personsOffsets = new int[media.persons.size()];
+    int formatOffset = builder.createString(media.getFormat());
+    int[] personsOffsets = new int[media.getPersons().size()];
     for (int i = 0; i < personsOffsets.length; i++) {
-      personsOffsets[i] = builder.createString(media.persons.get(i));
+      personsOffsets[i] = builder.createString(media.getPersons().get(i));
     }
     int personsOffset = FBSMedia.createPersonsVector(builder, personsOffsets);
-    int copyrightOffset = builder.createString(media.copyright);
+    int copyrightOffset = builder.createString(media.getCopyright());
     FBSMedia.startFBSMedia(builder);
     FBSMedia.addUri(builder, uriOffset);
-    if (media.title != null) {
+    if (media.getTitle() != null) {
       FBSMedia.addTitle(builder, titleOffset);
     }
     FBSMedia.addFormat(builder, formatOffset);
-    FBSMedia.addWidth(builder, media.width);
-    FBSMedia.addHeight(builder, media.height);
-    FBSMedia.addDuration(builder, media.duration);
-    FBSMedia.addSize(builder, media.size);
-    FBSMedia.addBitrate(builder, media.bitrate);
-    FBSMedia.addHasBitrate(builder, media.hasBitrate);
-    FBSMedia.addPlayer(builder, (byte) media.player.ordinal());
+    FBSMedia.addWidth(builder, media.getWidth());
+    FBSMedia.addHeight(builder, media.getHeight());
+    FBSMedia.addDuration(builder, media.getDuration());
+    FBSMedia.addSize(builder, media.getSize());
+    FBSMedia.addBitrate(builder, media.getBitrate());
+    FBSMedia.addHasBitrate(builder, media.isHasBitrate());
+    FBSMedia.addPlayer(builder, (byte) media.getPlayer().ordinal());
     FBSMedia.addCopyright(builder, copyrightOffset);
     FBSMedia.addPersons(builder, personsOffset);
     return FBSMedia.endFBSMedia(builder);
   }
 
   private static int serializeImage(Image image, FlatBufferBuilder builder) {
-    int uriOffset = builder.createString(image.uri);
+    int uriOffset = builder.createString(image.getUri());
     int titleOffset = 0;
-    if (image.title != null) {
-      titleOffset = builder.createString(image.title);
+    if (image.getTitle() != null) {
+      titleOffset = builder.createString(image.getTitle());
     }
-    Preconditions.checkArgument(image.media == null);
+    Preconditions.checkArgument(image.getMedia() == null);
     FBSImage.startFBSImage(builder);
     FBSImage.addUri(builder, uriOffset);
-    if (image.title != null) {
+    if (image.getTitle() != null) {
       FBSImage.addTitle(builder, titleOffset);
     }
-    FBSImage.addWidth(builder, image.width);
-    FBSImage.addHeight(builder, image.height);
-    FBSImage.addSize(builder, (byte) image.size.ordinal());
+    FBSImage.addWidth(builder, image.getWidth());
+    FBSImage.addHeight(builder, image.getHeight());
+    FBSImage.addSize(builder, (byte) image.getSize().ordinal());
     return FBSImage.endFBSImage(builder);
   }
 
   public static MediaContent deserializeMediaContent(ByteBuffer data) {
     MediaContent mediaContent = new MediaContent();
     FBSMediaContent fbsMediaContent = 
FBSMediaContent.getRootAsFBSMediaContent(data);
-    mediaContent.media = deserializeMedia(fbsMediaContent.media());
+    mediaContent.setMedia(deserializeMedia(fbsMediaContent.media()));
     List<Image> images = new ArrayList<>();
     for (int i = 0; i < fbsMediaContent.imagesLength(); i++) {
       images.add(deserializeImage(fbsMediaContent.images(i)));
     }
-    mediaContent.images = images;
+    mediaContent.setImages(images);
     return mediaContent;
   }
 
   private static Image deserializeImage(FBSImage fbsImage) {
     Image image = new Image();
-    image.uri = fbsImage.uri();
-    image.title = fbsImage.title();
-    image.width = fbsImage.width();
-    image.height = fbsImage.height();
-    image.size = Image.Size.values()[fbsImage.size()];
+    image.setUri(fbsImage.uri());
+    image.setTitle(fbsImage.title());
+    image.setWidth(fbsImage.width());
+    image.setHeight(fbsImage.height());
+    image.setSize(Image.Size.values()[fbsImage.size()]);
     return image;
   }
 
   private static Media deserializeMedia(FBSMedia fbsMedia) {
     Media media = new Media();
-    media.uri = fbsMedia.uri();
-    media.title = fbsMedia.title();
-    media.width = fbsMedia.width();
-    media.height = fbsMedia.height();
-    media.format = fbsMedia.format();
-    media.duration = fbsMedia.duration();
-    media.size = fbsMedia.size();
-    media.bitrate = fbsMedia.bitrate();
-    media.hasBitrate = fbsMedia.hasBitrate();
+    media.setUri(fbsMedia.uri());
+    media.setTitle(fbsMedia.title());
+    media.setWidth(fbsMedia.width());
+    media.setHeight(fbsMedia.height());
+    media.setFormat(fbsMedia.format());
+    media.setDuration(fbsMedia.duration());
+    media.setSize(fbsMedia.size());
+    media.setBitrate(fbsMedia.bitrate());
+    media.setHasBitrate(fbsMedia.hasBitrate());
     List<String> persons = new ArrayList<>();
     for (int i = 0; i < fbsMedia.personsLength(); i++) {
       persons.add(fbsMedia.persons(i));
     }
-    media.persons = persons;
-    media.player = Media.Player.values()[fbsMedia.player()];
-    media.copyright = fbsMedia.copyright();
+    media.setPersons(persons);
+    media.setPlayer(Media.Player.values()[fbsMedia.player()]);
+    media.setCopyright(fbsMedia.copyright());
     return media;
   }
 
diff --git 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/ProtoBuffersState.java
 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/ProtoBuffersState.java
index d581c2b30..6603b7bd6 100644
--- 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/ProtoBuffersState.java
+++ 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/state/ProtoBuffersState.java
@@ -287,44 +287,44 @@ public class ProtoBuffersState {
 
   public static byte[] serializeMediaContent(MediaContent mediaContent) {
     ProtoMessage.MediaContent.Builder builder = 
ProtoMessage.MediaContent.newBuilder();
-    builder.setMedia(serializeMedia(mediaContent.media));
-    mediaContent.images.forEach(image -> 
builder.addImages(serializeImage(image)));
+    builder.setMedia(serializeMedia(mediaContent.getMedia()));
+    mediaContent.getImages().forEach(image -> 
builder.addImages(serializeImage(image)));
     return builder.build().toByteArray();
   }
 
   private static ProtoMessage.Image serializeImage(Image image) {
     ProtoMessage.Image.Builder builder = ProtoMessage.Image.newBuilder();
-    builder.setUri(image.uri);
-    if (image.title != null) {
-      builder.setTitle(image.title);
+    builder.setUri(image.getUri());
+    if (image.getTitle() != null) {
+      builder.setTitle(image.getTitle());
     } else {
       builder.clearTitle();
     }
-    builder.setWidth(image.width);
-    builder.setHeight(image.height);
-    builder.setSize(ProtoMessage.Size.forNumber(image.size.ordinal()));
-    Preconditions.checkArgument(image.media == null);
+    builder.setWidth(image.getWidth());
+    builder.setHeight(image.getHeight());
+    builder.setSize(ProtoMessage.Size.forNumber(image.getSize().ordinal()));
+    Preconditions.checkArgument(image.getMedia() == null);
     return builder.build();
   }
 
   private static ProtoMessage.Media serializeMedia(Media media) {
     ProtoMessage.Media.Builder builder = ProtoMessage.Media.newBuilder();
-    builder.setUri(media.uri);
-    if (media.title != null) {
-      builder.setTitle(media.title);
+    builder.setUri(media.getUri());
+    if (media.getTitle() != null) {
+      builder.setTitle(media.getTitle());
     } else {
       builder.clearTitle();
     }
-    builder.setWidth(media.width);
-    builder.setHeight(media.height);
-    builder.setFormat(media.format);
-    builder.setDuration(media.duration);
-    builder.setSize(media.size);
-    builder.setBitrate(media.bitrate);
-    builder.setHasBitrate(media.hasBitrate);
-    builder.addAllPersons(media.persons);
-    builder.setPlayerValue(media.player.ordinal());
-    builder.setCopyright(media.copyright);
+    builder.setWidth(media.getWidth());
+    builder.setHeight(media.getHeight());
+    builder.setFormat(media.getFormat());
+    builder.setDuration(media.getDuration());
+    builder.setSize(media.getSize());
+    builder.setBitrate(media.getBitrate());
+    builder.setHasBitrate(media.isHasBitrate());
+    builder.addAllPersons(media.getPersons());
+    builder.setPlayerValue(media.getPlayer().ordinal());
+    builder.setCopyright(media.getCopyright());
     return builder.build();
   }
 
@@ -336,42 +336,42 @@ public class ProtoBuffersState {
     } catch (InvalidProtocolBufferException e) {
       throw new RuntimeException(e);
     }
-    mediaContent.media = deserializeMedia(builder.getMedia());
-    mediaContent.images =
+    mediaContent.setMedia(deserializeMedia(builder.getMedia()));
+    mediaContent.setImages(
         builder.getImagesList().stream()
             .map(ProtoBuffersState::deserializeImage)
-            .collect(Collectors.toList());
+            .collect(Collectors.toList()));
     return mediaContent;
   }
 
   private static Media deserializeMedia(ProtoMessage.Media mediaProto) {
     Media media = new Media();
-    media.uri = mediaProto.getUri();
+    media.setUri(mediaProto.getUri());
     if (mediaProto.hasTitle()) {
-      media.title = mediaProto.getTitle();
-    }
-    media.width = mediaProto.getWidth();
-    media.height = mediaProto.getHeight();
-    media.format = mediaProto.getFormat();
-    media.duration = mediaProto.getDuration();
-    media.size = mediaProto.getSize();
-    media.bitrate = mediaProto.getBitrate();
-    media.hasBitrate = mediaProto.getHasBitrate();
-    media.persons = mediaProto.getPersonsList();
-    media.player = Media.Player.values()[mediaProto.getPlayerValue()];
-    media.copyright = mediaProto.getCopyright();
+      media.setTitle(mediaProto.getTitle());
+    }
+    media.setWidth(mediaProto.getWidth());
+    media.setHeight(mediaProto.getHeight());
+    media.setFormat(mediaProto.getFormat());
+    media.setDuration(mediaProto.getDuration());
+    media.setSize(mediaProto.getSize());
+    media.setBitrate(mediaProto.getBitrate());
+    media.setHasBitrate(mediaProto.getHasBitrate());
+    media.setPersons(mediaProto.getPersonsList());
+    media.setPlayer(Media.Player.values()[mediaProto.getPlayerValue()]);
+    media.setCopyright(mediaProto.getCopyright());
     return media;
   }
 
   private static Image deserializeImage(ProtoMessage.Image imageProto) {
     Image image = new Image();
-    image.uri = imageProto.getUri();
+    image.setUri(imageProto.getUri());
     if (imageProto.hasTitle()) {
-      image.title = imageProto.getTitle();
+      image.setTitle(imageProto.getTitle());
     }
-    image.width = imageProto.getWidth();
-    image.height = imageProto.getHeight();
-    image.size = Image.Size.values()[imageProto.getSizeValue()];
+    image.setWidth(imageProto.getWidth());
+    image.setHeight(imageProto.getHeight());
+    image.setSize(Image.Size.values()[imageProto.getSizeValue()]);
     return image;
   }
 
diff --git 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/util/MsgpackUtil.java 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/util/MsgpackUtil.java
index 709a52e89..d9f97624d 100644
--- 
a/benchmarks/java/src/main/java/org/apache/fory/benchmark/util/MsgpackUtil.java
+++ 
b/benchmarks/java/src/main/java/org/apache/fory/benchmark/util/MsgpackUtil.java
@@ -63,16 +63,16 @@ public class MsgpackUtil {
     messagePacker.packMapHeader(2);
 
     messagePacker.packString("media");
-    if (mediaContent.media != null) {
-      packMedia(messagePacker, mediaContent.media);
+    if (mediaContent.getMedia() != null) {
+      packMedia(messagePacker, mediaContent.getMedia());
     } else {
       messagePacker.packNil();
     }
 
     messagePacker.packString("images");
-    if (mediaContent.images != null) {
-      messagePacker.packArrayHeader(mediaContent.images.size());
-      for (Image image : mediaContent.images) {
+    if (mediaContent.getImages() != null) {
+      messagePacker.packArrayHeader(mediaContent.getImages().size());
+      for (Image image : mediaContent.getImages()) {
         packImage(messagePacker, image);
       }
     } else {
@@ -91,9 +91,9 @@ public class MsgpackUtil {
       switch (key) {
         case "media":
           if (!messageUnpacker.tryUnpackNil()) {
-            mediaContent.media = unpackMedia(messageUnpacker);
+            mediaContent.setMedia(unpackMedia(messageUnpacker));
           } else {
-            mediaContent.media = null;
+            mediaContent.setMedia(null);
           }
           break;
         case "images":
@@ -103,9 +103,9 @@ public class MsgpackUtil {
             for (int j = 0; j < arraySize; j++) {
               images.add(unpackImage(messageUnpacker));
             }
-            mediaContent.images = images;
+            mediaContent.setImages(images);
           } else {
-            mediaContent.images = null;
+            mediaContent.setImages(null);
           }
           break;
         default:
@@ -121,24 +121,24 @@ public class MsgpackUtil {
     messagePacker.packMapHeader(9); // Media object's field count
 
     messagePacker.packString("uri");
-    messagePacker.packString(media.uri);
+    messagePacker.packString(media.getUri());
     messagePacker.packString("width");
-    messagePacker.packInt(media.width);
+    messagePacker.packInt(media.getWidth());
     messagePacker.packString("height");
-    messagePacker.packInt(media.height);
+    messagePacker.packInt(media.getHeight());
     messagePacker.packString("format");
-    messagePacker.packString(media.format);
+    messagePacker.packString(media.getFormat());
     messagePacker.packString("duration");
-    messagePacker.packLong(media.duration);
+    messagePacker.packLong(media.getDuration());
     messagePacker.packString("size");
-    messagePacker.packLong(media.size);
+    messagePacker.packLong(media.getSize());
     messagePacker.packString("player");
-    messagePacker.packString(media.player.name());
+    messagePacker.packString(media.getPlayer().name());
 
-    if (media.persons != null) {
+    if (media.getPersons() != null) {
       messagePacker.packString("persons");
-      messagePacker.packArrayHeader(media.persons.size());
-      for (String person : media.persons) {
+      messagePacker.packArrayHeader(media.getPersons().size());
+      for (String person : media.getPersons()) {
         messagePacker.packString(person);
       }
     } else {
@@ -146,9 +146,9 @@ public class MsgpackUtil {
       messagePacker.packNil();
     }
 
-    if (media.copyright != null) {
+    if (media.getCopyright() != null) {
       messagePacker.packString("copyright");
-      messagePacker.packString(media.copyright);
+      messagePacker.packString(media.getCopyright());
     } else {
       messagePacker.packString("copyright");
       messagePacker.packNil();
@@ -164,44 +164,44 @@ public class MsgpackUtil {
 
       switch (key) {
         case "uri":
-          media.uri = messageUnpacker.unpackString();
+          media.setUri(messageUnpacker.unpackString());
           break;
         case "width":
-          media.width = messageUnpacker.unpackInt();
+          media.setWidth(messageUnpacker.unpackInt());
           break;
         case "height":
-          media.height = messageUnpacker.unpackInt();
+          media.setHeight(messageUnpacker.unpackInt());
           break;
         case "format":
-          media.format = messageUnpacker.unpackString();
+          media.setFormat(messageUnpacker.unpackString());
           break;
         case "duration":
-          media.duration = messageUnpacker.unpackLong();
+          media.setDuration(messageUnpacker.unpackLong());
           break;
         case "size":
-          media.size = messageUnpacker.unpackInt();
+          media.setSize(messageUnpacker.unpackInt());
           break;
         case "player":
-          media.player = Media.Player.valueOf(messageUnpacker.unpackString());
+          
media.setPlayer(Media.Player.valueOf(messageUnpacker.unpackString()));
           break;
         case "persons":
           if (!messageUnpacker.tryUnpackNil()) {
             int arraySize = messageUnpacker.unpackArrayHeader();
-            media.persons = new ArrayList<>();
+            media.setPersons(new ArrayList<>());
             for (int j = 0; j < arraySize; j++) {
-              media.persons.add(messageUnpacker.unpackString());
+              media.getPersons().add(messageUnpacker.unpackString());
             }
           } else {
             messageUnpacker.unpackNil();
-            media.persons = null;
+            media.setPersons(null);
           }
           break;
         case "copyright":
           if (!messageUnpacker.tryUnpackNil()) {
-            media.copyright = messageUnpacker.unpackString();
+            media.setCopyright(messageUnpacker.unpackString());
           } else {
             messageUnpacker.unpackNil();
-            media.copyright = null;
+            media.setCopyright(null);
           }
           break;
         default:
@@ -217,22 +217,22 @@ public class MsgpackUtil {
     messagePacker.packMapHeader(6);
 
     messagePacker.packString("uri");
-    messagePacker.packString(image.uri);
+    messagePacker.packString(image.getUri());
     messagePacker.packString("title");
-    if (image.title == null) {
+    if (image.getTitle() == null) {
       messagePacker.packNil();
     } else {
-      messagePacker.packString(image.title);
+      messagePacker.packString(image.getTitle());
     }
     messagePacker.packString("width");
-    messagePacker.packInt(image.width);
+    messagePacker.packInt(image.getWidth());
     messagePacker.packString("height");
-    messagePacker.packInt(image.height);
+    messagePacker.packInt(image.getHeight());
     messagePacker.packString("size");
-    messagePacker.packString(image.size.name());
+    messagePacker.packString(image.getSize().name());
     messagePacker.packString("media");
-    if (image.media != null) {
-      packMedia(messagePacker, image.media);
+    if (image.getMedia() != null) {
+      packMedia(messagePacker, image.getMedia());
     } else {
       messagePacker.packNil();
     }
@@ -247,29 +247,29 @@ public class MsgpackUtil {
 
       switch (key) {
         case "uri":
-          image.uri = messageUnpacker.unpackString();
+          image.setUri(messageUnpacker.unpackString());
           break;
         case "title":
           if (!messageUnpacker.tryUnpackNil()) {
-            image.title = messageUnpacker.unpackString();
+            image.setTitle(messageUnpacker.unpackString());
           } else {
-            image.title = null;
+            image.setTitle(null);
           }
           break;
         case "width":
-          image.width = messageUnpacker.unpackInt();
+          image.setWidth(messageUnpacker.unpackInt());
           break;
         case "height":
-          image.height = messageUnpacker.unpackInt();
+          image.setHeight(messageUnpacker.unpackInt());
           break;
         case "size":
-          image.size = Image.Size.valueOf(messageUnpacker.unpackString());
+          image.setSize(Image.Size.valueOf(messageUnpacker.unpackString()));
           break;
         case "media":
           if (!messageUnpacker.tryUnpackNil()) {
-            image.media = unpackMedia(messageUnpacker);
+            image.setMedia(unpackMedia(messageUnpacker));
           } else {
-            image.media = null;
+            image.setMedia(null);
           }
           break;
         default:
diff --git a/integration_tests/jpms_tests/run_jlink_smoke.sh 
b/integration_tests/jpms_tests/run_jlink_smoke.sh
index 08364dc48..a3a2bfdbf 100755
--- a/integration_tests/jpms_tests/run_jlink_smoke.sh
+++ b/integration_tests/jpms_tests/run_jlink_smoke.sh
@@ -155,7 +155,11 @@ jlink \
   --add-modules jpms.smoke \
   --output "$WORK_DIR/image"
 
-"$WORK_DIR/image/bin/java" -m jpms.smoke/org.apache.fory.jpms.Smoke | grep -qx 
"ok"
+# JDK25+ zero-Unsafe mode uses Fory as a named module in this image.
+"$WORK_DIR/image/bin/java" \
+  --add-opens=java.base/java.lang.invoke=org.apache.fory.core \
+  -m jpms.smoke/org.apache.fory.jpms.Smoke \
+  | grep -qx "ok"
 
 IMAGE_MODULES="$("$WORK_DIR/image/bin/java" --list-modules)"
 echo "$IMAGE_MODULES" | grep -q "^org.apache.fory.core"
diff --git 
a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
 
b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
index 73c8ae72a..5abb72c25 100644
--- 
a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
+++ 
b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java
@@ -20,6 +20,7 @@
 package org.apache.fory.integration_tests;
 
 import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -37,8 +38,7 @@ import org.testng.annotations.Test;
 
 public class JpmsFieldAccessorTest {
   private static final int JDK_MAJOR_VERSION = Runtime.version().feature();
-  private static final String INSTANCE_ACCESSOR =
-      "org.apache.fory.reflect.InstanceFieldAccessors$InstanceAccessor";
+  private static final String VAR_HANDLE = "java.lang.invoke.VarHandle";
 
   @Test
   public void testPrivateFieldAccess() throws Exception {
@@ -133,7 +133,7 @@ public class JpmsFieldAccessorTest {
     Assert.assertTrue((Boolean) 
Class.class.getMethod("isHidden").invoke(serializerClass));
     Assert.assertSame(
         Class.class.getMethod("getNestHost").invoke(serializerClass), 
PrivateFieldBean.class);
-    assertAccessorField(serializerClass, "value");
+    assertVarHandleField(serializerClass, "value");
   }
 
   @Test
@@ -174,13 +174,16 @@ public class JpmsFieldAccessorTest {
     return serializer.getClass();
   }
 
-  private static void assertAccessorField(Class<?> serializerClass, String 
fieldName) {
+  private static void assertVarHandleField(Class<?> serializerClass, String 
fieldName) {
     for (Field field : serializerClass.getDeclaredFields()) {
-      if (field.getName().contains(fieldName + "_accessor_")) {
-        Assert.assertEquals(field.getType().getName(), INSTANCE_ACCESSOR);
+      if (field.getName().contains(fieldName + "_varHandle_")) {
+        Assert.assertEquals(field.getType().getName(), VAR_HANDLE);
+        int modifiers = field.getModifiers();
+        Assert.assertTrue(Modifier.isStatic(modifiers));
+        Assert.assertTrue(Modifier.isFinal(modifiers));
         return;
       }
     }
-    Assert.fail("Missing generated accessor field for " + fieldName + " in " + 
serializerClass);
+    Assert.fail("Missing generated VarHandle field for " + fieldName + " in " 
+ serializerClass);
   }
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java 
b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
index 68db4af0f..7bfd4898c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java
@@ -59,8 +59,6 @@ import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.NativeByteOrder;
 import org.apache.fory.platform.GraalvmSupport;
 import org.apache.fory.platform.JdkVersion;
-import org.apache.fory.reflect.FieldAccessor;
-import org.apache.fory.reflect.InstanceFieldAccessors.InstanceAccessor;
 import org.apache.fory.reflect.ObjectInstantiator;
 import org.apache.fory.reflect.ObjectInstantiators;
 import org.apache.fory.reflect.ReflectionUtils;
@@ -91,6 +89,13 @@ public abstract class CodecBuilder {
   static TypeRef<MemoryBuffer> bufferTypeRef = TypeRef.of(MemoryBuffer.class);
   static TypeRef<TypeInfo> classInfoTypeRef = TypeRef.of(TypeInfo.class);
   static TypeRef<TypeInfoHolder> classInfoHolderTypeRef = 
TypeRef.of(TypeInfoHolder.class);
+  private static final String VAR_HANDLE_TYPE_NAME = 
"java.lang.invoke.VarHandle";
+  private static final String VAR_HANDLE_SUPPORT =
+      "org.apache.fory.builder.VarHandleCodegenSupport";
+  private static final Class<?> VAR_HANDLE_CLASS =
+      JdkVersion.MAJOR_VERSION >= 9 ? loadClass(VAR_HANDLE_TYPE_NAME) : 
Object.class;
+  private static final Class<?> VAR_HANDLE_SUPPORT_CLASS =
+      JdkVersion.MAJOR_VERSION >= 25 ? loadClass(VAR_HANDLE_SUPPORT) : 
Object.class;
 
   protected final CodegenContext ctx;
   protected final TypeRef<?> beanType;
@@ -314,18 +319,7 @@ public abstract class CodecBuilder {
       Expression inputObject, Class<?> cls, Descriptor descriptor) {
     String fieldName = descriptor.getName();
     if (JdkVersion.MAJOR_VERSION >= 25) {
-      Reference fieldAccessor = getFieldAccessor(descriptor);
-      boolean fieldNullable = fieldNullable(descriptor);
-      if (descriptor.getTypeRef().isPrimitive()) {
-        Preconditions.checkArgument(!fieldNullable);
-        TypeRef<?> returnType = descriptor.getTypeRef();
-        String funcName = "get" + 
StringUtils.capitalize(descriptor.getRawType().toString());
-        return new Invoke(fieldAccessor, funcName, returnType, false, 
inputObject);
-      } else {
-        Invoke getObj =
-            new Invoke(fieldAccessor, "getObject", OBJECT_TYPE, fieldNullable, 
inputObject);
-        return tryCastIfPublic(getObj, descriptor.getTypeRef(), fieldName);
-      }
+      return varHandleGetField(inputObject, descriptor);
     }
     Expression fieldOffsetExpr = fieldOffsetExpr(cls, descriptor);
     boolean fieldNullable = fieldNullable(descriptor);
@@ -418,45 +412,6 @@ public abstract class CodecBuilder {
     }
   }
 
-  private Reference getFieldAccessor(Descriptor descriptor) {
-    Field field = descriptor.getField();
-    String fieldName = descriptor.getName();
-    String fieldAccessorName =
-        (duplicatedFields.contains(fieldName)
-                ? field.getDeclaringClass().getName().replaceAll("\\.|\\$", 
"_") + "_"
-                : "")
-            + fieldName
-            + "_accessor_";
-    if (JdkVersion.MAJOR_VERSION >= 25) {
-      // JDK25+ field writes go through the VarHandle-backed instance 
accessor. Keep the generated
-      // static field typed as the concrete final accessor so hot-path putX 
calls do not pay a
-      // FieldAccessor virtual dispatch. FieldAccessor.createAccessor still 
owns platform dispatch;
-      // this one-time cast happens only during generated-class initialization.
-      return getOrCreateField(
-          true,
-          InstanceAccessor.class,
-          fieldAccessorName,
-          () ->
-              new Cast(
-                  new StaticInvoke(
-                      FieldAccessor.class,
-                      "createAccessor",
-                      TypeRef.of(FieldAccessor.class),
-                      getReflectField(field.getDeclaringClass(), field, 
false)),
-                  TypeRef.of(InstanceAccessor.class)));
-    }
-    return getOrCreateField(
-        true,
-        FieldAccessor.class,
-        fieldAccessorName,
-        () ->
-            new StaticInvoke(
-                FieldAccessor.class,
-                "createAccessor",
-                TypeRef.of(FieldAccessor.class),
-                getReflectField(field.getDeclaringClass(), field, false)));
-  }
-
   /**
    * Returns an expression that deserialize data as a java bean of type {@link
    * CodecBuilder#beanClass}.
@@ -471,6 +426,11 @@ public abstract class CodecBuilder {
     if (value instanceof Inlineable) {
       ((Inlineable) value).inline();
     }
+    if (JdkVersion.MAJOR_VERSION >= 25 && d.isFinalField()) {
+      // Final-field restoration must not fall through public-field or setter 
branches. Only the
+      // target-class trusted VarHandle supports JDK25+ final writes across 
all field visibilities.
+      return varHandleSetField(bean, d, value);
+    }
     if (duplicatedFields.contains(fieldName) || 
!sourcePublicAccessible(beanClass)) {
       return unsafeSetField(bean, d, value);
     }
@@ -540,14 +500,7 @@ public abstract class CodecBuilder {
   private Expression unsafeSetField(Expression bean, Descriptor descriptor, 
Expression value) {
     TypeRef<?> fieldType = descriptor.getTypeRef();
     if (JdkVersion.MAJOR_VERSION >= 25) {
-      Reference fieldAccessor = getFieldAccessor(descriptor);
-      if (descriptor.getTypeRef().isPrimitive()) {
-        Preconditions.checkArgument(getRawType(value.type()) == 
getRawType(fieldType));
-        String funcName = "put" + 
StringUtils.capitalize(getRawType(fieldType).toString());
-        return new Invoke(fieldAccessor, funcName, bean, value);
-      } else {
-        return new Invoke(fieldAccessor, "putObject", bean, value);
-      }
+      return varHandleSetField(bean, descriptor, value);
     }
     // Use Field in case the class has duplicate field name as `fieldName`.
     Expression fieldOffsetExpr = fieldOffsetExpr(beanClass, descriptor);
@@ -560,6 +513,21 @@ public abstract class CodecBuilder {
     }
   }
 
+  private Expression varHandleSetField(Expression bean, Descriptor descriptor, 
Expression value) {
+    TypeRef<?> fieldType = descriptor.getTypeRef();
+    if (descriptor.getTypeRef().isPrimitive()) {
+      Preconditions.checkArgument(getRawType(value.type()) == 
getRawType(fieldType));
+    }
+    return new StaticInvoke(
+        varHandleSupportClass(),
+        varHandleSetMethod(fieldType),
+        PRIMITIVE_VOID_TYPE,
+        false,
+        getVarHandle(descriptor),
+        bean,
+        value);
+  }
+
   private Reference getReflectField(Class<?> cls, Field field) {
     return getReflectField(cls, field, true);
   }
@@ -605,6 +573,27 @@ public abstract class CodecBuilder {
         });
   }
 
+  private Reference getVarHandle(Descriptor descriptor) {
+    Field field = descriptor.getField();
+    String fieldName = descriptor.getName();
+    String fieldHandleName =
+        (duplicatedFields.contains(fieldName)
+                ? field.getDeclaringClass().getName().replaceAll("\\.|\\$", 
"_") + "_"
+                : "")
+            + fieldName
+            + "_varHandle_";
+    return getOrCreateField(
+        true,
+        varHandleClass(),
+        fieldHandleName,
+        () ->
+            new StaticInvoke(
+                varHandleSupportClass(),
+                "getVarHandle",
+                TypeRef.of(varHandleClass()),
+                getReflectField(field.getDeclaringClass(), field, false)));
+  }
+
   protected Reference getOrCreateField(
       boolean isStatic, Class<?> type, String fieldName, Supplier<Expression> 
value) {
     Reference fieldRef = fieldMap.get(fieldName);
@@ -617,6 +606,58 @@ public abstract class CodecBuilder {
     return fieldRef;
   }
 
+  private Expression varHandleGetField(Expression inputObject, Descriptor 
descriptor) {
+    TypeRef<?> returnType =
+        descriptor.getTypeRef().isPrimitive() ? descriptor.getTypeRef() : 
OBJECT_TYPE;
+    Expression getValue =
+        new StaticInvoke(
+            varHandleSupportClass(),
+            varHandleGetMethod(returnType),
+            descriptor.getName(),
+            returnType,
+            fieldNullable(descriptor),
+            getVarHandle(descriptor),
+            inputObject);
+    return descriptor.getTypeRef().isPrimitive()
+        ? getValue
+        : tryCastIfPublic(getValue, descriptor.getTypeRef(), 
descriptor.getName());
+  }
+
+  private static String varHandleGetMethod(TypeRef<?> type) {
+    if (type.isPrimitive()) {
+      return "get" + StringUtils.capitalize(getRawType(type).toString());
+    }
+    return "getObject";
+  }
+
+  private static String varHandleSetMethod(TypeRef<?> type) {
+    if (type.isPrimitive()) {
+      return "set" + StringUtils.capitalize(getRawType(type).toString());
+    }
+    return "setObject";
+  }
+
+  // Keep the Java 8 baseline CodecBuilder linkable: guarded class constants 
do not resolve
+  // higher-JDK class names on older runtimes.
+  private static Class<?> varHandleClass() {
+    Preconditions.checkState(JdkVersion.MAJOR_VERSION >= 9, "VarHandle 
requires JDK9+");
+    return VAR_HANDLE_CLASS;
+  }
+
+  private static Class<?> varHandleSupportClass() {
+    Preconditions.checkState(
+        JdkVersion.MAJOR_VERSION >= 25, "VarHandle codegen support requires 
JDK25+");
+    return VAR_HANDLE_SUPPORT_CLASS;
+  }
+
+  private static Class<?> loadClass(String className) {
+    try {
+      return Class.forName(className, false, 
CodecBuilder.class.getClassLoader());
+    } catch (ClassNotFoundException e) {
+      throw new IllegalStateException("Cannot load generated-code helper class 
" + className, e);
+    }
+  }
+
   /** Returns an Expression that create a new java object of type {@link 
CodecBuilder#beanClass}. */
   protected Expression newBean() {
     // TODO allow default access-level class.
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java
 
b/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java
index cc664e32b..363973449 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/reflect/InstanceFieldAccessors.java
@@ -36,15 +36,9 @@ import org.apache.fory.platform.internal._UnsafeUtils;
 import org.apache.fory.util.Preconditions;
 import sun.misc.Unsafe;
 
-/**
- * Non-record instance field accessor owner.
- *
- * <p>This class is public only so generated serializers can name {@link 
InstanceAccessor} as a
- * concrete field type on JDK25+. Callers must still create accessors through 
{@link
- * FieldAccessor#createAccessor(Field)} so platform dispatch stays centralized.
- */
+/** Non-record instance field accessor owner. */
 @Internal
-public final class InstanceFieldAccessors {
+final class InstanceFieldAccessors {
   private static final int BOOLEAN_ACCESS = 1;
   private static final int BYTE_ACCESS = 2;
   private static final int CHAR_ACCESS = 3;
@@ -119,8 +113,7 @@ public final class InstanceFieldAccessors {
     }
   }
 
-  /** Public only for generated serializers; use {@link 
FieldAccessor#createAccessor(Field)}. */
-  public static final class InstanceAccessor extends FieldAccessor {
+  static final class InstanceAccessor extends FieldAccessor {
     private static final Unsafe UNSAFE = _UnsafeUtils.UNSAFE;
 
     private final long fieldOffset;
diff --git 
a/java/fory-core/src/main/java25/org/apache/fory/builder/VarHandleCodegenSupport.java
 
b/java/fory-core/src/main/java25/org/apache/fory/builder/VarHandleCodegenSupport.java
new file mode 100644
index 000000000..f2e08cad4
--- /dev/null
+++ 
b/java/fory-core/src/main/java25/org/apache/fory/builder/VarHandleCodegenSupport.java
@@ -0,0 +1,120 @@
+/*
+ * 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.builder;
+
+import java.lang.invoke.VarHandle;
+import java.lang.reflect.Field;
+import org.apache.fory.annotation.Internal;
+import org.apache.fory.platform.internal._JDKAccess;
+
+/** JDK25 helper for source-generated serializers that use per-field static 
VarHandles. */
+@Internal
+public final class VarHandleCodegenSupport {
+  private VarHandleCodegenSupport() {}
+
+  public static VarHandle getVarHandle(Field field) {
+    try {
+      Class<?> declaringClass = field.getDeclaringClass();
+      // JDK25+ final-field writes require a target-class trusted lookup from 
_JDKAccess.
+      // A normal private lookup returns a read-only VarHandle for final 
instance fields.
+      return _JDKAccess._trustedLookup(declaringClass)
+          .findVarHandle(declaringClass, field.getName(), field.getType());
+    } catch (IllegalAccessException | NoSuchFieldException | RuntimeException 
e) {
+      throw new IllegalStateException(
+          "Cannot create VarHandle for field "
+              + field
+              + ". "
+              + _JDKAccess.jdk25AccessMessage(),
+          e);
+    }
+  }
+
+  public static boolean getBoolean(VarHandle handle, Object bean) {
+    return (boolean) handle.get(bean);
+  }
+
+  public static byte getByte(VarHandle handle, Object bean) {
+    return (byte) handle.get(bean);
+  }
+
+  public static char getChar(VarHandle handle, Object bean) {
+    return (char) handle.get(bean);
+  }
+
+  public static short getShort(VarHandle handle, Object bean) {
+    return (short) handle.get(bean);
+  }
+
+  public static int getInt(VarHandle handle, Object bean) {
+    return (int) handle.get(bean);
+  }
+
+  public static long getLong(VarHandle handle, Object bean) {
+    return (long) handle.get(bean);
+  }
+
+  public static float getFloat(VarHandle handle, Object bean) {
+    return (float) handle.get(bean);
+  }
+
+  public static double getDouble(VarHandle handle, Object bean) {
+    return (double) handle.get(bean);
+  }
+
+  public static Object getObject(VarHandle handle, Object bean) {
+    return handle.get(bean);
+  }
+
+  public static void setBoolean(VarHandle handle, Object bean, boolean value) {
+    handle.set(bean, value);
+  }
+
+  public static void setByte(VarHandle handle, Object bean, byte value) {
+    handle.set(bean, value);
+  }
+
+  public static void setChar(VarHandle handle, Object bean, char value) {
+    handle.set(bean, value);
+  }
+
+  public static void setShort(VarHandle handle, Object bean, short value) {
+    handle.set(bean, value);
+  }
+
+  public static void setInt(VarHandle handle, Object bean, int value) {
+    handle.set(bean, value);
+  }
+
+  public static void setLong(VarHandle handle, Object bean, long value) {
+    handle.set(bean, value);
+  }
+
+  public static void setFloat(VarHandle handle, Object bean, float value) {
+    handle.set(bean, value);
+  }
+
+  public static void setDouble(VarHandle handle, Object bean, double value) {
+    handle.set(bean, value);
+  }
+
+  public static void setObject(VarHandle handle, Object bean, Object value) {
+    handle.set(bean, value);
+  }
+}
diff --git 
a/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java
 
b/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java
index 022ed59f4..fe468bc9e 100644
--- 
a/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java
+++ 
b/java/fory-core/src/main/java25/org/apache/fory/reflect/InstanceFieldAccessors.java
@@ -26,15 +26,9 @@ import org.apache.fory.annotation.Internal;
 import org.apache.fory.platform.internal._JDKAccess;
 import org.apache.fory.util.Preconditions;
 
-/**
- * Non-record instance field accessor owner.
- *
- * <p>This class is public only so generated serializers can name {@link 
InstanceAccessor} as a
- * concrete field type on JDK25+. Callers must still create accessors through 
{@link
- * FieldAccessor#createAccessor(Field)} so platform dispatch stays centralized.
- */
+/** Non-record instance field accessor owner. */
 @Internal
-public final class InstanceFieldAccessors {
+final class InstanceFieldAccessors {
   private static final int BOOLEAN_ACCESS = 1;
   private static final int BYTE_ACCESS = 2;
   private static final int CHAR_ACCESS = 3;
@@ -92,8 +86,7 @@ public final class InstanceFieldAccessors {
         cause);
   }
 
-  /** Public only for generated serializers; use {@link 
FieldAccessor#createAccessor(Field)}. */
-  public static final class InstanceAccessor extends FieldAccessor {
+  static final class InstanceAccessor extends FieldAccessor {
     private final VarHandle handle;
     private final int accessKind;
 
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/builder/ObjectCodecBuilderTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/builder/ObjectCodecBuilderTest.java
index 713a91cf4..240409936 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/builder/ObjectCodecBuilderTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/builder/ObjectCodecBuilderTest.java
@@ -39,6 +39,7 @@ import org.apache.fory.codegen.CodeGenerator;
 import org.apache.fory.codegen.CompileUnit;
 import org.apache.fory.codegen.JaninoUtils;
 import org.apache.fory.codegen.javalangnameconflict.MethodSpiltObject;
+import org.apache.fory.platform.JdkVersion;
 import org.apache.fory.serializer.collection.CollectionSerializersTest;
 import org.apache.fory.test.bean.AccessBeans;
 import org.apache.fory.test.bean.BeanA;
@@ -228,6 +229,132 @@ public class ObjectCodecBuilderTest extends ForyTestBase {
     checkMethodSize(NestedContainer.class, fory);
   }
 
+  public static final class VarHandleFinalFields {
+    public final int publicFinal;
+    protected final long protectedFinal;
+    final String packageFinal;
+    private final String privateFinal;
+    private int privateValue;
+
+    public VarHandleFinalFields() {
+      this(0, 0, null, null, 0);
+    }
+
+    VarHandleFinalFields(
+        int publicFinal,
+        long protectedFinal,
+        String packageFinal,
+        String privateFinal,
+        int privateValue) {
+      this.publicFinal = publicFinal;
+      this.protectedFinal = protectedFinal;
+      this.packageFinal = packageFinal;
+      this.privateFinal = privateFinal;
+      this.privateValue = privateValue;
+    }
+  }
+
+  @Test
+  public void testJdk25VarHandleFieldAccess() {
+    Fory fory =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withCompatible(false)
+            .build();
+    String code = new ObjectCodecBuilder(VarHandleFinalFields.class, 
fory).genCode();
+    if (JdkVersion.MAJOR_VERSION >= 25) {
+      Assert.assertTrue(code.contains("java.lang.invoke.VarHandle"));
+      Assert.assertTrue(code.contains("publicFinal_varHandle_"));
+      Assert.assertTrue(code.contains("protectedFinal_varHandle_"));
+      Assert.assertTrue(code.contains("packageFinal_varHandle_"));
+      Assert.assertTrue(code.contains("privateFinal_varHandle_"));
+      Assert.assertTrue(code.contains("privateValue_varHandle_"));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.getVarHandle"));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.getObject"));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.setInt"));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.setLong"));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.setObject"));
+      
Assert.assertTrue(code.contains("VarHandleCodegenSupport.setInt(publicFinal_varHandle_"));
+      
Assert.assertTrue(code.contains("VarHandleCodegenSupport.setLong(protectedFinal_varHandle_"));
+      
Assert.assertTrue(code.contains("VarHandleCodegenSupport.setObject(packageFinal_varHandle_"));
+      
Assert.assertTrue(code.contains("VarHandleCodegenSupport.setObject(privateFinal_varHandle_"));
+      
Assert.assertTrue(code.contains("VarHandleCodegenSupport.setInt(privateValue_varHandle_"));
+      Assert.assertFalse(code.contains("FieldAccessor.createAccessor"));
+      Assert.assertFalse(code.contains("_varHandle_.get("));
+      Assert.assertFalse(code.contains("_varHandle_.set("));
+    }
+    VarHandleFinalFields bean = new VarHandleFinalFields(1, 2L, "package", 
"private", 3);
+    VarHandleFinalFields copy = (VarHandleFinalFields) 
fory.deserialize(fory.serialize(bean));
+    Assert.assertEquals(copy.publicFinal, 1);
+    Assert.assertEquals(copy.protectedFinal, 2L);
+    Assert.assertEquals(copy.packageFinal, "package");
+    Assert.assertEquals(copy.privateFinal, "private");
+    Assert.assertEquals(copy.privateValue, 3);
+  }
+
+  public static class VarHandleDuplicateParent {
+    private final int value;
+
+    public VarHandleDuplicateParent() {
+      this(0);
+    }
+
+    VarHandleDuplicateParent(int value) {
+      this.value = value;
+    }
+
+    public int parentValue() {
+      return value;
+    }
+  }
+
+  public static final class VarHandleDuplicateChild extends 
VarHandleDuplicateParent {
+    private final int value;
+
+    public VarHandleDuplicateChild() {
+      this(0, 0);
+    }
+
+    VarHandleDuplicateChild(int parentValue, int childValue) {
+      super(parentValue);
+      this.value = childValue;
+    }
+
+    public int childValue() {
+      return value;
+    }
+  }
+
+  @Test
+  public void testJdk25DuplicateVarHandles() {
+    Fory fory =
+        Fory.builder()
+            .withXlang(false)
+            .requireClassRegistration(false)
+            .withCompatible(false)
+            .build();
+    String code = new ObjectCodecBuilder(VarHandleDuplicateChild.class, 
fory).genCode();
+    if (JdkVersion.MAJOR_VERSION >= 25) {
+      String parentHandle = 
duplicateValueVarHandleName(VarHandleDuplicateParent.class);
+      String childHandle = 
duplicateValueVarHandleName(VarHandleDuplicateChild.class);
+      Assert.assertTrue(code.contains(parentHandle));
+      Assert.assertTrue(code.contains(childHandle));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.getInt(" + 
parentHandle));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.getInt(" + 
childHandle));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.setInt(" + 
parentHandle));
+      Assert.assertTrue(code.contains("VarHandleCodegenSupport.setInt(" + 
childHandle));
+    }
+    VarHandleDuplicateChild bean = new VarHandleDuplicateChild(1, 2);
+    VarHandleDuplicateChild copy = (VarHandleDuplicateChild) 
fory.deserialize(fory.serialize(bean));
+    Assert.assertEquals(copy.parentValue(), 1);
+    Assert.assertEquals(copy.childValue(), 2);
+  }
+
+  private static String duplicateValueVarHandleName(Class<?> declaringClass) {
+    return declaringClass.getName().replaceAll("\\.|\\$", "_") + 
"_value_varHandle_";
+  }
+
   @Test
   public void testAccessLevel() {
     Fory fory =


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

Reply via email to