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]