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 3fc76a4fe feat(java): optimize java serialization perf (#3794)
3fc76a4fe is described below
commit 3fc76a4fe7f8ef68db080a608367e415cb7ce4c5
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Jun 29 12:13:53 2026 +0530
feat(java): optimize java serialization perf (#3794)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.agents/ci-and-pr.md | 6 +-
.agents/languages/java.md | 15 ++-
.github/workflows/build-containerized-pr.yml | 3 +
.github/workflows/build-containerized-release.yml | 3 +
.github/workflows/build-native-pr.yml | 3 +
.github/workflows/build-native-release.yml | 3 +
.github/workflows/ci.yml | 5 +-
.github/workflows/clean-pr-body.yml | 3 +
.github/workflows/lint.yml | 3 +
.github/workflows/pr-lint.yml | 3 +
.github/workflows/release-compiler.yaml | 3 +
.github/workflows/release-csharp.yaml | 3 +
.github/workflows/release-dart.yaml | 3 +
.github/workflows/release-java-snapshot.yaml | 3 +
.github/workflows/release-javascript.yaml | 3 +
.github/workflows/release-python.yaml | 3 +
.github/workflows/release-rust.yaml | 3 +
.github/workflows/sync.yml | 3 +
docs/compiler/generated-code.md | 3 +-
.../integration_tests/JpmsFieldAccessorTest.java | 5 +-
.../src/main/java/org/apache/fory/Fory.java | 4 +-
.../fory/builder/BaseObjectCodecBuilder.java | 135 ++++++++++++++++++---
.../java/org/apache/fory/builder/CodecBuilder.java | 3 +
.../java/org/apache/fory/builder/CodecUtils.java | 47 ++-----
.../apache/fory/builder/ObjectCodecBuilder.java | 5 +-
.../org/apache/fory/codegen/CodeGenerator.java | 80 +-----------
.../org/apache/fory/codegen/CodegenContext.java | 19 +--
.../java/org/apache/fory/codegen/CompileUnit.java | 18 ---
.../apache/fory/codegen/ExpressionOptimizer.java | 12 +-
.../java/org/apache/fory/context/ReadContext.java | 18 +++
.../java/org/apache/fory/context/WriteContext.java | 28 +++++
.../java/org/apache/fory/memory/MemoryBuffer.java | 4 +-
.../org/apache/fory/resolver/ClassResolver.java | 52 ++++----
.../org/apache/fory/resolver/TypeResolver.java | 124 ++++++++++++++-----
.../org/apache/fory/serializer/EnumSerializer.java | 16 ++-
.../apache/fory/serializer/StringSerializer.java | 17 +--
.../apache/fory/serializer/UnionSerializer.java | 10 +-
.../fory/serializer/UnknownClassSerializers.java | 16 ++-
.../org/apache/fory/memory/MemoryBuffer.java | 4 +-
.../org/apache/fory/builder/CodecUtilsTest.java | 15 ---
.../fory/serializer/StringSerializerTest.java | 35 ++++++
java/fory-json/pom.xml | 54 +++++++++
.../org/apache/fory/json/codegen/JsonCodegen.java | 23 ++--
.../org/apache/fory/json/ForyJsonTestModels.java | 3 +-
44 files changed, 524 insertions(+), 297 deletions(-)
diff --git a/.agents/ci-and-pr.md b/.agents/ci-and-pr.md
index 6f7171f09..6bf0672ac 100644
--- a/.agents/ci-and-pr.md
+++ b/.agents/ci-and-pr.md
@@ -132,7 +132,11 @@ implementation, CI-fix, or verification task, or when the
user explicitly asks f
## Workflow Changes
-- In ASF GitHub Actions, verify new `uses:` entries against approved Apache
infrastructure action patterns; replace unapproved actions with approved
actions plus shell or Python logic when needed.
+- In ASF GitHub Actions, verify new or updated `uses:` entries against the
approved Apache
+ infrastructure action reference at
+ `https://github.com/apache/infrastructure-actions/blob/main/actions.yml`;
use an allowlisted pin
+ from that file or replace unapproved actions with approved actions plus
shell or Python logic when
+ needed.
- Do not add workflow dependencies on new repository variables or secrets when
GitHub Actions context, constants, or checked-in configuration can provide
stable non-secret values. Coordinate required repo config before landing if no
safe default exists.
- If a setup action fails because of a runtime deprecation, verify the current
official major and upgrade to the compatible major rather than adding temporary
runner flags.
- Reusable release automation belongs under `ci/` unless the user explicitly
requests a language-local script.
diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 3f4b16512..41b19b206 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -205,15 +205,14 @@ Load this file when changing anything under `java/` or
when Java drives a cross-
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
direct `Lookup.ClassOption` linkage and cache the method-handle/option-array
setup off the hot
- path.
-- Hidden generated serializers are Java25+ only. Do not broaden serializer
hidden-class definition
- to Java15-24, because those runtimes still use the unsafe-backed
field/object path. Keep
- `AccessorHelper` as the source-generated same-package helper; do not turn it
into a bytecode
+ path. Keep this method available for future Java25+ designs even when
runtime codegen does not
+ call it.
+- Runtime generated serializers use the normal `CodeGenerator` classloader
path, not hidden
+ nestmate class definition. Janino-generated source still cannot directly
access target bean
+ private fields through hidden nestmate definition, so do not reintroduce
hidden serializer loading
+ or same-package source-access plumbing without a separate Java25 design and
measured proof.
+ `AccessorHelper` remains the source-generated same-package helper; do not
turn it into a bytecode
hidden-field owner unless a separate Java25-only design explicitly requires
that.
-- JDK25 hidden generated serializers must not emit private split helper
methods. Janino lowers
- private instance helpers to static bridge methods whose receiver parameter
uses the original
- binary class name, which fails hidden-class verification. Use non-private
final split helpers on
- that path.
- Runtime codegen must not emit Janino source that names bootstrap JDK
implementation classes in
concealed or non-source-public packages. Generated source in the unnamed
module cannot access
those classes even when Fory's trusted field-access path can read/write
their fields; use
diff --git a/.github/workflows/build-containerized-pr.yml
b/.github/workflows/build-containerized-pr.yml
index 55d516324..6434f2cf5 100644
--- a/.github/workflows/build-containerized-pr.yml
+++ b/.github/workflows/build-containerized-pr.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Build Containerized PR Wheels
on:
pull_request:
diff --git a/.github/workflows/build-containerized-release.yml
b/.github/workflows/build-containerized-release.yml
index 6e6899144..e8b1bc7c7 100644
--- a/.github/workflows/build-containerized-release.yml
+++ b/.github/workflows/build-containerized-release.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Build Containerized Release Wheels
on:
push:
diff --git a/.github/workflows/build-native-pr.yml
b/.github/workflows/build-native-pr.yml
index 6c7227a75..e45cd26d4 100644
--- a/.github/workflows/build-native-pr.yml
+++ b/.github/workflows/build-native-pr.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Build Native PR Wheels
on:
pull_request:
diff --git a/.github/workflows/build-native-release.yml
b/.github/workflows/build-native-release.yml
index fac625cd2..9098ade6a 100644
--- a/.github/workflows/build-native-release.yml
+++ b/.github/workflows/build-native-release.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Build Native Release Wheels
on:
push:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5c770366e..0207528a0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Fory CI
concurrency:
@@ -535,7 +538,7 @@ jobs:
java-version: ["17", "21", "25"]
steps:
- uses: actions/checkout@v5
- - uses: graalvm/setup-graalvm@f744c72a42b1995d7b0cbc314bde4bace7ac1fe1 #
1.5.0
+ - uses: graalvm/setup-graalvm@6f3fa030c4b8f77c1f554a860f593a654538fa38 #
1.5.6
with:
java-version: ${{ matrix.java-version }}
distribution: "graalvm"
diff --git a/.github/workflows/clean-pr-body.yml
b/.github/workflows/clean-pr-body.yml
index a90312f73..5961c9323 100644
--- a/.github/workflows/clean-pr-body.yml
+++ b/.github/workflows/clean-pr-body.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: "Clean PR Body"
on:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 67a573f5d..b64a3ad4f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: ❄️ Lint
on:
diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml
index bbd4c57fa..c330f6ac0 100644
--- a/.github/workflows/pr-lint.yml
+++ b/.github/workflows/pr-lint.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: "Lint PR"
on:
diff --git a/.github/workflows/release-compiler.yaml
b/.github/workflows/release-compiler.yaml
index d155c180a..096e4c1c1 100644
--- a/.github/workflows/release-compiler.yaml
+++ b/.github/workflows/release-compiler.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish Compiler
run-name: "Compiler Release: ${{ github.ref_name }}"
diff --git a/.github/workflows/release-csharp.yaml
b/.github/workflows/release-csharp.yaml
index 1f58e544c..8f458f46e 100644
--- a/.github/workflows/release-csharp.yaml
+++ b/.github/workflows/release-csharp.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish C#
run-name: "C# Release: ${{ github.ref_name }}"
diff --git a/.github/workflows/release-dart.yaml
b/.github/workflows/release-dart.yaml
index 8525237af..1191d8544 100644
--- a/.github/workflows/release-dart.yaml
+++ b/.github/workflows/release-dart.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish Dart
run-name: "Dart Release: ${{ github.ref_name }}"
diff --git a/.github/workflows/release-java-snapshot.yaml
b/.github/workflows/release-java-snapshot.yaml
index af869e9eb..d82dcc6a0 100644
--- a/.github/workflows/release-java-snapshot.yaml
+++ b/.github/workflows/release-java-snapshot.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish Fory Java Snapshot
on:
diff --git a/.github/workflows/release-javascript.yaml
b/.github/workflows/release-javascript.yaml
index a2883ebcf..f0c43db2e 100644
--- a/.github/workflows/release-javascript.yaml
+++ b/.github/workflows/release-javascript.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish JavaScript
run-name: "JavaScript Release: ${{ github.ref_name }}"
diff --git a/.github/workflows/release-python.yaml
b/.github/workflows/release-python.yaml
index f09ac43d2..5c480eeac 100644
--- a/.github/workflows/release-python.yaml
+++ b/.github/workflows/release-python.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish Python
run-name: "Python Release: ${{ contains(github.event.workflow_run.name,
'Containerized') && 'Containerized' || 'Native' }} (Run ID: ${{
github.event.workflow_run.id }})"
diff --git a/.github/workflows/release-rust.yaml
b/.github/workflows/release-rust.yaml
index 9f1153bb4..9ce88548f 100644
--- a/.github/workflows/release-rust.yaml
+++ b/.github/workflows/release-rust.yaml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Publish Rust
run-name: "Rust Release: ${{ github.ref_name }}"
diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml
index a920ef65d..543b0ed13 100644
--- a/.github/workflows/sync.yml
+++ b/.github/workflows/sync.yml
@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
+# All `uses:` action pins in this workflow must come from the Apache action
allowlist:
+# https://github.com/apache/infrastructure-actions/blob/main/actions.yml
+
name: Sync Files
on:
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index b3e5ebc91..68fddf234 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -1062,8 +1062,7 @@ export enum AnimalCase {
}
export type Animal =
- | { case: AnimalCase.DOG; value: Dog }
- | { case: AnimalCase.CAT; value: Cat };
+ { case: AnimalCase.DOG; value: Dog } | { case: AnimalCase.CAT; value: Cat };
```
### Schema Helpers
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 5abb72c25..3f81ea9c7 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
@@ -130,9 +130,8 @@ public class JpmsFieldAccessorTest {
Assert.assertEquals(result.value(), 17);
Class<?> serializerClass = serializerClass(fory, PrivateFieldBean.class);
- Assert.assertTrue((Boolean)
Class.class.getMethod("isHidden").invoke(serializerClass));
- Assert.assertSame(
- Class.class.getMethod("getNestHost").invoke(serializerClass),
PrivateFieldBean.class);
+ // Runtime codegen uses the normal CodeGenerator classloader path. JDK25+
final-field access is
+ // guaranteed by the generated VarHandle field, not by hidden-nestmate
source access.
assertVarHandleField(serializerClass, "value");
}
diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java
b/java/fory-core/src/main/java/org/apache/fory/Fory.java
index 5ad083d69..a7916400a 100644
--- a/java/fory-core/src/main/java/org/apache/fory/Fory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java
@@ -352,7 +352,7 @@ public final class Fory implements BaseFory {
if (writeContext.getDepth() > 0) {
throwDepthSerializationException();
}
- writeContext.writeRef(obj);
+ writeContext.writeRootRef(obj);
return buffer;
} catch (Throwable t) {
throw processSerializationError(t);
@@ -512,7 +512,7 @@ public final class Fory implements BaseFory {
if (readContext.getDepth() > 0) {
throwDepthDeserializationException();
}
- return readContext.readRef();
+ return readContext.readRootRef();
} finally {
jitContext.unlock();
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
index c431f7ec7..a5cbb9c6e 100644
---
a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
+++
b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java
@@ -138,6 +138,7 @@ import
org.apache.fory.serializer.PrimitiveSerializers.LongSerializer;
import org.apache.fory.serializer.ReplaceResolveSerializer;
import org.apache.fory.serializer.Serializer;
import org.apache.fory.serializer.StringSerializer;
+import org.apache.fory.serializer.UnknownClassSerializers;
import org.apache.fory.serializer.collection.CollectionFlags;
import org.apache.fory.serializer.collection.CollectionLikeSerializer;
import org.apache.fory.serializer.collection.CollectionSerializer;
@@ -220,10 +221,6 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
descriptorDispatchId = new HashMap<>();
}
- void setSamePackageAccess(boolean samePackageAccess) {
- ctx.setSamePackageAccess(samePackageAccess);
- }
-
// Must be static to be shared across the whole process life.
private static final Map<String, Map<String, Integer>> idGenerator = new
ConcurrentHashMap<>();
@@ -701,6 +698,18 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
Expression inputObject, Expression buffer, Descriptor descriptor,
Expression serializer) {
TypeRef<?> typeRef = descriptor.getTypeRef();
Class<?> clz = getRawType(typeRef);
+ if (isEnumType(clz)) {
+ Expression enumSerializer = serializer == null ?
getSerializerForField(clz) : serializer;
+ if (hasEnumValueMethods(enumSerializer)) {
+ Class<?> serializerClass = getRawType(enumSerializer.type());
+ Expression value =
+ EnumSerializer.class.isAssignableFrom(serializerClass)
+ ? cast(inputObject, TypeRef.of(Enum.class))
+ : inputObject;
+ return new Invoke(enumSerializer, "writeValue", writeContextRef(),
buffer, value);
+ }
+ return new Invoke(enumSerializer, writeMethodName, writeContextRef(),
inputObject);
+ }
if (serializer != null) {
return new Invoke(serializer, writeMethodName, writeContextRef(),
inputObject);
}
@@ -717,6 +726,17 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
return getOrCreateSerializer(cls, true);
}
+ private static boolean isEnumType(Class<?> cls) {
+ return cls != Enum.class && Enum.class.isAssignableFrom(cls);
+ }
+
+ private static boolean hasEnumValueMethods(Expression serializer) {
+ Class<?> serializerClass = getRawType(serializer.type());
+ // UnknownEnumSerializer is enum-shaped but does not extend EnumSerializer.
+ return EnumSerializer.class.isAssignableFrom(serializerClass)
+ ||
UnknownClassSerializers.UnknownEnumSerializer.class.isAssignableFrom(serializerClass);
+ }
+
protected Expression serializeForNullable(
Expression inputObject, Expression buffer, TypeRef<?> typeRef, boolean
nullable) {
return serializeForNullable(inputObject, buffer, typeRef, null, false,
nullable);
@@ -885,6 +905,7 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
Class<?> clz = getRawType(typeRef);
Expression clsExpr = new Invoke(inputObject, "getClass", "cls",
CLASS_TYPE);
ListExpression writeClassAndObject = new ListExpression();
+ Expression exactClassWrite = exactClassWrite(inputObject, buffer, clz);
Tuple2<Reference, Boolean> classInfoRef = addTypeInfoField(clz);
Expression classInfo = classInfoRef.f0;
if (classInfoRef.f1) {
@@ -904,12 +925,25 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
PRIMITIVE_VOID_TYPE,
writeContextRef(),
inputObject));
+ Expression write =
+ exactClassWrite == null
+ ? writeClassAndObject
+ : new If(eq(clsExpr, getClassExpr(clz)), exactClassWrite,
writeClassAndObject, false);
return invokeGenerated(
- ctx,
- writeCutPoints(buffer, inputObject),
- writeClassAndObject,
- "writeClassAndObject",
- false);
+ ctx, writeCutPoints(buffer, inputObject), write,
"writeClassAndObject", false);
+ }
+
+ private Expression exactClassWrite(Expression inputObject, Expression
buffer, Class<?> clz) {
+ if (clz.isInterface() || Modifier.isAbstract(clz.getModifiers())) {
+ return null;
+ }
+ Reference typeInfo = addExactTypeInfoField(clz);
+ Expression serializer = getOrCreateSerializer(clz);
+ return new ListExpression(
+ typeResolver(
+ r -> r.writeExactClassExpr(typeResolverRef, writeContextRef(),
buffer, typeInfo, clz)),
+ new Invoke(
+ serializer, writeMethodName, PRIMITIVE_VOID_TYPE,
writeContextRef(), inputObject));
}
protected Expression writeTypeInfo(
@@ -1128,6 +1162,21 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
return classInfoRef;
}
+ protected Reference addExactTypeInfoField(Class<?> cls) {
+ String key = "exactClassInfo:" + cls;
+ Reference reference = (Reference) sharedFieldMap.get(key);
+ if (reference != null) {
+ return reference;
+ }
+ Expression classInfoExpr =
+ inlineInvoke(typeResolverRef, "getTypeInfo", classInfoTypeRef,
getClassExpr(cls));
+ String name = ctx.newName(ctx.newName(cls) + "ExactTypeInfo");
+ ctx.addField(true, ctx.type(TypeInfo.class), name, classInfoExpr);
+ reference = fieldRef(name, classInfoTypeRef);
+ sharedFieldMap.put(key, reference);
+ return reference;
+ }
+
protected Reference addTypeInfoHolderField(Class<?> cls) {
// Final type need to write classinfo when meta share enabled.
String key;
@@ -1334,11 +1383,18 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
builder.add(
writeContainerElements(elementType, true, null, null, buffer,
collection, size));
} else {
+ Expression declSameNoNull =
+ eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL),
"declSameNoNull");
Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL);
Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag,
"hasNull");
builder.add(
+ declSameNoNull,
hasNull,
- writeContainerElements(elementType, false, null, hasNull, buffer,
collection, size));
+ new If(
+ declSameNoNull,
+ writeContainerElements(elementType, false, null, null, buffer,
collection, size),
+ writeContainerElements(elementType, false, null, hasNull,
buffer, collection, size),
+ false));
}
} else {
Literal flag = ofInt(CollectionFlags.IS_SAME_TYPE);
@@ -1398,9 +1454,28 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
differentTypeWrite);
} else {
// if declared elem type don't track ref, all elements must not write
ref.
+ Expression declSameNoNull =
+ eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL),
"declSameNoNull");
Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL);
Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag,
"hasNull");
- builder.add(hasNull);
+ builder.add(declSameNoNull, hasNull);
+ Expression declaredNoNullWrite = null;
+ if (maybeDecl) {
+ declaredNoNullWrite =
+ invokeGenerated(
+ ctx,
+ writeCutPoints(buffer, collection, size),
+ writeContainerElements(
+ elementType,
+ false,
+ cast(getOrCreateSerializer(elemClass), serializerType),
+ null,
+ buffer,
+ collection,
+ size),
+ "declSameNoNullWrite",
+ false);
+ }
ListExpression writeBuilder = new ListExpression(elemSerializer);
writeBuilder.add(
writeContainerElements(
@@ -1415,6 +1490,9 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
invokeGenerated(ctx, cutPoint, writeBuilder,
"sameElementClassWrite", false),
writeContainerElements(
elementType, false, null, hasNull, buffer, collection,
size));
+ if (declaredNoNullWrite != null) {
+ action = new If(declSameNoNull, declaredNoNullWrite, action, false);
+ }
}
builder.add(action);
}
@@ -2344,6 +2422,16 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
return StringSerializer.readStringExpr(
getOrCreateStringSerializer(), buffer, config.compressString());
}
+ if (isEnumType(cls)) {
+ Expression enumSerializer = serializer == null ?
getSerializerForField(cls) : serializer;
+ if (!hasEnumValueMethods(enumSerializer)) {
+ return cast(
+ new Invoke(enumSerializer, readMethodName, OBJECT_TYPE,
readContextRef()),
+ TypeRef.of(Enum.class));
+ }
+ return new Invoke(
+ enumSerializer, "readValue", TypeRef.of(Enum.class),
readContextRef(), buffer);
+ }
Expression obj;
if (usesPrimitiveListArrayProtocol(descriptor)) {
serializer = getPrimitiveListArraySerializer(cls);
@@ -2595,7 +2683,7 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
classInfo = readTypeInfo(getRawType(typeRef), buffer);
}
} else {
- classInfo = readTypeInfo(getRawType(typeRef), buffer);
+ classInfo = readTypeInfo(rawType, buffer);
}
serializer = inlineInvoke(classInfo, "getSerializer", SERIALIZER_TYPE);
}
@@ -2638,16 +2726,20 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
Expression collection =
new Invoke(serializer, "newCollection", COLLECTION_TYPE,
readContextRef);
Expression size = new Invoke(serializer, "getAndClearNumElements", "size",
PRIMITIVE_INT_TYPE);
- // if add branch by `ArrayList`, generated code will be > 325 bytes.
- // and List#add is more likely be inlined if there is only one subclass.
+ // Do not add an ArrayList-specific branch here: it pushes generated code
over 325 bytes, and
+ // List#add is more likely to inline when the call site has only one
receiver subclass.
Expression hookRead = readCollectionCodegen(buffer, collection, size,
elementType);
hookRead = new Invoke(serializer, "onCollectionRead", OBJECT_TYPE,
hookRead);
- Expression action =
- new If(
- supportHook,
- new ListExpression(collection, hookRead),
- read(serializer, buffer, OBJECT_TYPE),
+ Expression fallbackAction = read(serializer, buffer, OBJECT_TYPE);
+ Expression fallbackRead =
+ invokeGenerated(
+ ctx,
+ readCutPoints(buffer, serializer),
+ new ListExpression(fallbackAction, new Return(fallbackAction)),
+ "readCollectionFallback",
false);
+ Expression action =
+ new If(supportHook, new ListExpression(collection, hookRead),
fallbackRead, false);
if (invokeHint != null && invokeHint.genNewMethod) {
invokeHint.add(buffer);
invokeHint.add(readContextRef());
@@ -2664,7 +2756,10 @@ public abstract class BaseObjectCodecBuilder extends
CodecBuilder {
protected Expression readCollectionCodegen(
Expression buffer, Expression collection, Expression size, TypeRef<?>
elementType) {
ListExpression builder = new ListExpression();
- Invoke flags = new Invoke(buffer, "readByte", "flags", PRIMITIVE_INT_TYPE,
false);
+ // The flags byte is the next encoded byte. Use the checked read path so
truncated
+ // buffer-backed or stream-backed input fails before advancing the reader
index.
+ Expression flags =
+ cast(new Invoke(buffer, "readByte", PRIMITIVE_BYTE_TYPE),
PRIMITIVE_INT_TYPE);
builder.add(flags);
Class<?> elemClass = TypeUtils.getRawType(elementType);
walkPath.add(elementType.toString());
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 7bfd4898c..8e5083b6c 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
@@ -518,6 +518,8 @@ public abstract class CodecBuilder {
if (descriptor.getTypeRef().isPrimitive()) {
Preconditions.checkArgument(getRawType(value.type()) ==
getRawType(fieldType));
}
+ // Janino cannot compile VarHandle's signature-polymorphic access modes
directly. Keep generated
+ // serializers on typed helper methods even though the VarHandle itself is
a static final field.
return new StaticInvoke(
varHandleSupportClass(),
varHandleSetMethod(fieldType),
@@ -609,6 +611,7 @@ public abstract class CodecBuilder {
private Expression varHandleGetField(Expression inputObject, Descriptor
descriptor) {
TypeRef<?> returnType =
descriptor.getTypeRef().isPrimitive() ? descriptor.getTypeRef() :
OBJECT_TYPE;
+ // See varHandleSetField: direct VarHandle access-mode calls are not valid
Janino output.
Expression getValue =
new StaticInvoke(
varHandleSupportClass(),
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
index 2361179dd..c9092f672 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
@@ -19,6 +19,7 @@
package org.apache.fory.builder;
+import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.fory.Fory;
@@ -26,10 +27,7 @@ import org.apache.fory.codegen.CodeGenerator;
import org.apache.fory.codegen.CompileUnit;
import org.apache.fory.collection.Tuple3;
import org.apache.fory.meta.TypeDef;
-import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.JdkVersion;
-import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.resolver.TypeResolver;
import org.apache.fory.serializer.Serializer;
@@ -120,6 +118,12 @@ public class CodecUtils {
@SuppressWarnings("unchecked")
static <T> Class<? extends Serializer> loadOrGenCodecClass(
Class<T> beanClass, Fory fory, BaseObjectCodecBuilder codecBuilder) {
+ // use genCodeFunc to avoid gen code repeatedly
+ CompileUnit compileUnit =
+ new CompileUnit(
+ CodeGenerator.getPackage(beanClass),
+ codecBuilder.codecClassName(beanClass),
+ codecBuilder::genCode);
CodeGenerator codeGenerator;
ClassLoader beanClassClassLoader =
beanClass.getClassLoader() == null
@@ -130,40 +134,15 @@ public class CodecUtils {
}
TypeResolver typeResolver = fory.getTypeResolver();
codeGenerator = getCodeGenerator(beanClassClassLoader, typeResolver);
- Class<?> neighborClass = codecNeighbor(beanClass, beanClassClassLoader);
- codecBuilder.setSamePackageAccess(neighborClass != null);
- // use genCodeFunc to avoid gen code repeatedly
- CompileUnit compileUnit =
- new CompileUnit(
- CodeGenerator.getPackage(beanClass),
- codecBuilder.codecClassName(beanClass),
- codecBuilder::genCode,
- neighborClass);
- return (Class<? extends Serializer>)
- codeGenerator.compileAndLoad(compileUnit, compileState ->
compileState.lock.lock());
- }
-
- private static Class<?> codecNeighbor(Class<?> beanClass, ClassLoader
beanClassClassLoader) {
- // Hidden generated serializers are only a JDK25+ path. JDK8-24 keeps the
existing unsafe-backed
- // field/object access strategy, so broadening this to Java15+ adds
complexity without removing
- // unsafe from those runtimes.
- if (AndroidSupport.IS_ANDROID
- || JdkVersion.MAJOR_VERSION < 25
- || beanClass.getClassLoader() == null) {
- return null;
- }
- if
(!CodeGenerator.getPackage(beanClass).equals(ReflectionUtils.getPackage(beanClass)))
{
- return null;
- }
+ ClassLoader classLoader =
+ codeGenerator.compile(
+ Collections.singletonList(compileUnit), compileState ->
compileState.lock.lock());
+ String className = codecBuilder.codecQualifiedClassName(beanClass);
try {
- // A generated serializer defined in the bean loader must resolve Fory
runtime classes there.
- if (beanClassClassLoader.loadClass(Fory.class.getName()) == Fory.class) {
- return beanClass;
- }
+ return (Class<? extends Serializer>) classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
- // The composed-loader path remains the owner when the bean loader
cannot see Fory directly.
+ throw new IllegalStateException("Impossible because we just compiled
class", e);
}
- return null;
}
private static CodeGenerator getCodeGenerator(
diff --git
a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
index e22cd2b38..b467361ac 100644
---
a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
+++
b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java
@@ -799,9 +799,10 @@ public class ObjectCodecBuilder extends
BaseObjectCodecBuilder {
Expression bean;
if (!isRecord) {
bean = newBean();
- Expression referenceObject = invokeReadContext("reference", bean);
expressions.add(bean);
- expressions.add(referenceObject);
+ if (typeResolver.trackingRef()) {
+ expressions.add(invokeReadContext("reference", bean));
+ }
} else {
if (recordCtrAccessible) {
bean = new FieldsCollector();
diff --git
a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
index f63e4151c..826029a4c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java
@@ -40,7 +40,6 @@ import org.apache.fory.logging.Logger;
import org.apache.fory.logging.LoggerFactory;
import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.platform.GraalvmSupport;
-import org.apache.fory.platform.internal.DefineClass;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.util.ClassLoaderUtils;
import org.apache.fory.util.ClassLoaderUtils.ByteArrayClassLoader;
@@ -136,46 +135,6 @@ public class CodeGenerator {
}
parentClassLoader = classLoader;
}
- Map<String, byte[]> classes = compileToBytecode(compileUnits,
parentClassLoader, callback);
- return defineClasses(classes);
- }
-
- public Class<?> compileAndLoad(CompileUnit unit, CompileCallback callback) {
- Class<?> neighborClass = unit.getNeighborClass();
- if (neighborClass == null) {
- ClassLoader loader = compile(java.util.Collections.singletonList(unit),
callback);
- try {
- return loader.loadClass(unit.getQualifiedClassName());
- } catch (ClassNotFoundException e) {
- throw new IllegalStateException("Impossible because we just compiled
class", e);
- }
- }
- checkRuntimeCodegenSupported();
- DefineState defineState = getDefineState(unit.getQualifiedClassName());
- if (defineState.definedClass != null) {
- return defineState.definedClass;
- }
- synchronized (defineState.lock) {
- if (defineState.definedClass != null) {
- return defineState.definedClass;
- }
- ClassLoader parentClassLoader = getClassLoader();
- Map<String, byte[]> classes =
- compileToBytecode(java.util.Collections.singletonList(unit),
parentClassLoader, callback);
- byte[] bytecodes = classes.get(classFilepath(unit));
- if (bytecodes == null) {
- throw new IllegalStateException(
- "Compiler did not produce bytecode for " +
unit.getQualifiedClassName());
- }
- Class<?> definedClass = DefineClass.defineHiddenNestmate(neighborClass,
bytecodes);
- defineState.definedClass = definedClass;
- defineState.defined = true;
- return definedClass;
- }
- }
-
- private Map<String, byte[]> compileToBytecode(
- List<CompileUnit> compileUnits, ClassLoader parentClassLoader,
CompileCallback callback) {
CompileState compileState = getCompileState(compileUnits);
callback.lock(compileState);
Map<String, byte[]> classes;
@@ -192,7 +151,7 @@ public class CodeGenerator {
compileState.lock.unlock();
}
}
- return classes;
+ return defineClasses(classes);
}
/**
@@ -342,7 +301,6 @@ public class CodeGenerator {
private static class DefineState {
final Object lock;
volatile boolean defined;
- volatile Class<?> definedClass;
private DefineState() {
this.lock = new Object();
@@ -449,7 +407,7 @@ public class CodeGenerator {
}
public static String fullClassName(CompileUnit unit) {
- return unit.getQualifiedClassName();
+ return unit.pkg + "." + unit.mainClassName;
}
public static String fullClassNameFromClassFilePath(String classFilePath) {
@@ -564,40 +522,10 @@ public class CodeGenerator {
if (clz.isPrimitive()) {
return true;
}
- if (!sourcePkgLevelAccessible(clz)) {
+ if (!ReflectionUtils.isPublic(clz)) {
return false;
}
- Class<?> current = clz;
- while (current != null) {
- if (!ReflectionUtils.isPublic(current)) {
- return false;
- }
- current = current.getEnclosingClass();
- }
- return true;
- }
-
- public static boolean sourceAccessibleFrom(Class<?> clz, String pkg) {
- if (sourcePublicAccessible(clz)) {
- return true;
- }
- if (clz.isArray()) {
- return sourceAccessibleFrom(clz.getComponentType(), pkg);
- }
- if (!sourcePkgLevelAccessible(clz)) {
- return false;
- }
- if (!ReflectionUtils.getPackage(clz).equals(pkg)) {
- return false;
- }
- Class<?> current = clz;
- while (current != null) {
- if (ReflectionUtils.isPrivate(current)) {
- return false;
- }
- current = current.getEnclosingClass();
- }
- return true;
+ return sourcePkgLevelAccessible(clz);
}
private static final Map<Class<?>, Boolean> sourcePkgLevelAccessible =
diff --git
a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
index 779d7d9cf..ccfed3237 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java
@@ -140,7 +140,6 @@ public class CodegenContext {
String pkg;
LinkedHashSet<String> imports = new LinkedHashSet<>();
String className;
- private boolean samePackageAccess;
String classModifiers = "public final";
String[] superClasses;
String[] interfaces;
@@ -356,17 +355,6 @@ public class CodegenContext {
return pkg;
}
- public void setSamePackageAccess(boolean samePackageAccess) {
- if (this.samePackageAccess != samePackageAccess) {
- sourcePublicAccessibleCache.clear();
- }
- this.samePackageAccess = samePackageAccess;
- }
-
- public boolean hasSamePackageAccess() {
- return samePackageAccess;
- }
-
public Set<String> getValNames() {
return valNames;
}
@@ -741,12 +729,7 @@ public class CodegenContext {
/** Returns true if class is public accessible from source. */
public boolean sourcePublicAccessible(Class<?> clz) {
- return sourcePublicAccessibleCache.computeIfAbsent(
- clz,
- c ->
- samePackageAccess
- ? CodeGenerator.sourceAccessibleFrom(c, pkg)
- : CodeGenerator.sourcePublicAccessible(c));
+ return sourcePublicAccessibleCache.computeIfAbsent(clz,
CodeGenerator::sourcePublicAccessible);
}
/** Returns true if class is package level accessible from source. */
diff --git
a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
index d90b6fdae..f442c71c5 100644
--- a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
+++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java
@@ -31,33 +31,19 @@ public class CompileUnit {
String pkg;
String mainClassName;
- // Non-null only when the compiled class must be defined as a JDK25+ hidden
nestmate of this
- // neighbor. Ordinary generated classes still use the CodeGenerator
classloader path.
- private final Class<?> neighborClass;
private String code;
private Supplier<String> genCodeFunc;
public CompileUnit(String pkg, String mainClassName, String code) {
- this(pkg, mainClassName, code, null);
- }
-
- public CompileUnit(String pkg, String mainClassName, String code, Class<?>
neighborClass) {
this.pkg = pkg;
this.mainClassName = mainClassName;
this.code = code;
- this.neighborClass = neighborClass;
}
public CompileUnit(String pkg, String mainClassName, Supplier<String>
genCodeFunc) {
- this(pkg, mainClassName, genCodeFunc, null);
- }
-
- public CompileUnit(
- String pkg, String mainClassName, Supplier<String> genCodeFunc, Class<?>
neighborClass) {
this.pkg = pkg;
this.mainClassName = mainClassName;
this.genCodeFunc = genCodeFunc;
- this.neighborClass = neighborClass;
}
public String getCode() {
@@ -79,10 +65,6 @@ public class CompileUnit {
}
}
- public Class<?> getNeighborClass() {
- return neighborClass;
- }
-
@Override
public String toString() {
return "CompileUnit{" + "pkg='" + pkg + '\'' + ", mainClassName='" +
mainClassName + '\'' + '}';
diff --git
a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
index 68eae904b..a9c3568e6 100644
---
a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java
@@ -68,12 +68,13 @@ public class ExpressionOptimizer {
Expression groupExpressions,
String methodPrefix,
boolean inlineInvoke) {
- // Janino lowers private instance helpers to static bridge methods with
the original binary
- // class as receiver. Hidden classes have a different runtime identity, so
JDK25 hidden
- // generated serializers must keep split helpers non-private.
- String modifier = ctx.hasSamePackageAccess() ? "final" : "private";
return invokeGenerated(
- ctx, new LinkedHashSet<>(cutPoint), groupExpressions, modifier,
methodPrefix, inlineInvoke);
+ ctx,
+ new LinkedHashSet<>(cutPoint),
+ groupExpressions,
+ "private",
+ methodPrefix,
+ inlineInvoke);
}
/**
@@ -132,7 +133,6 @@ public class ExpressionOptimizer {
// instance field name.
CodegenContext codegenContext =
new CodegenContext(ctx.getPackage(), ctx.getValNames(),
ctx.getImports());
- codegenContext.setSamePackageAccess(ctx.hasSamePackageAccess());
for (Reference reference : cutExprMap.values()) {
Preconditions.checkArgument(codegenContext.containName(reference.name()));
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
index 6dca2e503..b1000161b 100644
--- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java
@@ -55,9 +55,11 @@ public final class ReadContext {
private final Generics generics;
private final TypeResolver typeResolver;
private final RefReader refReader;
+ private final TypeInfoHolder rootTypeInfoHolder;
private final MetaStringReader metaStringReader;
private final StringSerializer stringSerializer;
private final boolean crossLanguage;
+ private final boolean trackingRef;
private final boolean compressInt;
private final Int64Encoding longEncoding;
private final int maxDepth;
@@ -86,9 +88,11 @@ public final class ReadContext {
this.generics = generics;
this.typeResolver = typeResolver;
this.refReader = refReader;
+ rootTypeInfoHolder = typeResolver.nilTypeInfoHolder();
this.metaStringReader = metaStringReader;
stringSerializer = (StringSerializer)
typeResolver.getSerializer(String.class);
crossLanguage = config.isXlang();
+ trackingRef = config.trackingRef();
compressInt = config.compressInt();
longEncoding = config.longEncoding();
maxDepth = config.maxDepth();
@@ -578,6 +582,20 @@ public final class ReadContext {
return (T) readNonRef(serializer);
}
+ /** Reads the root object for one deserialization operation. */
+ public Object readRootRef() {
+ if (trackingRef) {
+ return readRef(rootTypeInfoHolder);
+ }
+ MemoryBuffer buffer = this.buffer;
+ int headFlag = buffer.readByte();
+ if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) {
+ TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder);
+ return readNonRef(typeInfo);
+ }
+ return null;
+ }
+
/** Reads a non-null, first-seen object together with its type metadata. */
public Object readNonRef() {
TypeInfo typeInfo = typeResolver.readTypeInfo(this);
diff --git
a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
index ebab5330b..bf74c0a67 100644
--- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
+++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java
@@ -56,9 +56,11 @@ public final class WriteContext {
private final Generics generics;
private final TypeResolver typeResolver;
private final RefWriter refWriter;
+ private final TypeInfoHolder rootTypeInfoHolder;
private final MetaStringWriter metaStringWriter;
private final StringSerializer stringSerializer;
private final boolean crossLanguage;
+ private final boolean trackingRef;
private final boolean compressInt;
private final Int64Encoding longEncoding;
private final boolean forVirtualThread;
@@ -85,9 +87,11 @@ public final class WriteContext {
this.generics = generics;
this.typeResolver = typeResolver;
this.refWriter = refWriter;
+ rootTypeInfoHolder = typeResolver.nilTypeInfoHolder();
this.metaStringWriter = metaStringWriter;
stringSerializer = (StringSerializer)
typeResolver.getSerializer(String.class);
crossLanguage = config.isXlang();
+ trackingRef = config.trackingRef();
compressInt = config.compressInt();
longEncoding = config.longEncoding();
forVirtualThread = config.forVirtualThread();
@@ -527,6 +531,30 @@ public final class WriteContext {
}
}
+ /** Writes the root object for one serialization operation. */
+ public void writeRootRef(Object obj) {
+ if (trackingRef) {
+ writeRef(obj, rootTypeInfoHolder);
+ return;
+ }
+ MemoryBuffer buffer = this.buffer;
+ if (obj == null) {
+ buffer.writeByte(Fory.NULL_FLAG);
+ return;
+ }
+ buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG);
+ TypeResolver resolver = typeResolver;
+ TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(),
rootTypeInfoHolder);
+ if (crossLanguage && typeInfo.getType() == UnknownStruct.class) {
+ depth++;
+ typeInfo.getSerializer().write(this, obj);
+ depth--;
+ return;
+ }
+ resolver.writeTypeInfo(this, typeInfo);
+ writeData(typeInfo, obj);
+ }
+
/**
* Writes a non-null, first-seen object together with its type metadata.
*
diff --git
a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
index 94dd96188..c0f002ca9 100644
--- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
+++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java
@@ -962,7 +962,9 @@ public final class MemoryBuffer {
} else {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + 1;
- ensure(newIdx);
+ if (newIdx > size) {
+ globalAllocator.grow(this, newIdx);
+ }
final long pos = address + writerIdx;
UNSAFE.putByte(heapMemory, pos, value);
writerIndex = newIdx;
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 253c14b50..b0c8caf7c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -42,6 +42,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
@@ -245,7 +246,7 @@ public class ClassResolver extends TypeResolver {
public static final short REPLACE_STUB_ID = INTERNAL_TYPE_START_ID + 28;
public static final int NONEXISTENT_META_SHARED_ID = REPLACE_STUB_ID + 1;
- private TypeInfo typeInfoCache;
+ private final TypeInfo[] typeInfoCache;
// Every deserialization for unregistered class will query it, performance
is important.
private final ObjectMap<TypeNameBytes, TypeInfo> compositeNameBytes2TypeInfo
=
new ObjectMap<>(16, foryMapLoadFactor);
@@ -257,12 +258,17 @@ public class ClassResolver extends TypeResolver {
SharedRegistry sharedRegistry,
JITContext jitContext) {
super(config, classLoader, sharedRegistry, jitContext);
- typeInfoCache = NIL_TYPE_INFO;
+ typeInfoCache = new TypeInfo[config.maxDepth() +
TYPE_INFO_CACHE_DEPTH_SLACK];
+ clearTypeInfoCache();
extRegistry.classIdGenerator = NONEXISTENT_META_SHARED_ID + 1;
shimDispatcher = new ShimDispatcher(this);
_addGraalvmClassRegistry(config.getConfigHash(), this);
}
+ private void clearTypeInfoCache() {
+ Arrays.fill(typeInfoCache, NIL_TYPE_INFO);
+ }
+
@Override
public void initialize() {
extRegistry.objectGenericType = buildGenericType(OBJECT_TYPE);
@@ -1245,9 +1251,7 @@ public class ClassResolver extends TypeResolver {
TypeNameBytes typeNameBytes = new TypeNameBytes(typeInfo.namespace,
typeInfo.typeName);
compositeNameBytes2TypeInfo.put(typeNameBytes, typeInfo);
}
- if (typeInfoCache.type == type) {
- typeInfoCache = NIL_TYPE_INFO;
- }
+ clearTypeInfoCache();
}
}
// in order to support customized serializer for abstract or interface.
@@ -1306,7 +1310,7 @@ public class ClassResolver extends TypeResolver {
if (cls.getName().equals(className)) {
LOG.info("Clear serializer for class {}.", className);
entry.getValue().setSerializer(this, Serializers.newSerializer(this,
cls, serializer));
- typeInfoCache = NIL_TYPE_INFO;
+ clearTypeInfoCache();
return;
}
}
@@ -1323,7 +1327,7 @@ public class ClassResolver extends TypeResolver {
if (className.startsWith(classNamePrefix)) {
LOG.info("Clear serializer for class {}.", className);
entry.getValue().setSerializer(this, Serializers.newSerializer(this,
cls, serializer));
- typeInfoCache = NIL_TYPE_INFO;
+ clearTypeInfoCache();
}
}
}
@@ -1570,9 +1574,7 @@ public class ClassResolver extends TypeResolver {
@Override
public void onSuccess(Class<? extends Serializer> result) {
setSerializer(clz, Serializers.newSerializer(ClassResolver.this,
clz, result));
- if (typeInfoCache.type == clz) {
- typeInfoCache = NIL_TYPE_INFO; // clear class info cache
- }
+ clearTypeInfoCache();
Preconditions.checkState(getSerializer(clz).getClass() ==
result);
}
@@ -1707,30 +1709,18 @@ public class ClassResolver extends TypeResolver {
@Internal
public TypeInfo getOrUpdateTypeInfo(Class<?> cls) {
- TypeInfo typeInfo = typeInfoCache;
+ return getOrUpdateTypeInfo(cls, 0);
+ }
+
+ private TypeInfo getOrUpdateTypeInfo(Class<?> cls, int depth) {
+ TypeInfo typeInfo = typeInfoCache[depth];
if (typeInfo.type != cls) {
typeInfo = classInfoMap.get(cls);
if (typeInfo == null || typeInfo.serializer == null) {
addSerializer(cls, createSerializer(cls));
typeInfo = classInfoMap.get(cls);
}
- typeInfoCache = typeInfo;
- }
- return typeInfo;
- }
-
- private TypeInfo getOrUpdateTypeInfo(short classId) {
- TypeInfo typeInfo = typeInfoCache;
- TypeInfo internalInfo = classId < typeIdToTypeInfo.length ?
typeIdToTypeInfo[classId] : null;
- Preconditions.checkArgument(
- internalInfo != null, "Internal class id %s is not registered",
classId);
- if (typeInfo != internalInfo) {
- typeInfo = internalInfo;
- if (typeInfo.serializer == null) {
- addSerializer(typeInfo.type, createSerializer(typeInfo.type));
- typeInfo = classInfoMap.get(typeInfo.type);
- }
- typeInfoCache = typeInfo;
+ typeInfoCache[depth] = typeInfo;
}
return typeInfo;
}
@@ -1881,7 +1871,7 @@ public class ClassResolver extends TypeResolver {
.putCompatibleDeserializerClass(
cls, CodecUtils.loadOrGenStaticCompatibleCodecClass(this, cls,
typeDef));
}
- typeInfoCache = NIL_TYPE_INFO;
+ clearTypeInfoCache();
if (RecordUtils.isRecord(cls)) {
RecordUtils.getRecordConstructor(cls);
RecordUtils.getRecordComponents(cls);
@@ -1995,7 +1985,7 @@ public class ClassResolver extends TypeResolver {
} else if (cls == Long.class) {
buffer.writeVarUInt32Small7(Types.INT64);
} else {
- writeTypeInfo(writeContext, getOrUpdateTypeInfo(cls));
+ writeTypeInfo(writeContext, getOrUpdateTypeInfo(cls,
writeContext.getDepth()));
}
}
@@ -2474,7 +2464,7 @@ public class ClassResolver extends TypeResolver {
}
});
if (GraalvmSupport.isGraalBuildTime()) {
- typeInfoCache = NIL_TYPE_INFO;
+ clearTypeInfoCache();
clearGraalvmGeneratedTypeInfoSerializers();
compositeNameBytes2TypeInfo.forEach(
(typeNameBytes, typeInfo) ->
clearGraalvmTypeInfoSerializer(typeInfo));
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index b10498552..275ccf1d0 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -123,6 +123,7 @@ public abstract class TypeResolver {
static final int INTERNAL_NATIVE_ID_LIMIT = 250;
private static final GenericType OBJECT_GENERIC_TYPE =
GenericType.build(Object.class);
private static final float TYPE_ID_MAP_LOAD_FACTOR = 0.5f;
+ static final int TYPE_INFO_CACHE_DEPTH_SLACK = 10;
static final long MAX_USER_TYPE_ID = 0xffff_fffEL;
private static final class TransformedTypeInfo {
@@ -149,9 +150,9 @@ public abstract class TypeResolver {
final TypeInfo[] typeIdToTypeInfo;
// Map for user-registered type ids, keyed by user id.
final LongMap<TypeInfo> userTypeIdToTypeInfo = new LongMap<>(4,
TYPE_ID_MAP_LOAD_FACTOR);
- // Cache for readTypeInfo(MemoryBuffer) - persists between calls to avoid
reloading
+ // Caches for readTypeInfo(ReadContext) - persist between calls to avoid
reloading
// dynamically created classes that can't be found by Class.forName
- private TypeInfo typeInfoCache;
+ private final TypeInfo[] typeInfoCache;
private boolean registrationFinished;
protected TypeResolver(
@@ -167,6 +168,8 @@ public abstract class TypeResolver {
typeDefMap = sharedRegistry.typeDefMap;
int length = isCrossLanguage() ? Types.BOUND : INTERNAL_NATIVE_ID_LIMIT;
typeIdToTypeInfo = new TypeInfo[length];
+ typeInfoCache = new TypeInfo[config.maxDepth() +
TYPE_INFO_CACHE_DEPTH_SLACK];
+ Arrays.fill(typeInfoCache, NIL_TYPE_INFO);
}
public final Config getConfig() {
@@ -557,6 +560,27 @@ public abstract class TypeResolver {
}
}
+ // Generated exact-class branches call this only after code generation
resolves the TypeInfo to a
+ // compatible struct id. The wire body stays in writeSharedClassMeta; this
helper only skips the
+ // generic type-id switch.
+ @Internal
+ public final void writeCompatibleTypeInfo(
+ WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) {
+ assert isCompatibleStructTypeId(typeInfo.typeId);
+ buffer.writeUInt8(typeInfo.typeId);
+ writeSharedClassMeta(writeContext, buffer, typeInfo);
+ }
+
+ private static boolean isCompatibleStructTypeId(int typeId) {
+ switch (typeId) {
+ case Types.COMPATIBLE_STRUCT:
+ case Types.NAMED_COMPATIBLE_STRUCT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
static int toUserTypeId(long userTypeId) {
Preconditions.checkArgument(
userTypeId >= 0 && userTypeId <= MAX_USER_TYPE_ID,
@@ -576,6 +600,21 @@ public abstract class TypeResolver {
return new Invoke(classResolverRef, "writeTypeInfo", buffer, classInfo);
}
+ // Note: Thread safe for jit thread to call.
+ public Expression writeExactClassExpr(
+ Expression classResolverRef,
+ Expression writeContext,
+ Expression buffer,
+ Expression classInfo,
+ Class<?> cls) {
+ TypeInfo typeInfo = getTypeInfo(cls);
+ if (isCompatibleStructTypeId(typeInfo.typeId)) {
+ return new Invoke(
+ classResolverRef, "writeCompatibleTypeInfo", writeContext, buffer,
classInfo);
+ }
+ return writeClassExpr(classResolverRef, writeContext, classInfo);
+ }
+
/**
* Writes shared class metadata using the meta-share protocol. Protocol: If
class already written,
* writes {@code (index << 1) | 1} (reference). If new class, writes {@code
(index << 1)} followed
@@ -585,6 +624,11 @@ public abstract class TypeResolver {
*/
protected final void writeSharedClassMeta(WriteContext writeContext,
TypeInfo typeInfo) {
MemoryBuffer buffer = writeContext.getBuffer();
+ writeSharedClassMeta(writeContext, buffer, typeInfo);
+ }
+
+ private void writeSharedClassMeta(
+ WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) {
MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext();
assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG;
IdentityObjectIntMap<Class<?>> classMap = metaWriteContext.classMap;
@@ -619,6 +663,8 @@ public abstract class TypeResolver {
*/
public final TypeInfo readTypeInfo(ReadContext readContext) {
MemoryBuffer buffer = readContext.getBuffer();
+ int depth = readContext.getDepth();
+ TypeInfo cachedTypeInfo = typeInfoCache[depth];
int typeId = buffer.readUInt8();
TypeInfo typeInfo;
switch (typeId) {
@@ -626,20 +672,20 @@ public abstract class TypeResolver {
case Types.STRUCT:
case Types.EXT:
case Types.TYPED_UNION:
- typeInfo =
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+ typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(),
cachedTypeInfo);
break;
case Types.COMPATIBLE_STRUCT:
case Types.NAMED_COMPATIBLE_STRUCT:
- typeInfo = readSharedClassTypeInfo(readContext, null);
+ typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo);
break;
case Types.NAMED_ENUM:
case Types.NAMED_STRUCT:
case Types.NAMED_EXT:
case Types.NAMED_UNION:
if (!metaContextShareEnabled) {
- typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId);
+ typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo,
typeId);
} else {
- typeInfo = readSharedClassTypeInfo(readContext, null);
+ typeInfo = readSharedClassTypeInfo(readContext, null,
cachedTypeInfo);
}
break;
case Types.LIST:
@@ -654,7 +700,7 @@ public abstract class TypeResolver {
if (typeInfo.serializer == null) {
typeInfo = ensureSerializerForTypeInfo(typeInfo);
}
- typeInfoCache = typeInfo;
+ typeInfoCache[depth] = typeInfo;
return typeInfo;
}
@@ -664,6 +710,8 @@ public abstract class TypeResolver {
*/
public final TypeInfo readTypeInfo(ReadContext readContext, Class<?>
targetClass) {
MemoryBuffer buffer = readContext.getBuffer();
+ int depth = readContext.getDepth();
+ TypeInfo cachedTypeInfo = typeInfoCache[depth];
int typeId = buffer.readUInt8();
TypeInfo typeInfo;
switch (typeId) {
@@ -671,20 +719,20 @@ public abstract class TypeResolver {
case Types.STRUCT:
case Types.EXT:
case Types.TYPED_UNION:
- typeInfo =
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+ typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(),
cachedTypeInfo);
break;
case Types.COMPATIBLE_STRUCT:
case Types.NAMED_COMPATIBLE_STRUCT:
- typeInfo = readSharedClassMeta(readContext, targetClass);
+ typeInfo = readSharedClassMeta(readContext, targetClass,
cachedTypeInfo);
break;
case Types.NAMED_ENUM:
case Types.NAMED_STRUCT:
case Types.NAMED_EXT:
case Types.NAMED_UNION:
if (!metaContextShareEnabled) {
- typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId);
+ typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo,
typeId);
} else {
- typeInfo = readSharedClassMeta(readContext, targetClass);
+ typeInfo = readSharedClassMeta(readContext, targetClass,
cachedTypeInfo);
}
break;
case Types.LIST:
@@ -699,7 +747,7 @@ public abstract class TypeResolver {
if (typeInfo.serializer == null) {
typeInfo = ensureSerializerForTypeInfo(typeInfo);
}
- typeInfoCache = typeInfo;
+ typeInfoCache[depth] = typeInfo;
return typeInfo;
}
@@ -721,7 +769,7 @@ public abstract class TypeResolver {
case Types.STRUCT:
case Types.EXT:
case Types.TYPED_UNION:
- typeInfo =
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+ typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(),
typeInfoCache);
break;
case Types.COMPATIBLE_STRUCT:
case Types.NAMED_COMPATIBLE_STRUCT:
@@ -763,28 +811,27 @@ public abstract class TypeResolver {
public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder
classInfoHolder) {
MemoryBuffer buffer = readContext.getBuffer();
int typeId = buffer.readUInt8();
+ TypeInfo cachedTypeInfo = classInfoHolder.typeInfo;
TypeInfo typeInfo;
- boolean updateCache = false;
switch (typeId) {
case Types.ENUM:
case Types.STRUCT:
case Types.EXT:
case Types.TYPED_UNION:
- typeInfo =
Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32()));
+ typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(),
cachedTypeInfo);
break;
case Types.COMPATIBLE_STRUCT:
case Types.NAMED_COMPATIBLE_STRUCT:
- typeInfo = readSharedClassTypeInfo(readContext, null);
+ typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo);
break;
case Types.NAMED_ENUM:
case Types.NAMED_STRUCT:
case Types.NAMED_EXT:
case Types.NAMED_UNION:
if (!metaContextShareEnabled) {
- typeInfo = readTypeInfoFromBytes(readContext,
classInfoHolder.typeInfo, typeId);
- updateCache = true;
+ typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo,
typeId);
} else {
- typeInfo = readSharedClassTypeInfo(readContext, null);
+ typeInfo = readSharedClassTypeInfo(readContext, null,
cachedTypeInfo);
}
break;
case Types.LIST:
@@ -796,22 +843,24 @@ public abstract class TypeResolver {
default:
typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId));
}
+ // Do not return internal TypeInfo above before this check: lazy codegen
can briefly clear a
+ // cached TypeInfo serializer while resolving recursive serializers.
if (typeInfo.serializer == null) {
typeInfo = ensureSerializerForTypeInfo(typeInfo);
}
- if (updateCache) {
+ if (typeInfo != cachedTypeInfo) {
classInfoHolder.typeInfo = typeInfo;
}
return typeInfo;
}
- /**
- * Read class info using the provided cache. Returns cached TypeInfo if the
namespace and type
- * name bytes match.
- */
- protected final TypeInfo readTypeInfoByCache(
- ReadContext readContext, TypeInfo typeInfoCache, int header) {
- return readTypeInfoFromBytes(readContext, typeInfoCache, header);
+ private TypeInfo readRegisteredTypeInfo(int typeId, int userTypeId, TypeInfo
cachedTypeInfo) {
+ TypeInfo typeInfo = cachedTypeInfo;
+ if (typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) {
+ typeInfo = userTypeIdToTypeInfo.get(userTypeId);
+ assert typeInfo != null;
+ }
+ return typeInfo;
}
/**
@@ -849,7 +898,12 @@ public abstract class TypeResolver {
}
public final TypeInfo readSharedClassMeta(ReadContext readContext, Class<?>
targetClass) {
- TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass);
+ return readSharedClassMeta(readContext, targetClass, null);
+ }
+
+ private TypeInfo readSharedClassMeta(
+ ReadContext readContext, Class<?> targetClass, TypeInfo cachedTypeInfo) {
+ TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass,
cachedTypeInfo);
Class<?> readClass = typeInfo.getType();
if (targetClass != readClass) {
return getTargetTypeInfo(typeInfo, targetClass);
@@ -858,6 +912,11 @@ public abstract class TypeResolver {
}
private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class<?>
targetClass) {
+ return readSharedClassTypeInfo(readContext, targetClass, null);
+ }
+
+ private TypeInfo readSharedClassTypeInfo(
+ ReadContext readContext, Class<?> targetClass, TypeInfo cachedTypeInfo) {
MemoryBuffer buffer = readContext.getBuffer();
MetaReadContext metaReadContext = readContext.getMetaReadContext();
assert metaReadContext != null : SET_META_READ_CONTEXT_MSG;
@@ -875,7 +934,14 @@ public abstract class TypeResolver {
// body/hash/schema-limit/exact-local checks here; the header-miss path
owns them before
// cache publish.
long id = buffer.readInt64();
- typeInfo = extRegistry.typeInfoByTypeDefId.get(id);
+ TypeDef cachedTypeDef = cachedTypeInfo == null ? null :
cachedTypeInfo.getTypeDef();
+ // A field-local cache hit is valid only when the cached TypeInfo
carries the exact checked
+ // TypeDef id that was parsed and accepted earlier by this resolver.
+ if (cachedTypeDef != null && cachedTypeDef.getId() == id) {
+ typeInfo = cachedTypeInfo;
+ } else {
+ typeInfo = extRegistry.typeInfoByTypeDefId.get(id);
+ }
if (typeInfo != null) {
TypeDef.skipTypeDef(buffer, id);
} else {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
index f07098d6c..7e7e1d27b 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java
@@ -26,11 +26,13 @@ import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
+import org.apache.fory.annotation.CodegenInvoke;
import org.apache.fory.annotation.ForyEnumId;
import org.apache.fory.collection.LongMap;
import org.apache.fory.config.Config;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
+import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.util.Preconditions;
@SuppressWarnings("rawtypes")
@@ -65,15 +67,25 @@ public class EnumSerializer extends
ImmutableSerializer<Enum> implements Shareab
@Override
public void write(WriteContext writeContext, Enum value) {
+ writeValue(writeContext, writeContext.getBuffer(), value);
+ }
+
+ @CodegenInvoke
+ public final void writeValue(WriteContext writeContext, MemoryBuffer buffer,
Enum value) {
if (!config.isXlang() && config.serializeEnumByName()) {
writeContext.writeString(value.name());
} else {
-
writeContext.getBuffer().writeVarUInt32Small7(tagByOrdinal[value.ordinal()]);
+ buffer.writeVarUInt32Small7(tagByOrdinal[value.ordinal()]);
}
}
@Override
public Enum read(ReadContext readContext) {
+ return readValue(readContext, readContext.getBuffer());
+ }
+
+ @CodegenInvoke
+ public final Enum readValue(ReadContext readContext, MemoryBuffer buffer) {
if (!config.isXlang() && config.serializeEnumByName()) {
String name = readContext.readString();
Enum e = stringToEnum.get(name);
@@ -82,7 +94,7 @@ public class EnumSerializer extends ImmutableSerializer<Enum>
implements Shareab
}
return handleUnknownEnumValue(name);
} else {
- int tag = readContext.getBuffer().readVarUInt32Small7();
+ int tag = buffer.readVarUInt32Small7();
Enum value = null;
if (enumConstantByTagArray != null && tag <
enumConstantByTagArray.length) {
value = enumConstantByTagArray[tag];
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
index bb3d93f22..2ad935101 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java
@@ -537,16 +537,19 @@ public final class StringSerializer extends
ImmutableSerializer<String> {
int bytesLen = bytes.length;
long header = ((long) bytesLen << 2) | coder;
int writerIndex = buffer.writerIndex();
- // The `ensure` ensure next operations are safe without bound checks,
- // and inner heap buffer doesn't change.
- buffer.ensure(writerIndex + 9 + bytesLen); // 1 byte coder + varint max 8
bytes
+ buffer.ensure(writerIndex + 9 + bytesLen);
final byte[] targetArray = buffer.getHeapMemory();
if (targetArray != null) {
- // Some JDK11 Unsafe.copyMemory will `copyMemoryChecks`, and
- // jvm doesn't eliminate well in some jdk.
final int targetIndex = buffer._unsafeHeapWriterIndex();
int arrIndex = targetIndex;
- arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex,
header);
+ if (header >>> 7 == 0) {
+ targetArray[arrIndex++] = (byte) header;
+ } else if (header >>> 14 == 0) {
+ targetArray[arrIndex++] = (byte) ((header & 0x7F) | 0x80);
+ targetArray[arrIndex++] = (byte) (header >>> 7);
+ } else {
+ arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex,
header);
+ }
writerIndex += arrIndex - targetIndex;
System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen);
} else {
@@ -644,7 +647,7 @@ public final class StringSerializer extends
ImmutableSerializer<String> {
byte[] heapMemory = buffer.getHeapMemory();
if (heapMemory != null) {
final int arrIndex = buffer._unsafeHeapReaderIndex();
- buffer.increaseReaderIndex(numBytes);
+ buffer._increaseReaderIndexUnsafe(numBytes);
bytes = new byte[numBytes];
System.arraycopy(heapMemory, arrIndex, bytes, 0, numBytes);
} else {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
index 15b53b575..aadd27071 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
@@ -175,12 +175,13 @@ public class UnionSerializer extends Serializer<Union> {
if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) {
// ref value or not-null value
TypeInfo declared = getFinalCaseTypeInfo(caseId);
- TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared);
if (declared != null) {
+ TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared);
Serializer serializer = getCaseSerializer(caseId,
readTypeInfo.getTypeId(), declared);
GenericType genericType = getCaseGenericType(caseId,
readTypeInfo.getTypeId());
caseValue = readCaseValue(readContext, serializer, genericType);
} else {
+ TypeInfo readTypeInfo = resolver.readTypeInfo(readContext);
caseValue = Serializers.read(readContext,
readTypeInfo.getSerializer());
}
readContext.setReadRef(nextReadRefId, caseValue);
@@ -333,7 +334,12 @@ public class UnionSerializer extends Serializer<Union> {
int nextReadRefId = readContext.tryPreserveRefId();
if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) {
TypeInfo declared = getDeclaredCaseTypeInfo(fieldInfo, typeId);
- TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared);
+ TypeInfo readTypeInfo;
+ if (declared != null) {
+ readTypeInfo = resolver.readTypeInfo(readContext, declared);
+ } else {
+ readTypeInfo = resolver.readTypeInfo(readContext);
+ }
Serializer serializer = getCaseSerializer(fieldInfo,
readTypeInfo.getTypeId(), readTypeInfo);
Object caseValue =
readCaseValue(
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
index ef03486e1..1356d80a3 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java
@@ -21,6 +21,7 @@ package org.apache.fory.serializer;
import java.util.ArrayList;
import java.util.List;
+import org.apache.fory.annotation.CodegenInvoke;
import org.apache.fory.collection.IdentityObjectIntMap;
import org.apache.fory.collection.LongMap;
import org.apache.fory.collection.MapEntry;
@@ -294,21 +295,32 @@ public final class UnknownClassSerializers {
@Override
public void write(WriteContext writeContext, UnknownEnum value) {
+ writeValue(writeContext, writeContext.getBuffer(), value);
+ }
+
+ @CodegenInvoke
+ public final void writeValue(
+ WriteContext writeContext, MemoryBuffer buffer, UnknownEnum value) {
if (!config.isXlang() && config.serializeEnumByName()) {
writeContext.writeString(value.name());
} else {
- writeContext.getBuffer().writeVarUInt32Small7(value.ordinal());
+ buffer.writeVarUInt32Small7(value.ordinal());
}
}
@Override
public UnknownEnum read(ReadContext readContext) {
+ return readValue(readContext, readContext.getBuffer());
+ }
+
+ @CodegenInvoke
+ public final UnknownEnum readValue(ReadContext readContext, MemoryBuffer
buffer) {
if (!config.isXlang() && config.serializeEnumByName()) {
readContext.readString();
return UnknownEnum.UNKNOWN;
}
- int ordinal = readContext.getBuffer().readVarUInt32Small7();
+ int ordinal = buffer.readVarUInt32Small7();
if (ordinal >= enumConstants.length) {
return UnknownEnum.UNKNOWN;
}
diff --git
a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
index 7fbf021ff..52c4d0ce5 100644
--- a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
+++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java
@@ -1108,7 +1108,9 @@ public final class MemoryBuffer {
public void writeByte(byte value) {
final int writerIdx = writerIndex;
final int newIdx = writerIdx + 1;
- ensure(newIdx);
+ if (newIdx > size) {
+ globalAllocator.grow(this, newIdx);
+ }
final long pos = address + writerIdx;
storeByte(pos, value);
writerIndex = newIdx;
diff --git
a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
index 41702a442..c347b628a 100644
--- a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java
@@ -20,14 +20,11 @@
package org.apache.fory.builder;
import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertSame;
-import static org.testng.Assert.assertTrue;
import org.apache.fory.Fory;
import org.apache.fory.ForyTestBase;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.memory.MemoryUtils;
-import org.apache.fory.platform.JdkVersion;
import org.apache.fory.resolver.TypeResolver;
import org.apache.fory.test.bean.BeanA;
import org.testng.annotations.Test;
@@ -45,7 +42,6 @@ public class CodecUtilsTest {
.withCompatible(false)
.build();
Class<?> seqCodecClass =
fory.getTypeResolver().getSerializerClass(BeanA.class);
- assertGeneratedClassShape(seqCodecClass, BeanA.class);
Generated.GeneratedSerializer serializer =
seqCodecClass
.asSubclass(Generated.GeneratedSerializer.class)
@@ -58,15 +54,4 @@ public class CodecUtilsTest {
Object obj = ForyTestBase.readSerializer(fory, serializer,
MemoryUtils.wrap(bytes));
assertEquals(obj, beanA);
}
-
- private static void assertGeneratedClassShape(Class<?> serializerClass,
Class<?> beanClass)
- throws Exception {
- if (JdkVersion.MAJOR_VERSION >= 25) {
- assertTrue((Boolean)
Class.class.getMethod("isHidden").invoke(serializerClass));
- assertSame(Class.class.getMethod("getNestHost").invoke(serializerClass),
beanClass);
- } else {
- assertSame(
-
serializerClass.getClassLoader().loadClass(serializerClass.getName()),
serializerClass);
- }
- }
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
index 756c00e55..c91041495 100644
---
a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
+++
b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java
@@ -60,6 +60,41 @@ public class StringSerializerTest extends ForyTestBase {
Assert.assertThrows(IllegalArgumentException.class, () ->
serializer.readString(buffer));
}
+ @Test
+ public void testBytesStringWire() {
+ if (!stringValueIsBytes()) {
+ throw new SkipException("Skip when string value is not byte[]");
+ }
+ String[] values = {
+ "",
+ "a",
+ StringUtils.random(31),
+ StringUtils.random(40),
+ StringUtils.random(5000),
+ "你好",
+ "abc你好"
+ };
+ for (String value : values) {
+ int capacity = Math.max(64, value.length() * 8 + 64);
+ MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(capacity);
+ StringSerializer.writeBytesString(buffer, value);
+ Assert.assertEquals(
+ readJDK11String(MemoryBuffer.fromByteArray(buffer.getBytes(0,
buffer.writerIndex()))),
+ value);
+ }
+ }
+
+ private static boolean stringValueIsBytes() {
+ try {
+ Field valueIsBytesField =
+
StringSerializer.class.getDeclaredField("STRING_VALUE_FIELD_IS_BYTES");
+ valueIsBytesField.setAccessible(true);
+ return (boolean) valueIsBytesField.get(null);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
@Test
public void testJavaStringZeroCopy() {
if (JdkVersion.MAJOR_VERSION >= 17) {
diff --git a/java/fory-json/pom.xml b/java/fory-json/pom.xml
index bfaca0bda..5fd9652b7 100644
--- a/java/fory-json/pom.xml
+++ b/java/fory-json/pom.xml
@@ -39,6 +39,8 @@
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<fory.java.rootdir>${basedir}/..</fory.java.rootdir>
+
<fory.json.jdk25.test.classes>${project.build.directory}/jdk25-test-classes</fory.json.jdk25.test.classes>
+
<fory.core.jdk25.test.classes>${project.basedir}/../fory-core/target/jdk25-test-classes</fory.core.jdk25.test.classes>
</properties>
<dependencies>
@@ -153,5 +155,57 @@
</plugins>
</build>
</profile>
+ <profile>
+ <id>jdk25-core-test-overlay</id>
+ <activation>
+ <jdk>[25,)</jdk>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>3.1.0</version>
+ <executions>
+ <execution>
+ <id>prepare-jdk25-test-classes</id>
+ <phase>process-test-classes</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <delete dir="${fory.json.jdk25.test.classes}"/>
+ <mkdir dir="${fory.json.jdk25.test.classes}"/>
+ <!-- Reactor tests see dependency target/classes instead
of the packaged
+ multi-release jar. Put Fory core's JDK25 test overlay
ahead of the raw
+ dependency classes so JsonCodegen exercises the same
CodecBuilder owner
+ path as packaged JDK25 runs. -->
+ <copy todir="${fory.json.jdk25.test.classes}">
+ <fileset dir="${project.build.outputDirectory}">
+ <exclude name="module-info.class"/>
+ </fileset>
+ </copy>
+ <copy todir="${fory.json.jdk25.test.classes}"
overwrite="true">
+ <fileset dir="${fory.core.jdk25.test.classes}"
erroronmissingdir="false">
+ <exclude name="module-info.class"/>
+ </fileset>
+ </copy>
+ </target>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>${argLine}
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED</argLine>
+
<classesDirectory>${fory.json.jdk25.test.classes}</classesDirectory>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
</profiles>
</project>
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
index a98c7e6d5..df786e028 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java
@@ -40,7 +40,6 @@ import org.apache.fory.json.reader.Utf8ObjectReader;
import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.json.writer.StringObjectWriter;
import org.apache.fory.json.writer.Utf8ObjectWriter;
-import org.apache.fory.platform.JdkVersion;
import org.apache.fory.util.record.RecordUtils;
public final class JsonCodegen {
@@ -121,8 +120,7 @@ public final class JsonCodegen {
new JsonGeneratedCodecBuilder(this, className, type, properties, utf8,
true, false)
.genCode();
try {
- CompileUnit unit = new CompileUnit(PACKAGE, className, code,
JsonCodegen.class);
- Class<?> writerClass = codeGenerator.compileAndLoad(unit, state ->
state.lock.lock());
+ Class<?> writerClass = compileClass(className, code);
Constructor<?> constructor =
writerClass.getDeclaredConstructor(JsonFieldInfo[].class,
JsonCodec[].class);
constructor.setAccessible(true);
@@ -143,8 +141,7 @@ public final class JsonCodegen {
new JsonGeneratedCodecBuilder(this, className, type, properties,
false, false, record)
.genCode();
try {
- CompileUnit unit = new CompileUnit(PACKAGE, className, code,
JsonCodegen.class);
- Class<?> readerClass = codeGenerator.compileAndLoad(unit, state ->
state.lock.lock());
+ Class<?> readerClass = compileClass(className, code);
Constructor<?> constructor =
readerClass.getDeclaredConstructor(
JsonFieldInfo[].class, JsonCodec[].class,
BaseObjectCodec[].class);
@@ -155,6 +152,12 @@ public final class JsonCodegen {
}
}
+ private Class<?> compileClass(String className, String code) throws
ClassNotFoundException {
+ CompileUnit unit = new CompileUnit(PACKAGE, className, code);
+ ClassLoader classLoader = codeGenerator.compile(unit);
+ return classLoader.loadClass(PACKAGE + "." + className);
+ }
+
private static JsonCodec[] writeCodecs(JsonFieldInfo[] properties) {
JsonCodec[] codecs = new JsonCodec[properties.length];
for (int i = 0; i < properties.length; i++) {
@@ -230,15 +233,7 @@ public final class JsonCodegen {
}
private boolean canCompile(Class<?> type) {
- return supportsHiddenNestmateLoading()
- && CodeGenerator.sourcePublicAccessible(type)
- && isVisible(type);
- }
-
- private static boolean supportsHiddenNestmateLoading() {
- // Generated JSON codecs are defined as hidden nestmates of JsonCodegen;
JDK 8 must keep using
- // the interpreter path because hidden classes are unavailable there.
- return JdkVersion.MAJOR_VERSION >= 15;
+ return CodeGenerator.sourcePublicAccessible(type) && isVisible(type);
}
private boolean isVisible(Class<?> type) {
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
index cdbd4b4d1..390eca491 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java
@@ -50,7 +50,6 @@ import org.apache.fory.json.data.PrivateFields;
import org.apache.fory.json.data.PublicFields;
import org.apache.fory.json.data.TokenValues;
import org.apache.fory.json.data.UnicodeMatrix;
-import org.apache.fory.platform.JdkVersion;
import org.apache.fory.reflect.FieldAccessor;
import org.testng.SkipException;
@@ -270,7 +269,7 @@ public abstract class ForyJsonTestModels {
}
protected static void assertGeneratedWhenSupported(ForyJson json, Class<?>
type) {
- assertEquals(json.hasGeneratedWriter(type), JdkVersion.MAJOR_VERSION >=
15);
+ assertTrue(json.hasGeneratedWriter(type));
}
protected static String repeat(char ch, int length) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]