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 fb311aa06 feat(java): support Fory JSON in GraalVM native image (#3846)
fb311aa06 is described below
commit fb311aa0609480199ffaec3ca56a8001453dd0fb
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jul 15 08:23:38 2026 +0530
feat(java): support Fory JSON in GraalVM native image (#3846)
## Why?
Fory JSON generates codecs from each `ForyJsonBuilder` configuration.
Those configurations affect
field discovery, naming, null handling, custom codecs, and generated
source, so one codec generated
during Native Image construction cannot represent every runtime builder.
Native executables also
cannot run the existing runtime compiler.
This change supports the complete interpreted JSON codec path in GraalVM
Native Image while keeping
ordinary JVM code generation enabled and unchanged.
## What does this PR do?
- adds the non-inherited `@JsonType` marker for reachable native JSON
object models;
- adds an automatically activated MR-JAR Native Image Feature that
registers the reflection,
constructor, unsafe-field, record, container, built-in, and
annotation-codec metadata used by the
interpreted path;
- supports declaration, inherited, nested type-use, record, accessor,
and creator `@JsonCodec`
locations without constructing codecs at image build time;
- disables JSON runtime code generation and asynchronous compilation
only inside native images;
- preserves every other `ForyJsonBuilder` option for independently
configured runtime instances;
- adds classpath and JPMS GraalVM acceptance coverage, MR-JAR/source-JAR
verification, and user
documentation.
The hosted scanner reuses the existing Fory core metadata owner and the
effective JSON-member
classification owned by `ObjectCodecBuilder`. Hosted work does not enter
serialization or
deserialization hot paths, and the ordinary-JDK builder loop remains
unchanged.
## Related issues
None.
## AI Contribution Checklist
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change? Adds `@JsonType`.
- [ ] Does this PR introduce any binary protocol compatibility change?
## Validation
- `cd java && ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T8 -pl fory-core,fory-json
-am test`
- Fory core: 2,184 tests, 0 failures
- Fory JSON: 593 tests, 0 failures
- GraalVM 21 classpath native package/run: passed, `Fory JSON succeed`
- GraalVM 21 JPMS native package/run: passed, `Fory JSON succeed`
- GraalVM 25 classpath native package/run: passed, `Fory JSON succeed`
- GraalVM 25 JPMS native package/run: passed, `Fory JSON succeed`
- Fory JSON MR-JAR and source-JAR verification: passed
- Java and integration Spotless checks: passed
- Markdown Prettier and `git diff --check`: passed
## Benchmark
Not applicable. The change adds Native Image hosted metadata discovery
and native-only builder
resolution. It does not change ordinary JVM
serialization/deserialization hot paths, and no new
runtime allocation is introduced there.
---
docs/guide/java/graalvm-support.md | 45 ++
docs/guide/java/index.md | 2 +-
docs/guide/java/json-support.md | 22 +-
integration_tests/graalvm_tests/pom.xml | 9 +-
.../graalvm_tests/src/main/java/module-info.java | 5 +-
.../org/apache/fory/graalvm/ForyJsonExample.java | 573 +++++++++++++++++++++
.../main/java/org/apache/fory/graalvm/Main.java | 1 +
java/README.md | 4 +-
.../org/apache/fory/util/record/RecordUtils.java | 59 ++-
.../apache/fory/platform/ForyGraalVMFeature.java | 5 +
.../fory-core/native-image.properties | 1 +
.../fory/builder/GraalvmFeatureJarVerifier.java | 5 +
java/fory-json/README.md | 26 +-
java/fory-json/pom.xml | 164 +++++-
.../java/org/apache/fory/json/ForyJsonBuilder.java | 17 +-
.../org/apache/fory/json/annotation/JsonType.java | 26 +-
.../apache/fory/json/codec/ObjectCodecBuilder.java | 31 ++
.../org/apache/fory/json/meta/JsonTypeUse.java | 4 +-
.../fory/json/resolver/JsonSharedRegistry.java | 8 +-
.../fory/json/codec/ForyJsonGraalVMFeature.java | 384 ++++++++++++++
java/fory-json/src/main/java9/module-info.java | 2 +
.../fory-json/native-image.properties | 18 +
.../json/ForyJsonGraalVMFeatureJarVerifier.java} | 39 +-
23 files changed, 1355 insertions(+), 95 deletions(-)
diff --git a/docs/guide/java/graalvm-support.md
b/docs/guide/java/graalvm-support.md
index 72c8467bc..37095c1b5 100644
--- a/docs/guide/java/graalvm-support.md
+++ b/docs/guide/java/graalvm-support.md
@@ -44,6 +44,51 @@ shapes. Application classes still need to be registered with
Fory before seriali
Fory disables asynchronous serializer compilation in a native image because
runtime just-in-time
compilation is unavailable.
+## Fory JSON
+
+Fory JSON uses a separate Native Image workflow. It has no type-registration
API and does not
+create a `ForyJson` instance or generate JSON codecs while the image is built.
Add `@JsonType` to
+each object model that the native executable reads or writes:
+
+```java
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.JsonType;
+
+@JsonType
+public final class User {
+ public long id;
+ public String name;
+}
+
+public class JsonExample {
+ public static void main(String[] args) {
+ ForyJson json = ForyJson.builder().build();
+ User user = json.fromJson("{\"id\":1,\"name\":\"Ada\"}", User.class);
+ System.out.println(json.toJson(user));
+ }
+}
+```
+
+The `fory-json` artifact activates its Native Image Feature automatically.
Reachable `@JsonType`
+classes gate object-model metadata. `@JsonType` is not inherited, so annotate
every concrete runtime
+model. An annotated base with a class-literal `@JsonSubTypes` table registers
those listed subtypes
+automatically. Reachable concrete `Collection` and `Map` root types are also
supported when they
+have the public no-argument constructor required by Fory JSON. Reachable
`@JsonCodec` declarations
+register their codec constructor even when the declaration target is not an
object model. A class
+referenced only by a runtime string is not reachable;
`JsonSubTypes.Type.className` is therefore
+unsupported in a native image.
+
+Native execution uses Fory JSON's interpreted object codec.
`ForyJson.builder()` automatically
+disables runtime code generation and asynchronous compilation in the native
executable, while all
+other builder options retain their normal behavior. Applications can create
differently configured
+`ForyJson` instances at runtime and do not need build-time initialization or
reflection
+configuration.
+
+Declaration-level, inherited, and nested type-use `@JsonCodec` annotations are
supported. An
+annotation codec must have the same public no-argument constructor required on
the JVM. In a named
+module, export or open its package to `org.apache.fory.json`. A codec instance
supplied through
+`registerCodec` is constructed by the application and needs no
annotation-constructor metadata.
+
## Basic Usage
### Create Fory and Register Classes
diff --git a/docs/guide/java/index.md b/docs/guide/java/index.md
index e276e6d52..eb14921fd 100644
--- a/docs/guide/java/index.md
+++ b/docs/guide/java/index.md
@@ -254,4 +254,4 @@ ThreadSafeFory threadLocalFory = Fory.builder()
- [Native Serialization](native-serialization.md) - Java-only serialization
features
- [JSON Support](json-support.md) - Complete Fory JSON user guide
- [Static Generated Serializers](static-generated-serializers.md) -
Annotation-processor static generated serializers for `@ForyStruct`
-- [GraalVM Support](graalvm-support.md) - Build-time serializer compilation
for native images
+- [GraalVM Support](graalvm-support.md) - Native Image support for binary
serialization and JSON
diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md
index 343ed46e8..1956b5b3c 100644
--- a/docs/guide/java/json-support.md
+++ b/docs/guide/java/json-support.md
@@ -299,10 +299,13 @@ are rejected.
Depth, concurrency, and retained buffer limits must be positive. The buffer
setting does not limit
output size. Builder changes after `build()` do not mutate an existing runtime.
+In a GraalVM native image, runtime code generation and asynchronous
compilation are automatically
+disabled. Every other builder option keeps the behavior described above.
+
## Annotations
Fory JSON provides `JsonProperty`, `JsonPropertyOrder`, `JsonIgnore`,
`JsonAnyProperty`,
-`JsonAnyGetter`, `JsonAnySetter`, `JsonCreator`, `JsonCodec`, and
`JsonSubTypes` under
+`JsonAnyGetter`, `JsonAnySetter`, `JsonCreator`, `JsonCodec`, `JsonSubTypes`,
and `JsonType` under
`org.apache.fory.json.annotation`. They are not Jackson, Gson, or Fory
binary-protocol annotations.
```java
@@ -318,8 +321,13 @@ import org.apache.fory.json.annotation.JsonIgnore;
import org.apache.fory.json.annotation.JsonProperty;
import org.apache.fory.json.annotation.JsonPropertyOrder;
import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
```
+`JsonType` marks a reachable object model for GraalVM Native Image metadata.
It has no effect on
+ordinary JVM JSON behavior and is not inherited. See [GraalVM
Support](graalvm-support.md) for the
+complete native-image workflow.
+
### `JsonProperty`
An annotation on a field, getter, or setter configures the complete merged
logical property:
@@ -628,7 +636,8 @@ parent does not admit descendants. The annotation is read
from the declared base
inherited from another annotated base. Null is plain JSON null unless codec
precedence selects a
custom complete-value codec for the declared base, replacing the annotation.
Readers accept only
the configured shape, so changing inclusion is a wire-format change. At
GraalVM native-image
-runtime, use class literals instead of `className`.
+runtime, annotate the base with `JsonType` and use class literals instead of
`className`. Listed
+class-literal subtypes are registered automatically.
## Custom codecs
@@ -925,10 +934,11 @@ succeed. Returning a plain parent while decoding the
child fails with `ForyJsonE
names the target type, codec class, declaring type, and actual returned type.
The actual result,
not the parent's `final` status or the codec's generic signature, determines
validity.
-`@JsonCodec` is supported on ordinary JVMs. Android and GraalVM native image
ignore both declaration
-and type-use forms, avoiding partial behavior when nested runtime type
metadata is unavailable. Use
-exact `registerCodec` registration there. Native-image reflection
configuration is not a supported
-way to enable part of this annotation feature.
+`@JsonCodec` is supported on ordinary JVMs and GraalVM native images,
including inherited
+declarations and nested type uses. Native models must follow the `JsonType`
workflow described in
+[GraalVM Support](graalvm-support.md), which registers the annotation codec's
public no-argument
+constructor. Android ignores declaration and type-use forms; use exact
`registerCodec`
+registration there.
## Type validation and untrusted input
diff --git a/integration_tests/graalvm_tests/pom.xml
b/integration_tests/graalvm_tests/pom.xml
index ce204a5a1..13acb89b9 100644
--- a/integration_tests/graalvm_tests/pom.xml
+++ b/integration_tests/graalvm_tests/pom.xml
@@ -62,6 +62,11 @@
<artifactId>fory-core</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-json</artifactId>
+ <version>${project.version}</version>
+ </dependency>
</dependencies>
<build>
@@ -254,7 +259,7 @@
</goals>
<configuration>
<excludeTransitive>true</excludeTransitive>
- <includeArtifactIds>fory-core</includeArtifactIds>
+ <includeArtifactIds>fory-core,fory-json</includeArtifactIds>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/module-path</outputDirectory>
</configuration>
@@ -280,7 +285,7 @@
<argument>-o</argument>
<argument>${project.build.directory}/${moduleImageName}</argument>
<argument>--module-path</argument>
-
<argument>${project.build.outputDirectory}${path.separator}${project.build.directory}/module-path/fory-core-${project.version}.jar</argument>
+
<argument>${project.build.outputDirectory}${path.separator}${project.build.directory}/module-path/fory-core-${project.version}.jar${path.separator}${project.build.directory}/module-path/fory-json-${project.version}.jar</argument>
<argument>--module</argument>
<argument>${mainModule}/${mainClass}</argument>
</arguments>
diff --git a/integration_tests/graalvm_tests/src/main/java/module-info.java
b/integration_tests/graalvm_tests/src/main/java/module-info.java
index d3e59bb81..e514b7beb 100644
--- a/integration_tests/graalvm_tests/src/main/java/module-info.java
+++ b/integration_tests/graalvm_tests/src/main/java/module-info.java
@@ -19,9 +19,10 @@
module org.apache.fory.graalvm.tests {
requires org.apache.fory.core;
+ requires org.apache.fory.json;
+ requires java.sql;
- // Fory-generated codecs are defined by the existing codegen class loader,
so they access the
- // registered test models from its unnamed module.
+ // Fory-generated codecs and annotation codecs access the test models from
library modules.
exports org.apache.fory.graalvm;
exports org.apache.fory.graalvm.record;
diff --git
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
new file mode 100644
index 000000000..926608d5d
--- /dev/null
+++
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java
@@ -0,0 +1,573 @@
+/*
+ * 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.graalvm;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.PropertyNamingStrategy;
+import org.apache.fory.json.annotation.JsonAnyProperty;
+import org.apache.fory.json.annotation.JsonCodec;
+import org.apache.fory.json.annotation.JsonCreator;
+import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
+import org.apache.fory.json.codec.JsonValueCodec;
+import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.reader.Utf16JsonReader;
+import org.apache.fory.json.reader.Utf8JsonReader;
+import org.apache.fory.json.writer.StringJsonWriter;
+import org.apache.fory.json.writer.Utf8JsonWriter;
+import org.apache.fory.util.Preconditions;
+
+/** Native-image acceptance coverage for the complete interpreted Fory JSON
path. */
+public final class ForyJsonExample {
+ private ForyJsonExample() {}
+
+ public static void main(String[] args) {
+ testModels();
+ testConfigurations();
+ testCodecs();
+ testSubtypes();
+ testContainerRoots();
+ testGenericProperties();
+ testBigDecimal();
+ testSqlTypes();
+ System.out.println("Fory JSON succeed");
+ }
+
+ private static void testModels() {
+ ForyJson json = ForyJson.builder().build();
+ Model value = new Model();
+ value.child = new Child(1, "first");
+ value.children = List.of(new Child(2, "second"));
+ value.childrenByName = Map.of("third", new Child(3, "third"));
+ value.concreteChildren = new ArrayList<>(List.of(new Child(11,
"concrete")));
+ value.concreteChildrenByName = new HashMap<>();
+ value.concreteChildrenByName.put("map", new Child(12, "concrete-map"));
+ value.childArray = new Child[] {new Child(4, "fourth")};
+ value.status = Status.ACTIVE;
+ value.bean = new Bean("interface");
+ value.record = new DataRecord(5, "record");
+ value.creator = new CreatorValue(6, "creator");
+ value.factory = FactoryValue.create(7, "factory");
+ value.extra.put("dynamic", 8);
+
+ byte[] bytes = json.toJsonBytes(value);
+ Model decoded = json.fromJson(bytes, Model.class);
+ Preconditions.checkArgument(decoded.inheritedId() == 10);
+ Preconditions.checkArgument(decoded.child.id == 1);
+ Preconditions.checkArgument(decoded.children.get(0).name.equals("second"));
+ Preconditions.checkArgument(decoded.childrenByName.get("third").id == 3);
+ Preconditions.checkArgument(decoded.concreteChildren.get(0).id == 11);
+ Preconditions.checkArgument(decoded.concreteChildrenByName.get("map").id
== 12);
+ Preconditions.checkArgument(decoded.childArray[0].name.equals("fourth"));
+ Preconditions.checkArgument(decoded.status == Status.ACTIVE);
+
Preconditions.checkArgument(decoded.bean.getDisplayName().equals("interface"));
+ Preconditions.checkArgument(decoded.record.equals(new DataRecord(5,
"record")));
+ Preconditions.checkArgument(decoded.creator.name.equals("creator"));
+ Preconditions.checkArgument(decoded.factory.id == 7);
+ Preconditions.checkArgument(decoded.extra.containsKey("dynamic"));
+ }
+
+ private static void testConfigurations() {
+ ConfigValue value = new ConfigValue();
+ value.camelName = "configured";
+ String defaults = ForyJson.builder().build().toJson(value);
+ Preconditions.checkArgument(defaults.contains("\"camelName\""));
+ Preconditions.checkArgument(!defaults.contains("nullValue"));
+
+ ForyJson configured =
+ ForyJson.builder()
+ .withFieldMode(true)
+ .writeNullFields(true)
+ .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
+ .build();
+ String snakeCase = configured.toJson(value);
+ Preconditions.checkArgument(snakeCase.contains("\"camel_name\""));
+ Preconditions.checkArgument(snakeCase.contains("\"null_value\":null"));
+ ConfigValue decoded = configured.fromJson(snakeCase, ConfigValue.class);
+ Preconditions.checkArgument(decoded.camelName.equals("configured"));
+ }
+
+ private static void testCodecs() {
+ ForyJson json = ForyJson.builder().build();
+ CodecModel value = new CodecModel();
+ value.direct = new DirectValue("direct");
+ value.inherited = new InheritedValue("inherited");
+ value.nested = List.of(new TypeUseValue("nested"));
+ value.array = new TypeUseValue[] {new TypeUseValue("array")};
+ value.mapped = Map.of("key", new TypeUseValue("mapped"));
+ value.property = new TypeUseValue("property");
+ value.record = new CodecRecord(new TypeUseValue("record"));
+ value.creator = new CodecCreator(new TypeUseValue("creator"));
+ value.factory = CodecFactory.create(new TypeUseValue("factory"));
+
+ String stringJson = json.toJson(value);
+ Preconditions.checkArgument(stringJson.contains("string:direct"));
+ Preconditions.checkArgument(stringJson.contains("string:inherited"));
+ Preconditions.checkArgument(stringJson.contains("string:nested"));
+ Preconditions.checkArgument(stringJson.contains("string:array"));
+ Preconditions.checkArgument(stringJson.contains("string:mapped"));
+ String utf8Json = new String(json.toJsonBytes(value),
StandardCharsets.UTF_8);
+ Preconditions.checkArgument(utf8Json.contains("utf8:direct"));
+ Preconditions.checkArgument(utf8Json.contains("utf8:property"));
+
+ DirectValue stringValue = json.fromJson("\"value\"", DirectValue.class);
+ DirectValue utf16 = json.fromJson("\"\u4f60\"", DirectValue.class);
+ DirectValue utf8 =
+ json.fromJson("\"value\"".getBytes(StandardCharsets.UTF_8),
DirectValue.class);
+ checkStringRead(stringValue.text, "value");
+ Preconditions.checkArgument(utf16.text.equals("utf16:\u4f60"));
+ Preconditions.checkArgument(utf8.text.equals("utf8:value"));
+
+ CodecModel decoded = json.fromJson(stringJson, CodecModel.class);
+ checkStringRead(decoded.direct.text, "string:direct");
+ checkStringRead(decoded.inherited.text, "string:inherited");
+ checkStringRead(decoded.nested.get(0).text, "string:nested");
+ checkStringRead(decoded.array[0].text, "string:array");
+ checkStringRead(decoded.mapped.get("key").text, "string:mapped");
+ checkStringRead(decoded.property.text, "string:property");
+ checkStringRead(decoded.record.value.text, "string:record");
+ checkStringRead(decoded.creator.value.text, "string:creator");
+ checkStringRead(decoded.factory.value.text, "string:factory");
+ }
+
+ private static void checkStringRead(String actual, String value) {
+ Preconditions.checkArgument(actual.equals("latin:" + value) ||
actual.equals("utf16:" + value));
+ }
+
+ private static void testSubtypes() {
+ ForyJson json = ForyJson.builder().build();
+ Shape value = new Circle(9);
+ String encoded = json.toJson(value, Shape.class);
+ Shape decoded = json.fromJson(encoded, Shape.class);
+ Preconditions.checkArgument(decoded instanceof Circle);
+ Preconditions.checkArgument(((Circle) decoded).radius == 9);
+ }
+
+ private static void testContainerRoots() {
+ ForyJson json = ForyJson.builder().build();
+ StringList list = json.fromJson("[\"first\",\"second\"]",
StringList.class);
+ Preconditions.checkArgument(list.equals(List.of("first", "second")));
+ StringMap map = json.fromJson("{\"key\":\"value\"}", StringMap.class);
+ Preconditions.checkArgument(map.equals(Map.of("key", "value")));
+ }
+
+ private static void testGenericProperties() {
+ ForyJson json = ForyJson.builder().build();
+ GenericModel value =
+ json.fromJson("{\"values\":[{\"id\":13,\"name\":\"generic\"}]}",
GenericModel.class);
+ Object values = value.getValues();
+ Preconditions.checkArgument(
+ values.getClass().getName().equals(ForyJsonExample.class.getName() +
"$ChildList"));
+ Child child = (Child) ((List<?>) values).get(0);
+ Preconditions.checkArgument(child.id == 13);
+ Preconditions.checkArgument(child.name.equals("generic"));
+ }
+
+ private static void testBigDecimal() {
+ ForyJson json = ForyJson.builder().build();
+ BigDecimalHolder value = new BigDecimalHolder();
+ value.value = new BigDecimalSubtype("12345678901234567890.125");
+ String expected = "{\"value\":12345678901234567890.125}";
+ Preconditions.checkArgument(json.toJson(value).equals(expected));
+ Preconditions.checkArgument(
+ new String(json.toJsonBytes(value),
StandardCharsets.UTF_8).equals(expected));
+ }
+
+ private static void testSqlTypes() {
+ ForyJson json = ForyJson.builder().build();
+ SqlValues value = new SqlValues();
+ value.date = new Date(1_000L);
+ value.time = new Time(2_000L);
+ value.timestamp = new Timestamp(3_000L);
+ SqlValues decoded = json.fromJson(json.toJsonBytes(value),
SqlValues.class);
+ Preconditions.checkArgument(decoded.date.getTime() == 1_000L);
+ Preconditions.checkArgument(decoded.time.getTime() == 2_000L);
+ Preconditions.checkArgument(decoded.timestamp.getTime() == 3_000L);
+ }
+
+ public static class Parent {
+ private int inheritedId = 10;
+
+ int inheritedId() {
+ return inheritedId;
+ }
+ }
+
+ public static final class StringList extends ArrayList<String> {
+ public StringList() {}
+ }
+
+ public static final class StringMap extends HashMap<String, String> {
+ public StringMap() {}
+ }
+
+ public abstract static class GenericProperty<T> {
+ private Object value;
+
+ @SuppressWarnings("unchecked")
+ public T getValues() {
+ return (T) value;
+ }
+
+ public void setValues(T value) {
+ this.value = value;
+ }
+ }
+
+ @JsonType
+ public static final class GenericModel extends GenericProperty<ChildList> {}
+
+ public static final class ChildList extends ArrayList<Child> {
+ public ChildList() {}
+ }
+
+ @JsonType
+ public static final class BigDecimalHolder {
+ public BigDecimal value;
+ }
+
+ private static final class BigDecimalSubtype extends BigDecimal {
+ private BigDecimalSubtype(String value) {
+ super(value);
+ }
+
+ @Override
+ public String toString() {
+ throw new AssertionError("BigDecimal subtype toString must not be
invoked");
+ }
+
+ @Override
+ public BigInteger unscaledValue() {
+ throw new AssertionError("BigDecimal subtype unscaledValue must not be
invoked");
+ }
+
+ @Override
+ public int scale() {
+ throw new AssertionError("BigDecimal subtype scale must not be invoked");
+ }
+
+ @Override
+ public BigDecimal negate() {
+ throw new AssertionError("BigDecimal subtype negate must not be
invoked");
+ }
+ }
+
+ @JsonType
+ public static final class Model extends Parent {
+ public Child child;
+ public List<Child> children;
+ public Map<String, Child> childrenByName;
+ public ArrayList<Child> concreteChildren;
+ public HashMap<String, Child> concreteChildrenByName;
+ public Child[] childArray;
+ public Status status;
+ public Bean bean;
+ public DataRecord record;
+ public CreatorValue creator;
+ public FactoryValue factory;
+
+ @JsonAnyProperty public Map<String, Object> extra = new LinkedHashMap<>();
+ }
+
+ @JsonType
+ public static final class Child {
+ public int id;
+ public String name;
+
+ public Child() {}
+
+ Child(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ }
+
+ @JsonType
+ public enum Status {
+ ACTIVE,
+ INACTIVE
+ }
+
+ public interface NamedBean {
+ String getDisplayName();
+
+ void setDisplayName(String value);
+ }
+
+ @JsonType
+ public static final class Bean implements NamedBean {
+ private String displayName;
+
+ public Bean() {}
+
+ Bean(String displayName) {
+ this.displayName = displayName;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ public void setDisplayName(String value) {
+ displayName = value;
+ }
+ }
+
+ @JsonType
+ public record DataRecord(int id, String name) {}
+
+ @JsonType
+ public static final class CreatorValue {
+ public final int id;
+ public final String name;
+
+ @JsonCreator({"id", "name"})
+ public CreatorValue(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ }
+
+ @JsonType
+ public static final class FactoryValue {
+ public final int id;
+ public final String name;
+
+ private FactoryValue(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ @JsonCreator({"id", "name"})
+ public static FactoryValue create(int id, String name) {
+ return new FactoryValue(id, name);
+ }
+ }
+
+ @JsonType
+ public static final class ConfigValue {
+ public String camelName;
+ public String nullValue;
+
+ public @JsonCodec(IgnoredCodec.class) TypeUseValue ignoredCodecMethod() {
+ throw new AssertionError("Non-property methods must not participate in
JSON metadata");
+ }
+ }
+
+ @JsonType
+ public static final class CodecModel {
+ public DirectValue direct;
+ public InheritedValue inherited;
+ public List<@JsonCodec(TypeUseCodec.class) TypeUseValue> nested = new
ArrayList<>();
+ public @JsonCodec(TypeUseCodec.class) TypeUseValue[] array;
+ public Map<String, @JsonCodec(TypeUseCodec.class) TypeUseValue> mapped;
+ private TypeUseValue property;
+ public CodecRecord record;
+ public CodecCreator creator;
+ public CodecFactory factory;
+
+ public @JsonCodec(TypeUseCodec.class) TypeUseValue getProperty() {
+ return property;
+ }
+
+ public void setProperty(@JsonCodec(TypeUseCodec.class) TypeUseValue
property) {
+ this.property = property;
+ }
+ }
+
+ @JsonCodec(DirectCodec.class)
+ public static final class DirectValue implements TextValue {
+ private final String text;
+
+ DirectValue(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String text() {
+ return text;
+ }
+ }
+
+ @JsonCodec(InheritedCodec.class)
+ public interface InheritedText extends TextValue {}
+
+ public static final class InheritedValue implements InheritedText {
+ private final String text;
+
+ InheritedValue(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String text() {
+ return text;
+ }
+ }
+
+ public static final class TypeUseValue implements TextValue {
+ private final String text;
+
+ TypeUseValue(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String text() {
+ return text;
+ }
+ }
+
+ @JsonType
+ public record CodecRecord(@JsonCodec(TypeUseCodec.class) TypeUseValue value)
{}
+
+ @JsonType
+ public static final class CodecCreator {
+ public final TypeUseValue value;
+
+ @JsonCreator
+ public CodecCreator(@JsonProperty("value") @JsonCodec(TypeUseCodec.class)
TypeUseValue value) {
+ this.value = value;
+ }
+ }
+
+ @JsonType
+ public static final class CodecFactory {
+ public final TypeUseValue value;
+
+ private CodecFactory(TypeUseValue value) {
+ this.value = value;
+ }
+
+ @JsonCreator
+ public static CodecFactory create(
+ @JsonProperty("value") @JsonCodec(TypeUseCodec.class) TypeUseValue
value) {
+ return new CodecFactory(value);
+ }
+ }
+
+ public interface TextValue {
+ String text();
+ }
+
+ public abstract static class TextCodec<T extends TextValue> implements
JsonValueCodec<T> {
+ public TextCodec() {}
+
+ protected abstract T create(String text);
+
+ @Override
+ public void writeString(StringJsonWriter writer, T value) {
+ writer.writeString(value == null ? null : "string:" + value.text());
+ }
+
+ @Override
+ public void writeUtf8(Utf8JsonWriter writer, T value) {
+ writer.writeString(value == null ? null : "utf8:" + value.text());
+ }
+
+ @Override
+ public T readLatin1(Latin1JsonReader reader) {
+ return reader.tryReadNullToken() ? null : create("latin:" +
reader.readString());
+ }
+
+ @Override
+ public T readUtf16(Utf16JsonReader reader) {
+ return reader.tryReadNullToken() ? null : create("utf16:" +
reader.readString());
+ }
+
+ @Override
+ public T readUtf8(Utf8JsonReader reader) {
+ return reader.tryReadNullToken() ? null : create("utf8:" +
reader.readString());
+ }
+ }
+
+ public static final class DirectCodec extends TextCodec<DirectValue> {
+ public DirectCodec() {}
+
+ @Override
+ protected DirectValue create(String text) {
+ return new DirectValue(text);
+ }
+ }
+
+ public static final class InheritedCodec extends TextCodec<InheritedValue> {
+ public InheritedCodec() {}
+
+ @Override
+ protected InheritedValue create(String text) {
+ return new InheritedValue(text);
+ }
+ }
+
+ public static final class TypeUseCodec extends TextCodec<TypeUseValue> {
+ public TypeUseCodec() {}
+
+ @Override
+ protected TypeUseValue create(String text) {
+ return new TypeUseValue(text);
+ }
+ }
+
+ public static final class IgnoredCodec extends TextCodec<TypeUseValue> {
+ private IgnoredCodec(String ignored) {}
+
+ @Override
+ protected TypeUseValue create(String text) {
+ return new TypeUseValue(text);
+ }
+ }
+
+ @JsonType
+ @JsonSubTypes(
+ property = "kind",
+ value = {@JsonSubTypes.Type(value = Circle.class, name = "circle")})
+ public interface Shape {}
+
+ public static final class Circle implements Shape {
+ public int radius;
+
+ public Circle() {}
+
+ Circle(int radius) {
+ this.radius = radius;
+ }
+ }
+
+ @JsonType
+ public static final class SqlValues {
+ public Date date;
+ public Time time;
+ public Timestamp timestamp;
+ }
+}
diff --git
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
index f84eeb50b..14e290f31 100644
---
a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
+++
b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/Main.java
@@ -46,5 +46,6 @@ public class Main {
ExceptionExample.main(args);
AbstractClassExample.main(args);
FeatureTestExample.main(args);
+ ForyJsonExample.main(args);
}
}
diff --git a/java/README.md b/java/README.md
index f5955a51e..80fdd66ca 100644
--- a/java/README.md
+++ b/java/README.md
@@ -366,7 +366,9 @@ java --add-modules=jdk.incubator.vector ...
### GraalVM Native Image
-Fory supports GraalVM native image through code generation, eliminating the
need for reflection configuration. Build your native image as follows:
+Fory supports GraalVM Native Image without application reflection
configuration. Binary
+serialization generates serializers while the image is built; Fory JSON
registers reachable
+`@JsonType` models and uses its interpreted codec path. Build your native
image as follows:
```bash
# Generate serializers at build time
diff --git
a/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
b/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
index 215a92152..a59641449 100644
--- a/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/util/record/RecordUtils.java
@@ -31,10 +31,12 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.fory.annotation.Internal;
import org.apache.fory.collection.ClassValueCache;
import org.apache.fory.collection.Tuple2;
import org.apache.fory.exception.ForyException;
import org.apache.fory.platform.AndroidSupport;
+import org.apache.fory.platform.GraalvmSupport;
import org.apache.fory.platform.internal._JDKAccess;
/** Utils for java.lang.Record. */
@@ -100,6 +102,12 @@ public class RecordUtils {
private static final ClassValueCache<Tuple2<Constructor, MethodHandle>>
ctrCache =
ClassValueCache.newClassKeyCache(32);
+ // This holder must remain in fory-core's Native Image build-time
initialization list. Its cache
+ // stores getter functions created during analysis because Native Image
cannot create them later.
+ private static final class NativeImageRecordGetters {
+ private static final ClassValueCache<Object[]> CACHE =
ClassValueCache.newClassKeyCache(32);
+ }
+
/**
* Returns {@code true} if and only if this class is a record class.
*
@@ -134,9 +142,26 @@ public class RecordUtils {
if (GET_RECORD_COMPONENTS == null) {
return null;
}
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE &&
GraalvmSupport.isGraalBuildTime()) {
+ // Annotated type values can be JDK annotation proxies, which Native
Image must not persist in
+ // the image heap. Keep only the runtime-required getter functions and
let build-time codegen
+ // consume transient component metadata.
+ prepareRecordComponentGetters(cls);
+ return getRecordComponentsUncached(cls);
+ }
return recordComponentsCache.get(cls, () ->
getRecordComponentsUncached(cls));
}
+ /** Prepares record getter functions during Native Image hosted analysis. */
+ @Internal
+ public static void prepareRecordComponentGetters(Class<?> cls) {
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
+ && GET_RECORD_COMPONENTS != null
+ && !AndroidSupport.IS_ANDROID) {
+ NativeImageRecordGetters.CACHE.get(cls, () ->
buildRecordComponentGetters(cls));
+ }
+ }
+
/** Returns the record canonical constructor. */
public static Tuple2<Constructor, MethodHandle>
getRecordConstructor(Class<?> cls) {
return ctrCache.get(cls, () -> getRecordConstructorUncached(cls));
@@ -152,9 +177,15 @@ public class RecordUtils {
private static RecordComponent[] getRecordComponentsUncached(Class<?> type) {
try {
- MethodHandles.Lookup lookup =
- AndroidSupport.IS_ANDROID ? null : _JDKAccess._trustedLookup(type);
Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(type);
+ Object[] preparedGetters =
+ GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
+ ? NativeImageRecordGetters.CACHE.getIfPresent(type)
+ : null;
+ MethodHandles.Lookup lookup =
+ AndroidSupport.IS_ANDROID || preparedGetters != null
+ ? null
+ : _JDKAccess._trustedLookup(type);
RecordComponent[] recordComponents = new
RecordComponent[components.length];
for (int i = 0; i < components.length; i++) {
Object component = components[i];
@@ -168,8 +199,12 @@ public class RecordUtils {
throw new ForyException("Failed to make record accessor
accessible: " + accessor, e);
}
} else {
- MethodHandle handle = lookup.unreflect(accessor);
- getter = _JDKAccess.makeGetterFunction(lookup, handle, fieldType);
+ if (preparedGetters == null) {
+ MethodHandle handle = lookup.unreflect(accessor);
+ getter = _JDKAccess.makeGetterFunction(lookup, handle, fieldType);
+ } else {
+ getter = preparedGetters[i];
+ }
}
recordComponents[i] =
new RecordComponent(
@@ -187,6 +222,22 @@ public class RecordUtils {
}
}
+ private static Object[] buildRecordComponentGetters(Class<?> type) {
+ try {
+ MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(type);
+ Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(type);
+ Object[] getters = new Object[components.length];
+ for (int i = 0; i < components.length; i++) {
+ Method accessor = (Method) GET_ACCESSOR.invoke(components[i]);
+ Class<?> fieldType = (Class<?>) GET_TYPE.invoke(components[i]);
+ getters[i] = _JDKAccess.makeGetterFunction(lookup,
lookup.unreflect(accessor), fieldType);
+ }
+ return getters;
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
private static Tuple2<Constructor, MethodHandle>
getRecordConstructorUncached(Class<?> type) {
RecordComponent[] components = RecordUtils.getRecordComponents(type);
if (components == null) {
diff --git
a/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
b/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
index ba0edfe15..b9b77d06e 100644
---
a/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
+++
b/java/fory-core/src/main/java17/org/apache/fory/platform/ForyGraalVMFeature.java
@@ -111,6 +111,11 @@ final class ForyGraalVMFeature implements Feature {
if (RecordUtils.isRecord(clazz)) {
RuntimeReflection.registerAllRecordComponents(clazz);
+ // Native Image cannot spin the hidden getter classes used by
RecordUtils at runtime. Build
+ // only those getters here; annotated type proxies must remain runtime
metadata instead of
+ // becoming objects in the image heap.
+ RecordUtils.prepareRecordComponentGetters(clazz);
+ RecordUtils.getRecordConstructor(clazz);
} else if (GraalvmSupport.needReflectionRegisterForCreation(clazz)) {
RuntimeReflection.registerForReflectiveInstantiation(clazz);
}
diff --git
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
index d20a33f48..f5e5f2c88 100644
---
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
+++
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
@@ -542,6 +542,7 @@ Args=--features=org.apache.fory.platform.ForyGraalVMFeature
\
org.apache.fory.util.record.RecordUtils$1,\
org.apache.fory.util.record.RecordUtils$2,\
org.apache.fory.util.record.RecordUtils$3,\
+ org.apache.fory.util.record.RecordUtils$NativeImageRecordGetters,\
org.apache.fory.util.record.RecordUtils,\
org.apache.fory.serializer.PlatformStringUtils$StringCoderField,\
org.apache.fory.platform.internal._JDKAccess,\
diff --git
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
b/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
index 5683ad4c0..baad6d1d2 100644
---
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
+++
b/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
@@ -52,6 +52,8 @@ public final class GraalvmFeatureJarVerifier {
"META-INF/services/org.graalvm.nativeimage.hosted.Feature";
private static final String FEATURE_OPTION = "--features=" +
FEATURE_CLASS_NAME;
private static final String INITIALIZATION_OPTION =
"--initialize-at-build-time=";
+ private static final String RECORD_GETTERS =
+ "org.apache.fory.util.record.RecordUtils$NativeImageRecordGetters";
private GraalvmFeatureJarVerifier() {}
@@ -92,6 +94,9 @@ public final class GraalvmFeatureJarVerifier {
check(
properties.contains(INITIALIZATION_OPTION),
"Build-time initialization option is missing");
+ check(
+ properties.contains(RECORD_GETTERS),
+ "Native Image record getter cache must initialize at build time");
}
}
diff --git a/java/fory-json/README.md b/java/fory-json/README.md
index 9f19704be..4874abf8d 100644
--- a/java/fory-json/README.md
+++ b/java/fory-json/README.md
@@ -356,11 +356,18 @@ operation. Apply request/body size limits at the
transport boundary when parsing
Builder mutation after `build()` does not modify an existing `ForyJson`
runtime.
+In a GraalVM native image, runtime code generation and asynchronous
compilation are automatically
+disabled. Every other builder option keeps the behavior described above.
+
## JSON annotations
-Fory JSON defines nine annotations in `org.apache.fory.json.annotation`,
including `JsonCodec` for
-complete-value codec selection. They are Fory JSON APIs, not Jackson, Gson, or
Fory binary-protocol
-compatibility annotations.
+Fory JSON defines ten annotations in `org.apache.fory.json.annotation`,
including `JsonCodec` for
+complete-value codec selection and `JsonType` for GraalVM Native Image model
metadata. They are Fory
+JSON APIs, not Jackson, Gson, or Fory binary-protocol compatibility
annotations.
+
+`JsonType` has no effect on ordinary JVM JSON behavior and is not inherited.
Add it to every
+reachable object model used by a native executable. See the
+[GraalVM guide](../../docs/guide/java/graalvm-support.md) for the complete
workflow.
### `JsonProperty`
@@ -745,7 +752,8 @@ is not inherited from another annotated interface or
abstract class. Readers acc
configured inclusion; changing inclusion is a wire-format change and there is
no dual-read
fallback.
-At GraalVM native-image runtime, use class-literal entries rather than
`className` entries.
+At GraalVM native-image runtime, annotate the base with `JsonType` and use
class-literal entries
+rather than `className` entries. Listed class-literal subtypes are registered
automatically.
## Custom codecs
@@ -1070,11 +1078,11 @@ plain `Base` fails with `ForyJsonException` containing
the target type, codec cl
and actual returned class. Fory validates the actual result rather than
rejecting inheritance based
on the codec's generic signature.
-`@JsonCodec` declaration and type-use discovery is supported on ordinary JVMs.
Android and GraalVM
-native image ignore all `@JsonCodec` sources so that declaration defaults
cannot work while nested
-type-use metadata silently disappears. Use exact `registerCodec(Target.class,
instance)`
-registration for both environments. Native-image reflection configuration is
not a supported way
-to enable only part of the annotation feature.
+`@JsonCodec` declaration and type-use discovery is supported on ordinary JVMs
and GraalVM native
+images, including inherited declarations and nested type uses. Native object
models must follow the
+`JsonType` workflow in the [GraalVM
guide](../../docs/guide/java/graalvm-support.md), which registers
+the annotation codec's public no-argument constructor. Android ignores
annotation codec sources;
+use exact `registerCodec(Target.class, instance)` registration there.
## Type validation and untrusted input
diff --git a/java/fory-json/pom.xml b/java/fory-json/pom.xml
index 30c1c9e70..69dac5e7e 100644
--- a/java/fory-json/pom.xml
+++ b/java/fory-json/pom.xml
@@ -38,6 +38,7 @@
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
+ <maven.source.skip>false</maven.source.skip>
<fory.java.rootdir>${basedir}/..</fory.java.rootdir>
<fory.json.mr.classes>${project.build.directory}/multi-release-classes</fory.json.mr.classes>
<fory.json.jdk25.test.classes>${project.build.directory}/jdk25-test-classes</fory.json.jdk25.test.classes>
@@ -86,6 +87,20 @@
<activation>
<jdk>[9,)</jdk>
</activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.graalvm.sdk</groupId>
+ <artifactId>graal-sdk</artifactId>
+ <version>23.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.graalvm.sdk</groupId>
+ <artifactId>nativeimage</artifactId>
+ <version>23.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
<build>
<plugins>
<plugin>
@@ -162,9 +177,9 @@
</build>
</profile>
<profile>
- <id>jdk25-multi-release</id>
+ <id>graalvm-feature-java17</id>
<activation>
- <jdk>[25,)</jdk>
+ <jdk>[17,)</jdk>
</activation>
<build>
<plugins>
@@ -181,6 +196,127 @@
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>3.1.0</version>
+ <executions>
+ <execution>
+ <id>compile-java17-feature</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <delete dir="${fory.json.mr.classes}/17"/>
+ <mkdir dir="${fory.json.mr.classes}/17"/>
+ <javac srcdir="${project.basedir}/src/main/java17"
+ destdir="${fory.json.mr.classes}/17"
+ includeantruntime="false"
+ fork="true"
+ executable="${java.home}/bin/javac"
+ debug="true">
+ <include name="org/**/*.java"/>
+ <classpath>
+ <path refid="maven.compile.classpath"/>
+ <pathelement
location="${project.build.outputDirectory}"/>
+ </classpath>
+ <compilerarg value="--release"/>
+ <compilerarg value="17"/>
+ <compilerarg value="-sourcepath"/>
+ <compilerarg value=""/>
+ </javac>
+ </target>
+ </configuration>
+ </execution>
+ <execution>
+ <id>inject-java17-feature</id>
+ <phase>package</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <jar
destfile="${project.build.directory}/${project.build.finalName}.jar"
+ update="true">
+ <zipfileset dir="${fory.json.mr.classes}/17"
+ prefix="META-INF/versions/17"
+ includes="**/*.class"/>
+ </jar>
+ </target>
+ </configuration>
+ </execution>
+ <execution>
+ <id>patch-multi-release-source-jar</id>
+ <phase>package</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <skip>${maven.source.skip}</skip>
+ <target>
+ <property
+ name="multi.release.sources.jar"
+
value="${project.build.directory}/${project.build.finalName}-sources.jar"/>
+ <available
+ file="${multi.release.sources.jar}"
+ property="multi.release.sources.jar.present"/>
+ <fail
+ unless="multi.release.sources.jar.present"
+ message="Multi-release source patching requires
${multi.release.sources.jar}."/>
+ <jar destfile="${multi.release.sources.jar}" update="true">
+ <zipfileset dir="${project.basedir}/src/main/java9"
+ prefix="META-INF/versions/9">
+ <include name="**/*.java"/>
+ </zipfileset>
+ <zipfileset dir="${project.basedir}/src/main/java17"
+ prefix="META-INF/versions/17">
+ <include name="**/*.java"/>
+ </zipfileset>
+ <zipfileset dir="${project.basedir}/src/main/java25"
+ prefix="META-INF/versions/25">
+ <include name="**/*.java"/>
+ </zipfileset>
+ </jar>
+ </target>
+ </configuration>
+ </execution>
+ <execution>
+ <id>verify-graalvm-feature-jar</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <skip>${maven.source.skip}</skip>
+ <target>
+ <java
+
classname="org.apache.fory.json.ForyJsonGraalVMFeatureJarVerifier"
+ fork="true"
+ failonerror="true">
+ <classpath>
+ <pathelement
location="${project.build.testOutputDirectory}"/>
+ <path refid="maven.test.classpath"/>
+ </classpath>
+ <arg
value="${project.build.directory}/${project.build.finalName}.jar"/>
+ <arg
value="${project.build.directory}/${project.build.finalName}-sources.jar"/>
+ </java>
+ </target>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>jdk25-multi-release</id>
+ <activation>
+ <jdk>[25,)</jdk>
+ </activation>
+ <build>
+ <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
@@ -294,30 +430,6 @@
</target>
</configuration>
</execution>
- <execution>
- <id>patch-jdk25-source-jar</id>
- <phase>package</phase>
- <goals>
- <goal>run</goal>
- </goals>
- <configuration>
- <target>
- <property
- name="jdk25.sources.jar"
-
value="${project.build.directory}/${project.build.finalName}-sources.jar"/>
- <available file="${jdk25.sources.jar}"
property="jdk25.sources.jar.present"/>
- <fail
- unless="jdk25.sources.jar.present"
- message="Multi-release source patching requires
${jdk25.sources.jar}."/>
- <jar destfile="${jdk25.sources.jar}" update="true">
- <zipfileset dir="${project.basedir}/src/main/java25"
- prefix="META-INF/versions/25">
- <include name="**/*.java"/>
- </zipfileset>
- </jar>
- </target>
- </configuration>
- </execution>
</executions>
</plugin>
<plugin>
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
index 6661c4686..64b3e446a 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java
@@ -22,6 +22,7 @@ package org.apache.fory.json;
import java.util.Objects;
import org.apache.fory.json.codec.JsonValueCodec;
import org.apache.fory.json.resolver.CodecRegistry;
+import org.apache.fory.platform.GraalvmSupport;
/**
* Configures and creates an immutable, thread-safe {@link ForyJson} facade.
@@ -66,13 +67,19 @@ public final class ForyJsonBuilder {
return this;
}
- /** Enables generated object codecs for supported classes. Enabled by
default. */
+ /**
+ * Enables generated object codecs for supported classes. Enabled by default
and automatically
+ * disabled in a GraalVM native image.
+ */
public ForyJsonBuilder withCodegen(boolean codegenEnabled) {
this.codegenEnabled = codegenEnabled;
return this;
}
- /** Enables asynchronous runtime compilation for generated object codecs.
Enabled by default. */
+ /**
+ * Enables asynchronous runtime compilation for generated object codecs.
Enabled by default and
+ * automatically disabled when code generation is unavailable.
+ */
public ForyJsonBuilder withAsyncCompilation(boolean asyncCompilationEnabled)
{
this.asyncCompilationEnabled = asyncCompilationEnabled;
return this;
@@ -180,11 +187,13 @@ public final class ForyJsonBuilder {
fixedClassLoader = ForyJson.class.getClassLoader();
}
}
+ boolean effectiveCodegen = codegenEnabled &&
!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+ boolean effectiveAsyncCompilation = asyncCompilationEnabled &&
effectiveCodegen;
return new ForyJson(
new JsonConfig(
writeNullFields,
- codegenEnabled,
- asyncCompilationEnabled,
+ effectiveCodegen,
+ effectiveAsyncCompilation,
propertyDiscoveryEnabled,
propertyNamingStrategy,
fixedClassLoader,
diff --git a/integration_tests/graalvm_tests/src/main/java/module-info.java
b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
similarity index 52%
copy from integration_tests/graalvm_tests/src/main/java/module-info.java
copy to
java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
index d3e59bb81..dc9f40867 100644
--- a/integration_tests/graalvm_tests/src/main/java/module-info.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java
@@ -17,14 +17,22 @@
* under the License.
*/
-module org.apache.fory.graalvm.tests {
- requires org.apache.fory.core;
+package org.apache.fory.json.annotation;
- // Fory-generated codecs are defined by the existing codegen class loader,
so they access the
- // registered test models from its unnamed module.
- exports org.apache.fory.graalvm;
- exports org.apache.fory.graalvm.record;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
- opens org.apache.fory.graalvm.record to
- org.apache.fory.core;
-}
+/**
+ * Marks a reachable JSON model for reflection metadata registration in a
GraalVM native image.
+ *
+ * <p>The annotation does not change JSON behavior on the JVM and is
intentionally not inherited.
+ * Annotate every runtime model type that Fory JSON reads or writes. A
class-literal subtype listed
+ * by an annotated {@link JsonSubTypes} base is registered automatically.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JsonType {}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
index e0019e15a..60e8cd1d6 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodecBuilder.java
@@ -661,6 +661,37 @@ final class ObjectCodecBuilder {
return anySetter;
}
+ // Native Image hosted discovery must stay aligned with this builder without
adding duplicate
+ // property-name parsing or allocation to ordinary JVM metadata construction.
+ static boolean usesJsonMetadata(Method method, boolean record) {
+ // javac copies runtime annotations to generic bridge methods. Those
generated methods do not
+ // own JSON declarations and processing them would reject an otherwise
valid concrete method.
+ if (method.isSynthetic() || method.isBridge()) {
+ return false;
+ }
+ if (method.getDeclaringClass().isInterface()
+ && method.isAnnotationPresent(JsonProperty.class)) {
+ return true;
+ }
+ if (method.isAnnotationPresent(JsonAnyGetter.class)
+ || method.isAnnotationPresent(JsonAnySetter.class)) {
+ return true;
+ }
+ return !record
+ && isEligibleAccessor(method)
+ && (usesJsonReturn(method) || usesJsonParameters(method));
+ }
+
+ static boolean usesJsonReturn(Method method) {
+ // Java rejects type-use annotations on void, and setter returns are not
JSON value owners.
+ // Keep return and parameter roles separate so hosted metadata follows the
same ownership.
+ return method.isAnnotationPresent(JsonAnyGetter.class) ||
getterPropertyName(method) != null;
+ }
+
+ static boolean usesJsonParameters(Method method) {
+ return method.isAnnotationPresent(JsonAnySetter.class) ||
setterPropertyName(method) != null;
+ }
+
private static FieldBuilder findAnyBuilder(
Class<?> type, LinkedHashMap<String, FieldBuilder> builders) {
FieldBuilder anyBuilder = null;
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
index 886350562..2e179a5b6 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonTypeUse.java
@@ -39,7 +39,6 @@ import org.apache.fory.json.ForyJsonException;
import org.apache.fory.json.annotation.JsonCodec;
import org.apache.fory.json.codec.JsonValueCodec;
import org.apache.fory.platform.AndroidSupport;
-import org.apache.fory.platform.GraalvmSupport;
import org.apache.fory.reflect.TypeRef;
import org.apache.fory.reflect.TypeUseMetadata;
import org.apache.fory.type.TypeUtils;
@@ -56,8 +55,7 @@ import org.apache.fory.util.record.RecordComponent;
public final class JsonTypeUse {
private static final JsonTypeUse[] NO_CHILDREN = new JsonTypeUse[0];
private static final String[] NO_SOURCES = new String[0];
- private static final boolean ANNOTATIONS_SUPPORTED =
- !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+ private static final boolean ANNOTATIONS_SUPPORTED =
!AndroidSupport.IS_ANDROID;
private final Type type;
private final Class<?> rawType;
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
index 5256c236b..bd47fa5c4 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonSharedRegistry.java
@@ -129,8 +129,7 @@ import org.apache.fory.type.TypeUtils;
*/
public final class JsonSharedRegistry {
private static final int TYPE_CHECK_CACHE_LIMIT = 8192;
- private static final boolean CODEC_ANNOTATIONS_ENABLED =
- !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE;
+ private static final boolean CODEC_ANNOTATIONS_ENABLED =
!AndroidSupport.IS_ANDROID;
private static final Comparator<DeclarationCandidate> DECLARATION_ORDER =
new Comparator<DeclarationCandidate>() {
@Override
@@ -655,10 +654,9 @@ public final class JsonSharedRegistry {
hasStringEntry = true;
}
}
- if (hasStringEntry && GraalvmSupport.isGraalRuntime()) {
+ if (hasStringEntry && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
throw new ForyJsonException(
- "GraalVM native image requires build-time Fory codegen for
@JsonSubTypes className entries on "
- + baseType.getName());
+ "GraalVM native image requires @JsonSubTypes class literals on " +
baseType.getName());
}
for (String className : classNames) {
if (className != null) {
diff --git
a/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java
b/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java
new file mode 100644
index 000000000..ab169a761
--- /dev/null
+++
b/java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java
@@ -0,0 +1,384 @@
+/*
+ * 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.json.codec;
+
+import java.lang.reflect.AnnotatedArrayType;
+import java.lang.reflect.AnnotatedParameterizedType;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.AnnotatedTypeVariable;
+import java.lang.reflect.AnnotatedWildcardType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.RecordComponent;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.JsonCodec;
+import org.apache.fory.json.annotation.JsonCreator;
+import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
+import org.apache.fory.platform.GraalvmSupport;
+import org.apache.fory.reflect.TypeRef;
+import org.graalvm.nativeimage.hosted.Feature;
+import org.graalvm.nativeimage.hosted.RuntimeReflection;
+
+/** Registers reachable Fory JSON models for GraalVM native image reflection.
*/
+final class ForyJsonGraalVMFeature implements Feature {
+ private static final String[] SQL_TYPES = {
+ "java.sql.Date", "java.sql.Time", "java.sql.Timestamp"
+ };
+
+ private final Set<Class<?>> reachableTypes = ConcurrentHashMap.newKeySet();
+ private final Set<Class<?>> processedReachableTypes =
ConcurrentHashMap.newKeySet();
+ private final Set<Class<?>> processedDeclarations =
ConcurrentHashMap.newKeySet();
+ private final Set<Class<?>> processedModels = ConcurrentHashMap.newKeySet();
+ private final Set<Class<?>> processedCodecs = ConcurrentHashMap.newKeySet();
+ private final Set<Class<?>> processedContainers =
ConcurrentHashMap.newKeySet();
+
+ @Override
+ public String getDescription() {
+ return "Registers reachable Fory JSON models for GraalVM native image";
+ }
+
+ @Override
+ public void beforeAnalysis(BeforeAnalysisAccess access) {
+ access.registerSubtypeReachabilityHandler(this::processReachableType,
Object.class);
+ }
+
+ private void processReachableType(DuringAnalysisAccess ignored, Class<?>
type) {
+ reachableTypes.add(type);
+ }
+
+ @Override
+ public void duringAnalysis(DuringAnalysisAccess access) {
+ if (!reachableTypes.contains(ForyJson.class)) {
+ return;
+ }
+ boolean changed = false;
+ for (Class<?> type : reachableTypes) {
+ if (processedReachableTypes.add(type)) {
+ changed |= registerContainer(type);
+ changed |= registerDeclarations(type);
+ if (type.getDeclaredAnnotation(JsonType.class) != null) {
+ changed |= registerModel(access, type);
+ }
+ if (type == ForyJson.class) {
+ registerBuiltInTypes(access);
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ access.requireAnalysisIteration();
+ }
+ }
+
+ private boolean registerModel(DuringAnalysisAccess access, Class<?> type) {
+ if (!processedModels.add(type)) {
+ return false;
+ }
+ GraalvmSupport.registerClass(type);
+ registerContainer(type);
+ registerDeclarations(type);
+ if (!type.isEnum()
+ && !Collection.class.isAssignableFrom(type)
+ && !Map.class.isAssignableFrom(type)) {
+ registerModelHierarchy(access, type);
+ }
+ registerSubtypes(access, type);
+ return true;
+ }
+
+ private void registerModelHierarchy(BeforeAnalysisAccess access, Class<?>
type) {
+ TypeRef<?> ownerType = TypeRef.of(type);
+ boolean record = type.isRecord();
+ for (Class<?> current = type;
+ current != null && current != Object.class;
+ current = current.getSuperclass()) {
+ for (Field field : current.getDeclaredFields()) {
+ if (isJsonField(field)) {
+ if (!current.isRecord() && Runtime.version().feature() <= 24) {
+ access.registerAsUnsafeAccessed(field);
+ }
+ registerAnnotatedType(field.getAnnotatedType());
+
registerResolvedType(ownerType.resolveType(field.getGenericType()).getType());
+ }
+ }
+ }
+ for (Method method : type.getMethods()) {
+ if (ObjectCodecBuilder.usesJsonMetadata(method, record)) {
+ if (method.getDeclaringClass().isInterface()) {
+ RuntimeReflection.register(method);
+ }
+ if (ObjectCodecBuilder.usesJsonReturn(method)) {
+ registerAnnotatedType(method.getAnnotatedReturnType());
+
registerResolvedType(ownerType.resolveType(method.getGenericReturnType()).getType());
+ }
+ if (ObjectCodecBuilder.usesJsonParameters(method)) {
+ registerParameterTypes(method.getParameters());
+ registerResolvedParameterTypes(ownerType, method.getParameters());
+ }
+ }
+ }
+ for (Constructor<?> constructor : type.getDeclaredConstructors()) {
+ if (constructor.isAnnotationPresent(JsonCreator.class)) {
+ registerParameterTypes(constructor.getParameters());
+ registerResolvedParameterTypes(ownerType, constructor.getParameters());
+ }
+ }
+ for (Method method : type.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(JsonCreator.class)) {
+ registerParameterTypes(method.getParameters());
+ registerResolvedParameterTypes(ownerType, method.getParameters());
+ }
+ }
+ if (record) {
+ for (RecordComponent component : type.getRecordComponents()) {
+ registerAnnotatedType(component.getAnnotatedType());
+
registerResolvedType(ownerType.resolveType(component.getGenericType()).getType());
+ }
+ }
+ }
+
+ private boolean registerDeclarations(Class<?> type) {
+ if (type == null || type == Object.class ||
!processedDeclarations.add(type)) {
+ return false;
+ }
+ boolean changed = false;
+ JsonCodec annotation = type.getDeclaredAnnotation(JsonCodec.class);
+ if (annotation != null) {
+ RuntimeReflection.register(type);
+ registerCodec(annotation.value());
+ changed = true;
+ }
+ changed |= registerDeclarations(type.getSuperclass());
+ for (Class<?> interfaceType : type.getInterfaces()) {
+ changed |= registerDeclarations(interfaceType);
+ }
+ return changed;
+ }
+
+ private void registerParameterTypes(Parameter[] parameters) {
+ for (Parameter parameter : parameters) {
+ registerAnnotatedType(parameter.getAnnotatedType());
+ }
+ }
+
+ private void registerResolvedParameterTypes(TypeRef<?> ownerType,
Parameter[] parameters) {
+ for (Parameter parameter : parameters) {
+
registerResolvedType(ownerType.resolveType(parameter.getParameterizedType()).getType());
+ }
+ }
+
+ private void registerResolvedType(Type type) {
+ Set<TypeVariable<?>> visiting = Collections.newSetFromMap(new
IdentityHashMap<>());
+ registerResolvedType(type, visiting);
+ }
+
+ private void registerResolvedType(Type type, Set<TypeVariable<?>> visiting) {
+ if (type == null) {
+ return;
+ }
+ registerContainer(type);
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ registerResolvedType(parameterizedType.getOwnerType(), visiting);
+ for (Type argument : parameterizedType.getActualTypeArguments()) {
+ registerResolvedType(argument, visiting);
+ }
+ } else if (type instanceof GenericArrayType) {
+ registerResolvedType(((GenericArrayType)
type).getGenericComponentType(), visiting);
+ } else if (type instanceof WildcardType) {
+ WildcardType wildcardType = (WildcardType) type;
+ registerResolvedTypes(wildcardType.getUpperBounds(), visiting);
+ registerResolvedTypes(wildcardType.getLowerBounds(), visiting);
+ } else if (type instanceof TypeVariable<?>) {
+ TypeVariable<?> variable = (TypeVariable<?>) type;
+ if (visiting.add(variable)) {
+ registerResolvedTypes(variable.getBounds(), visiting);
+ visiting.remove(variable);
+ }
+ }
+ }
+
+ private void registerResolvedTypes(Type[] types, Set<TypeVariable<?>>
visiting) {
+ for (Type type : types) {
+ registerResolvedType(type, visiting);
+ }
+ }
+
+ private void registerAnnotatedType(AnnotatedType type) {
+ Set<TypeVariable<?>> visiting = Collections.newSetFromMap(new
IdentityHashMap<>());
+ registerAnnotatedType(type, visiting);
+ }
+
+ private void registerAnnotatedType(AnnotatedType type, Set<TypeVariable<?>>
visiting) {
+ if (type == null) {
+ return;
+ }
+ registerContainer(type.getType());
+ JsonCodec annotation = type.getDeclaredAnnotation(JsonCodec.class);
+ if (annotation != null) {
+ registerCodec(annotation.value());
+ }
+ if (type instanceof AnnotatedParameterizedType) {
+ AnnotatedParameterizedType parameterizedType =
(AnnotatedParameterizedType) type;
+ registerAnnotatedType(parameterizedType.getAnnotatedOwnerType(),
visiting);
+ for (AnnotatedType argument :
parameterizedType.getAnnotatedActualTypeArguments()) {
+ registerAnnotatedType(argument, visiting);
+ }
+ } else if (type instanceof AnnotatedArrayType) {
+ registerAnnotatedType(
+ ((AnnotatedArrayType) type).getAnnotatedGenericComponentType(),
visiting);
+ } else if (type instanceof AnnotatedWildcardType) {
+ AnnotatedWildcardType wildcardType = (AnnotatedWildcardType) type;
+ registerAnnotatedTypes(wildcardType.getAnnotatedUpperBounds(), visiting);
+ registerAnnotatedTypes(wildcardType.getAnnotatedLowerBounds(), visiting);
+ } else if (type instanceof AnnotatedTypeVariable) {
+ TypeVariable<?> variable = (TypeVariable<?>) type.getType();
+ if (visiting.add(variable)) {
+ registerAnnotatedTypes(((AnnotatedTypeVariable)
type).getAnnotatedBounds(), visiting);
+ visiting.remove(variable);
+ }
+ }
+ }
+
+ private void registerAnnotatedTypes(AnnotatedType[] types,
Set<TypeVariable<?>> visiting) {
+ for (AnnotatedType type : types) {
+ registerAnnotatedType(type, visiting);
+ }
+ }
+
+ private void registerCodec(Class<? extends JsonValueCodec<?>> codecClass) {
+ if (!processedCodecs.add(codecClass)) {
+ return;
+ }
+ RuntimeReflection.register(codecClass);
+ try {
+ RuntimeReflection.register(codecClass.getConstructor());
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException(
+ "@JsonCodec class must have a public no-argument constructor: " +
codecClass.getName(),
+ e);
+ }
+ }
+
+ private boolean registerContainer(Type type) {
+ Class<?> rawType = null;
+ if (type instanceof Class<?>) {
+ rawType = (Class<?>) type;
+ } else if (type instanceof ParameterizedType) {
+ Type parameterizedRawType = ((ParameterizedType) type).getRawType();
+ if (parameterizedRawType instanceof Class<?>) {
+ rawType = (Class<?>) parameterizedRawType;
+ }
+ }
+ if (rawType == null
+ || rawType.isInterface()
+ || Modifier.isAbstract(rawType.getModifiers())
+ || (!Collection.class.isAssignableFrom(rawType) &&
!Map.class.isAssignableFrom(rawType))
+ || !processedContainers.add(rawType)) {
+ return false;
+ }
+ try {
+ RuntimeReflection.register(rawType.getConstructor());
+ return true;
+ } catch (NoSuchMethodException ignored) {
+ // CollectionCodec and MapCodec preserve the same runtime failure for a
concrete container
+ // without a public no-argument constructor.
+ return false;
+ }
+ }
+
+ private void registerSubtypes(DuringAnalysisAccess access, Class<?> type) {
+ JsonSubTypes annotation = type.getDeclaredAnnotation(JsonSubTypes.class);
+ if (annotation == null) {
+ return;
+ }
+ for (JsonSubTypes.Type entry : annotation.value()) {
+ Class<?> subtype = entry.value();
+ if (subtype != Void.class) {
+ registerModel(access, subtype);
+ }
+ }
+ }
+
+ private void registerSqlTypes(DuringAnalysisAccess access) {
+ for (String className : SQL_TYPES) {
+ Class<?> type = access.findClassByName(className);
+ if (type != null) {
+ RuntimeReflection.register(type);
+ try {
+ RuntimeReflection.register(type.getConstructor(long.class));
+ } catch (NoSuchMethodException e) {
+ throw new IllegalStateException("Missing Fory JSON SQL constructor
for " + className, e);
+ }
+ }
+ }
+ }
+
+ private void registerBuiltInTypes(DuringAnalysisAccess access) {
+ registerSqlTypes(access);
+ registerBigDecimalFields(access);
+ }
+
+ private void registerBigDecimalFields(BeforeAnalysisAccess access) {
+ registerBigDecimalField(access, "intCompact", long.class);
+ registerBigDecimalField(access, "intVal", BigInteger.class);
+ registerBigDecimalField(access, "scale", int.class);
+ }
+
+ private void registerBigDecimalField(
+ BeforeAnalysisAccess access, String fieldName, Class<?> fieldType) {
+ try {
+ Field field = BigDecimal.class.getDeclaredField(fieldName);
+ if (field.getType() == fieldType) {
+ RuntimeReflection.register(field);
+ if (Runtime.version().feature() <= 24) {
+ access.registerAsUnsafeAccessed(field);
+ }
+ }
+ } catch (NoSuchFieldException ignored) {
+ // BigDecimalFields preserves its public-JDK fallback if a future JDK
changes this layout.
+ }
+ }
+
+ private static boolean isJsonField(Field field) {
+ int modifiers = field.getModifiers();
+ return !Modifier.isStatic(modifiers)
+ && !Modifier.isTransient(modifiers)
+ && field.getType() != Class.class
+ && !field.isSynthetic();
+ }
+}
diff --git a/java/fory-json/src/main/java9/module-info.java
b/java/fory-json/src/main/java9/module-info.java
index b93aeb3f9..f5a779a25 100644
--- a/java/fory-json/src/main/java9/module-info.java
+++ b/java/fory-json/src/main/java9/module-info.java
@@ -21,6 +21,8 @@ module org.apache.fory.json {
requires org.apache.fory.core;
requires static java.sql;
requires static com.google.common;
+ requires static org.graalvm.nativeimage;
+ requires static org.graalvm.sdk;
exports org.apache.fory.json;
exports org.apache.fory.json.annotation;
diff --git
a/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
new file mode 100644
index 000000000..b3943fab7
--- /dev/null
+++
b/java/fory-json/src/main/resources/META-INF/native-image/org.apache.fory/fory-json/native-image.properties
@@ -0,0 +1,18 @@
+# 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.
+
+Args=--features=org.apache.fory.json.codec.ForyJsonGraalVMFeature
diff --git
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
similarity index 82%
copy from
java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
copy to
java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
index 5683ad4c0..dc74e0e05 100644
---
a/java/fory-core/src/test/java/org/apache/fory/builder/GraalvmFeatureJarVerifier.java
+++
b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.fory.builder;
+package org.apache.fory.json;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -35,30 +35,30 @@ import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
-/** Verifies the packaged GraalVM Feature and its Native Image activation
metadata. */
-public final class GraalvmFeatureJarVerifier {
- private static final String FEATURE_CLASS_NAME =
"org.apache.fory.platform.ForyGraalVMFeature";
+/** Verifies the packaged Fory JSON GraalVM Feature and activation metadata. */
+public final class ForyJsonGraalVMFeatureJarVerifier {
+ private static final String FEATURE_CLASS_NAME =
+ "org.apache.fory.json.codec.ForyJsonGraalVMFeature";
private static final String FEATURE_CLASS_FILE =
- "org/apache/fory/platform/ForyGraalVMFeature.class";
+ "org/apache/fory/json/codec/ForyJsonGraalVMFeature.class";
private static final String VERSION_17_FEATURE_CLASS =
"META-INF/versions/17/" + FEATURE_CLASS_FILE;
private static final String FEATURE_SOURCE_FILE =
- "org/apache/fory/platform/ForyGraalVMFeature.java";
+ "org/apache/fory/json/codec/ForyJsonGraalVMFeature.java";
private static final String VERSION_17_FEATURE_SOURCE =
"META-INF/versions/17/" + FEATURE_SOURCE_FILE;
private static final String NATIVE_IMAGE_PROPERTIES =
-
"META-INF/native-image/org.apache.fory/fory-core/native-image.properties";
+
"META-INF/native-image/org.apache.fory/fory-json/native-image.properties";
private static final String FEATURE_SERVICE =
"META-INF/services/org.graalvm.nativeimage.hosted.Feature";
private static final String FEATURE_OPTION = "--features=" +
FEATURE_CLASS_NAME;
- private static final String INITIALIZATION_OPTION =
"--initialize-at-build-time=";
- private GraalvmFeatureJarVerifier() {}
+ private ForyJsonGraalVMFeatureJarVerifier() {}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new IllegalArgumentException(
- "Usage: GraalvmFeatureJarVerifier <fory-core.jar>
<fory-core-sources.jar>");
+ "Usage: ForyJsonGraalVMFeatureJarVerifier <fory-json.jar>
<sources.jar>");
}
Path jarPath = Paths.get(args[0]);
verifyBinaryJar(jarPath);
@@ -82,16 +82,10 @@ public final class GraalvmFeatureJarVerifier {
check(countEntries(jarFile, FEATURE_SERVICE) == 0, "Feature service file
must not exist");
check(
countEntries(jarFile, NATIVE_IMAGE_PROPERTIES) == 1,
- "Expected exactly one core native-image.properties");
-
+ "Expected exactly one JSON native-image.properties");
String properties = readEntry(jarFile, NATIVE_IMAGE_PROPERTIES);
- check(
- countOccurrences(properties, "--features=") == 1,
- "Expected exactly one --features option");
- check(properties.contains(FEATURE_OPTION), "Core Feature option is
missing");
- check(
- properties.contains(INITIALIZATION_OPTION),
- "Build-time initialization option is missing");
+ check(countOccurrences(properties, "--features=") == 1, "Expected one
--features option");
+ check(properties.contains(FEATURE_OPTION), "Fory JSON Feature option is
missing");
}
}
@@ -110,10 +104,9 @@ public final class GraalvmFeatureJarVerifier {
private static void verifyFeatureLoading(Path jarPath) throws Exception {
URL[] urls = {jarPath.toUri().toURL()};
try (URLClassLoader classLoader =
- new URLClassLoader(urls,
GraalvmFeatureJarVerifier.class.getClassLoader())) {
+ new URLClassLoader(urls,
ForyJsonGraalVMFeatureJarVerifier.class.getClassLoader())) {
Class<?> featureClass = Class.forName(FEATURE_CLASS_NAME, true,
classLoader);
- check(
- featureClass.getClassLoader() == classLoader, "Feature was not
loaded from packaged jar");
+ check(featureClass.getClassLoader() == classLoader, "Feature was not
loaded from the jar");
check(!Modifier.isPublic(featureClass.getModifiers()), "Feature must
remain non-public");
Constructor<?> constructor = featureClass.getDeclaredConstructor();
constructor.setAccessible(true);
@@ -122,7 +115,7 @@ public final class GraalvmFeatureJarVerifier {
getDescription.setAccessible(true);
Object description = getDescription.invoke(feature);
check(
- description instanceof String && ((String)
description).contains("Fory"),
+ description instanceof String && ((String)
description).contains("Fory JSON"),
"Packaged Feature returned an invalid description");
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]