From 04d5e27f134f9f5fc571143371bcacd861c9d528 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 2 Aug 2026 00:59:07 +0800 Subject: [PATCH 01/15] chore(json): checkpoint GraalVM hosted codegen work --- ci/run_ci.sh | 22 +- docs/guide/java/graalvm-support.md | 116 ++-- docs/guide/java/json-support.md | 29 +- integration_tests/graalvm_tests/README.md | 18 +- integration_tests/graalvm_tests/pom.xml | 8 +- .../apache/fory/graalvm/ForyJsonExample.java | 216 +++++++- .../graalvm/ForyJsonNoProviderExample.java | 197 +++++++ .../GeneratedJsonCodecSourceWriter.java | 13 +- .../processing/JsonTypeProcessor.java | 28 +- .../processing/JsonTypeProcessorTest.java | 17 - java/fory-json/README.md | 29 +- .../java/org/apache/fory/json/ForyJson.java | 6 + .../org/apache/fory/json/ForyJsonBuilder.java | 10 +- .../org/apache/fory/json/JsonCodegenKey.java | 70 +++ .../java/org/apache/fory/json/JsonConfig.java | 68 +-- .../fory/json/JsonGeneratedClassRegistry.java | 186 +++++++ .../json/annotation/ForyJsonProvider.java | 40 ++ .../apache/fory/json/annotation/JsonType.java | 11 +- .../fory/json/codec/CollectionCodec.java | 13 + .../json/codec/GeneratedJsonCodecFactory.java | 28 - .../org/apache/fory/json/codec/MapCodec.java | 13 + .../apache/fory/json/codec/ObjectCodec.java | 62 +++ .../fory/json/codec/ObjectCodecBuilder.java | 7 +- .../apache/fory/json/codec/SqlJsonCodecs.java | 30 +- .../fory/json/meta/JsonCreatorInfo.java | 48 ++ .../fory/json/meta/JsonFieldAccessor.java | 115 ++++ .../resolver/GeneratedCodecInstantiator.java | 216 +++----- .../resolver/GeneratedJsonCodecFactories.java | 98 ---- .../json/resolver/JsonSharedRegistry.java | 194 ++++++- .../json/resolver/JsonStringValueCodec.java | 56 +- .../fory/json/resolver/JsonTypeResolver.java | 106 +++- .../{codec => }/ForyJsonGraalVMFeature.java | 499 ++++++++++++++---- .../apache/fory/json/JsonCreatorCodegen.java | 442 ++++++++++++++++ .../fory-json/native-image.properties | 2 +- .../ForyJsonGraalVMFeatureJarVerifier.java | 7 +- 35 files changed, 2397 insertions(+), 623 deletions(-) create mode 100644 integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java delete mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java delete mode 100644 java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java rename java/fory-json/src/main/java17/org/apache/fory/json/{codec => }/ForyJsonGraalVMFeature.java (57%) create mode 100644 java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java diff --git a/ci/run_ci.sh b/ci/run_ci.sh index 1fddcd7440..5a9ecf5e72 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -80,7 +80,7 @@ install_jdks() { } run_graalvm_tests() { - local main_class="$1" + local main_classes=("$@") local java_version local java_major java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2; exit}') @@ -102,14 +102,16 @@ run_graalvm_tests() { -Dmaven.test.skip=true \ -Dmaven.source.skip=true \ -Dmaven.javadoc.skip=true - echo "Start to build GraalVM JPMS native image for $main_class" cd "$ROOT"/integration_tests/graalvm_tests - mvn -DmainClass="$main_class" -DskipTests=true -Dassembly.skipAssembly=true \ - --no-transfer-progress -Pnative-module clean package - echo "Built GraalVM JPMS native image" - echo "Start to run GraalVM JPMS native image" - ./target/main-module - echo "Execute GraalVM tests for $main_class succeed!" + for main_class in "${main_classes[@]}"; do + echo "Start to build GraalVM JPMS native image for $main_class" + mvn -DmainClass="$main_class" -DskipTests=true -Dassembly.skipAssembly=true \ + --no-transfer-progress -Pnative-module clean package + echo "Built GraalVM JPMS native image" + echo "Start to run GraalVM JPMS native image" + ./target/main-module + echo "Execute GraalVM tests for $main_class succeed!" + done } graalvm_test() { @@ -117,7 +119,9 @@ graalvm_test() { } graalvm_json_tests() { - run_graalvm_tests org.apache.fory.graalvm.ForyJsonExample + run_graalvm_tests \ + org.apache.fory.graalvm.ForyJsonExample \ + org.apache.fory.graalvm.ForyJsonNoProviderExample } jdk25_access_options() { diff --git a/docs/guide/java/graalvm-support.md b/docs/guide/java/graalvm-support.md index a41068a985..96701e611e 100644 --- a/docs/guide/java/graalvm-support.md +++ b/docs/guide/java/graalvm-support.md @@ -46,20 +46,8 @@ compilation is unavailable. ## Fory JSON -Fory JSON uses a separate Native Image workflow. Add the Fory annotation processor to the -application compiler path: - -```xml - - - org.apache.fory - fory-annotation-processor - ${fory.version} - - -``` - -Then add `@JsonType` to each concrete object model that the native executable reads or writes: +Fory JSON has its own Native Image Feature and does not use the Fory annotation processor. Add +`@JsonType` to each reachable concrete object model that the native executable reads or writes: ```java import org.apache.fory.json.ForyJson; @@ -80,7 +68,53 @@ public class JsonExample { } ``` -The processor also supports Fory JSON Mixins for models that cannot be modified: +This is sufficient for correct native execution. During image construction, Fory JSON retains the +model metadata and prepares its field, property, creator, record, and `JsonAnySetter` access +handles. At runtime, `ForyJson.builder().build()` can therefore use interpreted codecs without +application reflection configuration or build-time initialization. + +To include generated codecs for a configuration, return that completed configuration from a +reachable `@ForyJsonProvider`: + +```java +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.PropertyNamingStrategy; +import org.apache.fory.json.annotation.ForyJsonProvider; + +@ForyJsonProvider +public final class JsonConfigs { + private final ForyJson api = + ForyJson.builder() + .writeNullFields(true) + .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerCodec(Money.class, new MoneyCodec()) + .build(); + + public JsonConfigs() {} + + public ForyJson api() { + return api; + } +} +``` + +The provider class must be public and concrete and have a public no-argument constructor. Provider +members are public, non-static, zero-argument instance methods whose exact return type is +`ForyJson`. Inherited superclass methods and public interface default methods are included. A +provider may return multiple configurations, and multiple providers may be reachable. Equivalent +configurations are generated once. + +Provider objects exist only while the image is built. Prefer a dedicated configuration class with +instance fields and methods as shown above; no application `native-image.properties` entry is +needed. Static provider methods and fields are not supported. + +Only configurations returned by a provider receive generated codecs. The default configuration is +not generated implicitly. If a codegen-enabled runtime configuration was not included, Fory JSON +uses its prepared interpreted codecs and logs one process-wide warning recommending a reachable +`@ForyJsonProvider`. `withCodegen(false)` explicitly selects interpreted codecs and does not request +generated-codec lookup. Asynchronous compilation is disabled in a native executable. + +Use Fory JSON Mixins for models that cannot be modified: ```java import org.apache.fory.json.ForyJson; @@ -105,40 +139,25 @@ public class JsonExample { `JsonMixin` is a build-time entry point for its exact declared target, so the target does not need `JsonType` solely to use the Mixin. The registered Mixin class literal must be reachable from the -application. The processor emits available target operations for each non-empty Mixin, and the -Fory JSON Native Image Feature retains the effective runtime metadata. Normal runtime codec -precedence still selects the representation. An empty Mixin produces no generated output. +application. The Native Image Feature retains the target metadata and prepares the same access +handles as it does for a direct `JsonType` model. A provider configuration generates the Mixin +target only when that exact Mixin is registered in the returned `ForyJson`. Only one source is enabled for an exact target in a built `ForyJson`. Later registration replaces an earlier source for subsequent `build()` calls; a runtime keeps the immutable snapshot it was -built with. If the target also has a direct `JsonType` companion, a non-empty registered Mixin -selects the pair-specific artifact instead of combining the overlay with the direct companion. - -Do not add application reflection configuration as a replacement for the generated configuration. -The native executable resolves the same effective annotations as the JVM. - -The processor generates direct property and creator operations. The `fory-json` artifact activates -its Native Image Feature automatically and retains the generated factories and required 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, but each -concrete object subtype needs its own direct `@JsonType` to receive generated operations. 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 readers and writers with the generated property and -creator operations. `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. +built with. + +The `fory-json` artifact activates its Native Image Feature automatically. `@JsonType` is not +inherited, so annotate every concrete runtime model. An annotated base with a class-literal +`@JsonSubTypes` table registers its listed subtypes automatically. Reachable concrete `Collection` +and `Map` root types are supported when they have the public no-argument constructor required by +Fory JSON. A class referenced only by a runtime string is not reachable; +`JsonSubTypes.Type.className` is therefore unsupported in a native image. Type, field, effective ordinary getter, setter value parameter, and `JsonCreator` parameter -`@JsonCodec` annotations are supported. The Feature registers every selected complete-value, -element, content, Map-key, and Map-value codec constructor. This is the same annotation model used -on the JVM and Android. +`@JsonCodec` annotations are supported. The Feature retains every selected complete-value, element, +content, Map-key, and Map-value codec constructor. This is the same annotation model used on the +JVM and Android. `JsonValue` fields and effective public zero-argument methods are supported, including matching one-String `JsonCreator` constructors and public static factories. Fixed `JsonRawValue` fields and @@ -154,11 +173,10 @@ instead. `@JsonCodec(valueCodec = ...)` on that field or getter to customize each dynamic value. A second `JsonAnySetter` parameter may use the normal configuration for its own value shape. -`JsonUnwrapped` uses the same interpreted behavior as on the JVM. For direct target annotations, -annotate the containing model and every unwrapped child or intermediate object with `JsonType` so -each model receives its generated property and creator operations. A Mixin retains the -unwrapped models reached by its effective schema; register a separate exact Mixin for a child only -when that child's annotations also need an overlay. +`JsonUnwrapped` uses the same behavior as on the JVM. For direct target annotations, annotate the +containing model and every unwrapped child or intermediate object with `JsonType`. A Mixin retains +the unwrapped models reached by its effective schema; register a separate exact Mixin for a child +only when that child's annotations also need an overlay. Child codecs act on one direct level. `elementCodec` supports `Collection`, Java arrays, and `AtomicReferenceArray`; `contentCodec` supports `Optional` and `AtomicReference`; `keyCodec` and diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md index 73b9c96574..311c95159e 100644 --- a/docs/guide/java/json-support.md +++ b/docs/guide/java/json-support.md @@ -300,8 +300,10 @@ independently to each reader; zero disables the cache, and the setting does not input. 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. +In a GraalVM native image, runtime compilation and asynchronous compilation are unavailable. +Configurations returned by a reachable `ForyJsonProvider` use codecs generated while the image is +built; other configurations use interpreted codecs with build-time-prepared access handles. Every +other builder option keeps the behavior described above. ## Annotations @@ -335,15 +337,17 @@ import org.apache.fory.json.annotation.JsonUnwrapped; ``` `JsonType` asks the annotation processor to generate direct property and creator operations plus -the exact retention rules for an eligible concrete object model. A directly annotated -`JsonValue` Record also receives a companion so its value accessor and canonical constructor work -after Android desugaring. The same generated companion is used on the JVM, Android, and GraalVM -Native Image. The annotation is not inherited; a concrete subtype needs its own direct annotation -to receive a companion. See -[GraalVM Support](graalvm-support.md) and [Android Support](android-support.md) for setup. -A directly annotated model that uses the default object codec requires that generated companion; +the exact retention rules for an eligible concrete object model on the JVM and Android. A directly +annotated `JsonValue` Record also receives a companion so its value accessor and canonical +constructor work after Android desugaring. The annotation is not inherited; a concrete subtype +needs its own direct annotation to receive a companion on those runtimes. A directly annotated +model that uses the default object codec requires that generated companion outside Native Image; the runtime reports a configuration error if the processor output is missing. +GraalVM Native Image discovers `JsonType` directly and does not use annotation-processor output. +See [GraalVM Support](graalvm-support.md) for optional provider-based hosted code generation and +[Android Support](android-support.md) for annotation-processor setup. + ### Mixins Use a Mixin to configure an existing class without changing its source: @@ -427,9 +431,10 @@ Mixin does not introduce a separate record-component model. Use source selectors declarations and keep repeated annotations consistent as required by normal record property mapping. -On Android and GraalVM Native Image, compile non-empty Mixin sources with the Fory annotation -processor so required generated operations and platform configuration are available. See -[Android Support](android-support.md) and [GraalVM Support](graalvm-support.md). +On Android, compile non-empty Mixin sources with the Fory annotation processor so required +generated operations and platform configuration are available. GraalVM Native Image discovers +reachable Mixins directly. See [Android Support](android-support.md) and +[GraalVM Support](graalvm-support.md). ### `JsonProperty` diff --git a/integration_tests/graalvm_tests/README.md b/integration_tests/graalvm_tests/README.md index 3192ebdc64..5d82f836e6 100644 --- a/integration_tests/graalvm_tests/README.md +++ b/integration_tests/graalvm_tests/README.md @@ -1,17 +1,21 @@ # GraalVM Native Image Tests -Examples and tests for Fory serialization in GraalVM Native Image. The Fory JSON -entry point covers direct `JsonType` models and runtime registration of exact -`JsonMixin` target/source mappings. Native-image hosted analysis resolves each mapping and registers -its generated factory when present. The built executables then execute direct and Mixin mappings at -runtime; the module-path run verifies the same factories through JPMS. +Examples and tests for Fory serialization in GraalVM Native Image. The Fory JSON entry points are +compiled with annotation processing disabled. They cover direct `JsonType` models, exact +`JsonMixin` target/source mappings, provider-selected hosted codec generation, configuration +fallback to interpreted codecs, and a separate image with no reachable `ForyJsonProvider`. ## Test ```bash -mvn clean -DskipTests=true -Pnative package +mvn -DmainClass=org.apache.fory.graalvm.ForyJsonExample clean -DskipTests=true -Dexec.skip=true -Pnative package ./target/main -mvn clean -DskipTests=true -Pnative-module package +mvn -DmainClass=org.apache.fory.graalvm.ForyJsonExample clean -DskipTests=true -Pnative-module package +./target/main-module + +mvn -DmainClass=org.apache.fory.graalvm.ForyJsonNoProviderExample clean -DskipTests=true -Dexec.skip=true -Pnative package +./target/main +mvn -DmainClass=org.apache.fory.graalvm.ForyJsonNoProviderExample clean -DskipTests=true -Pnative-module package ./target/main-module ``` diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml index 96b28396db..63b9935101 100644 --- a/integration_tests/graalvm_tests/pom.xml +++ b/integration_tests/graalvm_tests/pom.xml @@ -95,13 +95,7 @@ ${maven.compiler.source} ${maven.compiler.source} - - - org.apache.fory - fory-annotation-processor - ${project.version} - - + none diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java index 4f634d4e97..42cf5a00d4 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -19,6 +19,8 @@ package org.apache.fory.graalvm; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; @@ -38,6 +40,7 @@ import org.apache.fory.graalvm.closed.ClosedJsonRecord; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; +import org.apache.fory.json.annotation.ForyJsonProvider; import org.apache.fory.json.annotation.JsonAnyProperty; import org.apache.fory.json.annotation.JsonBase64; import org.apache.fory.json.annotation.JsonCodec; @@ -52,35 +55,117 @@ import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.MapKeyCodec; +import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.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.platform.GraalvmSupport; import org.apache.fory.util.Preconditions; /** Native-image acceptance coverage for the complete interpreted Fory JSON path. */ public final class ForyJsonExample { + private static final String NATIVE_INTERPRETER_MESSAGE = + "Fory JSON is using interpreted codecs because the current configuration was not included " + + "in this native image. Return this configuration from a reachable " + + "@ForyJsonProvider to enable generated codecs."; + private ForyJsonExample() {} public static void main(String[] args) { - testModels(); - testConfigurations(); - testCodecs(); - testValueAnnotations(); - testSubtypes(); - testContainerRoots(); - testGenericProperties(); - testUnwrapped(); - testMixin(); - testMixinValue(); - testMixinValueRecord(); - testMixinEnumValue(); - testMixinCodec(); - testBigDecimal(); - testSqlTypes(); - testClosedPackage(); - System.out.println("Fory JSON succeed"); + PrintStream originalOut = System.out; + ByteArrayOutputStream captured = new ByteArrayOutputStream(); + try (PrintStream testOut = new PrintStream(captured, true, StandardCharsets.UTF_8)) { + System.setOut(testOut); + try { + Preconditions.checkArgument(JsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class)); + if (GraalvmSupport.isGraalRuntime()) { + testHostedCodegenConfigurations(); + } + testModels(); + testConfigurations(); + testCodecs(); + testValueAnnotations(); + testSubtypes(); + testContainerRoots(); + testGenericProperties(); + testUnwrapped(); + testMixin(); + testMixinValue(); + testMixinValueRecord(); + testMixinEnumValue(); + testMixinCodec(); + testBigDecimal(); + testSqlTypes(); + testClosedPackage(); + } finally { + System.setOut(originalOut); + } + } + String output = new String(captured.toByteArray(), StandardCharsets.UTF_8); + if (GraalvmSupport.isGraalRuntime()) { + int occurrences = countOccurrences(output, NATIVE_INTERPRETER_MESSAGE); + Preconditions.checkArgument( + occurrences == 1, + "Expected one Native Image interpreted-codec message, found " + + occurrences + + ": " + + output); + } + originalOut.print(output); + originalOut.println("Fory JSON succeed"); + } + + private static void testHostedCodegenConfigurations() { + exerciseCodegenConfiguration(ForyJson.builder().build(), false); + exerciseCodegenConfiguration(newProviderJson(), true); + exerciseCodegenConfiguration( + ForyJson.builder() + .withFieldMode(true) + .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .build(), + false); + } + + private static ForyJson newProviderJson() { + return ForyJson.builder() + .writeNullFields(true) + .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .build(); + } + + private static void exerciseCodegenConfiguration(ForyJson json, boolean generated) { + CodegenProbeCodec.expectGenerated = generated; + CodegenProbeModel value = new CodegenProbeModel(); + value.id = 41; + value.probe = new CodegenProbeValue("probe"); + String encoded = json.toJson(value); + Preconditions.checkArgument(encoded.contains("probe")); + String utf8 = new String(json.toJsonBytes(value), StandardCharsets.UTF_8); + Preconditions.checkArgument(utf8.contains("probe")); + Preconditions.checkArgument( + json.fromJson(encoded, CodegenProbeModel.class).probe.value.equals("probe")); + String utf16 = encoded.replace(":\"probe\"", ":\"\u4f60\""); + Preconditions.checkArgument( + json.fromJson(utf16, CodegenProbeModel.class).probe.value.equals("\u4f60")); + Preconditions.checkArgument( + json.fromJson(utf8.getBytes(StandardCharsets.UTF_8), CodegenProbeModel.class) + .probe + .value + .equals("probe")); + } + + private static int countOccurrences(String value, String target) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(target, offset)) >= 0) { + count++; + offset += target.length(); + } + return count; } private static void testClosedPackage() { @@ -368,6 +453,103 @@ private static void testSqlTypes() { Preconditions.checkArgument(decoded.timestamp.getTime() == 3_000L); } + public interface InheritedJsonConfig { + default ForyJson duplicateConfiguration() { + return newProviderJson(); + } + } + + public static class ParentJsonConfigs { + public ForyJson generatedConfiguration() { + return newProviderJson(); + } + } + + @ForyJsonProvider + public static final class JsonConfigs extends ParentJsonConfigs + implements InheritedJsonConfig { + public JsonConfigs() {} + } + + @JsonType + public static final class CodegenProbeModel { + public int id; + + @JsonCodec(CodegenProbeCodec.class) + public CodegenProbeValue probe; + + public CodegenProbeModel() {} + } + + public static final class CodegenProbeValue { + private final String value; + + private CodegenProbeValue(String value) { + this.value = value; + } + } + + public static final class CodegenProbeCodec implements JsonValueCodec { + private static boolean expectGenerated; + + public CodegenProbeCodec() {} + + @Override + public void writeString(StringJsonWriter writer, CodegenProbeValue value) { + checkCapability( + writer + .typeResolver() + .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) + .stringWriter()); + writer.writeString(value == null ? null : value.value); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, CodegenProbeValue value) { + checkCapability( + writer + .typeResolver() + .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) + .utf8Writer()); + writer.writeString(value == null ? null : value.value); + } + + @Override + public CodegenProbeValue readLatin1(Latin1JsonReader reader) { + checkCapability( + reader + .typeResolver() + .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) + .latin1Reader()); + return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); + } + + @Override + public CodegenProbeValue readUtf16(Utf16JsonReader reader) { + checkCapability( + reader + .typeResolver() + .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) + .utf16Reader()); + return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); + } + + @Override + public CodegenProbeValue readUtf8(Utf8JsonReader reader) { + checkCapability( + reader + .typeResolver() + .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) + .utf8Reader()); + return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); + } + + private static void checkCapability(Object capability) { + boolean generated = !(capability instanceof ObjectCodec); + Preconditions.checkArgument(generated == expectGenerated); + } + } + public static class Parent { private int inheritedId = 10; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java new file mode 100644 index 0000000000..17e81ec789 --- /dev/null +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java @@ -0,0 +1,197 @@ +/* + * 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.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +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.JsonProperty; +import org.apache.fory.json.annotation.JsonType; +import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.codec.ObjectCodec; +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.platform.GraalvmSupport; +import org.apache.fory.util.Preconditions; + +/** Native-image acceptance coverage when no {@code ForyJsonProvider} is reachable. */ +public final class ForyJsonNoProviderExample { + private static final String NATIVE_INTERPRETER_MESSAGE = + "Fory JSON is using interpreted codecs because the current configuration was not included " + + "in this native image. Return this configuration from a reachable " + + "@ForyJsonProvider to enable generated codecs."; + + private ForyJsonNoProviderExample() {} + + public static void main(String[] args) { + PrintStream originalOut = System.out; + ByteArrayOutputStream captured = new ByteArrayOutputStream(); + try (PrintStream testOut = new PrintStream(captured, true, StandardCharsets.UTF_8)) { + System.setOut(testOut); + try { + exercise(ForyJson.builder().build()); + exercise(ForyJson.builder().withFieldMode(true).build()); + } finally { + System.setOut(originalOut); + } + } + String output = new String(captured.toByteArray(), StandardCharsets.UTF_8); + if (GraalvmSupport.isGraalRuntime()) { + int occurrences = countOccurrences(output, NATIVE_INTERPRETER_MESSAGE); + Preconditions.checkArgument( + occurrences == 1, + "Expected one Native Image interpreted-codec message, found " + + occurrences + + ": " + + output); + } + originalOut.print(output); + originalOut.println("Fory JSON without provider succeed"); + } + + private static void exercise(ForyJson json) { + Model value = new Model(7, new Probe("value")); + String encoded = json.toJson(value); + Preconditions.checkArgument(json.toJsonBytes(value).length != 0); + Preconditions.checkArgument(json.fromJson(encoded, Model.class).equals(value)); + Preconditions.checkArgument( + json.fromJson(encoded.replace("value", "\u4f60"), Model.class) + .probe + .value + .equals("\u4f60")); + Preconditions.checkArgument( + json.fromJson(encoded.getBytes(StandardCharsets.UTF_8), Model.class).equals(value)); + } + + private static int countOccurrences(String value, String target) { + int count = 0; + int offset = 0; + while ((offset = value.indexOf(target, offset)) >= 0) { + count++; + offset += target.length(); + } + return count; + } + + @JsonType + public static final class Model { + private final int id; + + @JsonCodec(ProbeCodec.class) + private final Probe probe; + + @JsonCreator + public Model( + @JsonProperty("id") int id, @JsonProperty("probe") Probe probe) { + this.id = id; + this.probe = probe; + } + + public int getId() { + return id; + } + + public Probe getProbe() { + return probe; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Model)) { + return false; + } + Model that = (Model) other; + return id == that.id && probe.equals(that.probe); + } + + @Override + public int hashCode() { + return 31 * id + probe.hashCode(); + } + } + + public static final class Probe { + private final String value; + + private Probe(String value) { + this.value = value; + } + + @Override + public boolean equals(Object other) { + return other instanceof Probe && value.equals(((Probe) other).value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } + + public static final class ProbeCodec implements JsonValueCodec { + public ProbeCodec() {} + + @Override + public void writeString(StringJsonWriter writer, Probe value) { + checkInterpreted( + writer.typeResolver().getTypeInfo(Model.class, Model.class).stringWriter()); + writer.writeString(value == null ? null : value.value); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, Probe value) { + checkInterpreted(writer.typeResolver().getTypeInfo(Model.class, Model.class).utf8Writer()); + writer.writeString(value == null ? null : value.value); + } + + @Override + public Probe readLatin1(Latin1JsonReader reader) { + checkInterpreted(reader.typeResolver().getTypeInfo(Model.class, Model.class).latin1Reader()); + return reader.tryReadNullToken() ? null : new Probe(reader.readString()); + } + + @Override + public Probe readUtf16(Utf16JsonReader reader) { + checkInterpreted(reader.typeResolver().getTypeInfo(Model.class, Model.class).utf16Reader()); + return reader.tryReadNullToken() ? null : new Probe(reader.readString()); + } + + @Override + public Probe readUtf8(Utf8JsonReader reader) { + checkInterpreted(reader.typeResolver().getTypeInfo(Model.class, Model.class).utf8Reader()); + return reader.tryReadNullToken() ? null : new Probe(reader.readString()); + } + + private static void checkInterpreted(Object capability) { + if (GraalvmSupport.isGraalRuntime()) { + Preconditions.checkArgument(capability instanceof ObjectCodec); + } + } + } +} diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java index 0d9a6032e7..9bec70b354 100644 --- a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java +++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/GeneratedJsonCodecSourceWriter.java @@ -764,18 +764,7 @@ private String render(Model model) { if (model.anySetter != null) { renderAnySetter(source, model.anySetter); } - source - .append(" public static final class Factory\n") - .append(" implements org.apache.fory.json.codec.GeneratedJsonCodecFactory {\n") - .append(" public Factory() {}\n\n") - .append(" @Override\n") - .append(" public org.apache.fory.json.codec.GeneratedJsonCodec create() {\n") - .append(" return new ") - .append(model.simpleName) - .append("();\n") - .append(" }\n") - .append(" }\n") - .append("}\n"); + source.append("}\n"); return source.toString(); } diff --git a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java index ec2d0d715b..4f9c827def 100644 --- a/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java +++ b/java/fory-annotation-processor/src/main/java/org/apache/fory/annotation/processing/JsonTypeProcessor.java @@ -57,7 +57,7 @@ import javax.tools.Diagnostic; import javax.tools.StandardLocation; -/** Generates JSON companions and platform configuration for annotated models and Mixins. */ +/** Generates JSON companions and R8 configuration for annotated models and Mixins. */ final class JsonTypeProcessor { private static final String JSON_PACKAGE = "org.apache.fory.json"; private static final String JSON_TYPE = JSON_PACKAGE + ".annotation.JsonType"; @@ -78,8 +78,6 @@ final class JsonTypeProcessor { private static final String NO_MAP_KEY_CODEC = JSON_CODEC + "$NoMapKeyCodec"; private static final String R8_PREFIX = "META-INF/proguard/fory-json-"; private static final String R8_MIXIN_PREFIX = "META-INF/proguard/fory-json-mixin-"; - private static final String NATIVE_IMAGE_PREFIX = - "META-INF/native-image/org.apache.fory/fory-json-"; private static final String[] CODEC_MEMBERS = { "value", "elementCodec", "contentCodec", "keyCodec", "valueCodec" }; @@ -141,7 +139,6 @@ void process(RoundEnvironment roundEnvironment) { List subtypes = classLiteralSubtypes(type, model.binaryFallbackTypes); model.sort(); emitR8(model); - emitNativeImageProperties(model); pending.addAll(subtypes); } catch (GeneratedJsonCodecSourceWriter.InvalidJsonTypeException e) { messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), e.element); @@ -254,7 +251,6 @@ private void processMixins(RoundEnvironment roundEnvironment) { } model.sort(); emitR8(model); - emitNativeImageProperties(model); } catch (JsonMixinAnnotations.InvalidJsonMixinException e) { messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage(), e.element); } catch (GeneratedJsonCodecSourceWriter.InvalidJsonTypeException e) { @@ -542,28 +538,6 @@ private void emitR8(Model model) { } } - private void emitNativeImageProperties(Model model) { - if (model.companionBinaryName == null) { - return; - } - // The hosted feature freezes factory instances into the image heap after reachability is - // known, but GraalVM accepts class-initialization configuration only before analysis starts. - // Emit the exact generated class here so unreachable model classes remain removable. - String resourceName = - NATIVE_IMAGE_PREFIX + model.companionBinaryName + "/native-image.properties"; - try { - javax.tools.FileObject file = - filer.createResource( - StandardLocation.CLASS_OUTPUT, "", resourceName, model.originatingElements()); - try (Writer writer = file.openWriter()) { - writer.write("Args=--initialize-at-build-time=" + model.companionBinaryName + "$Factory\n"); - } - } catch (IOException e) { - throw new InvalidJsonTypeException( - "Failed to write generated JSON Native Image properties: " + e, model.target); - } - } - private String writeR8(Model model) { StringBuilder builder = new StringBuilder(8192); builder.append("-keepattributes Signature,RuntimeVisibleAnnotations\n"); diff --git a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java index 0c19450e7f..80bfa2edba 100644 --- a/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java +++ b/java/fory-annotation-processor/src/test/java/org/apache/fory/annotation/processing/JsonTypeProcessorTest.java @@ -49,7 +49,6 @@ import org.apache.fory.json.ForyJson; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; -import org.apache.fory.json.codec.GeneratedJsonCodecFactory; import org.apache.fory.json.meta.JsonAnySetterAccessor; import org.apache.fory.json.meta.JsonFieldAccessor; import org.testng.SkipException; @@ -58,8 +57,6 @@ public class JsonTypeProcessorTest { private static final String RULE_PREFIX = "META-INF/proguard/fory-json-"; private static final String MIXIN_RULE_PREFIX = "META-INF/proguard/fory-json-mixin-"; - private static final String NATIVE_IMAGE_PREFIX = - "META-INF/native-image/org.apache.fory/fory-json-"; @Test public void unannotatedType() throws Exception { @@ -82,8 +79,6 @@ public void emptyMixin() throws Exception { String companion = "test.EmptyMixin_ForyJsonMixin_test_x2e_Target_ForyJsonCodec"; assertFalse(result.hasGeneratedSource(companion.replace('.', '/') + ".java")); assertFalse(result.hasGeneratedResource(MIXIN_RULE_PREFIX + "test.EmptyMixin.pro")); - assertFalse( - result.hasGeneratedResource(NATIVE_IMAGE_PREFIX + companion + "/native-image.properties")); } @Test @@ -505,10 +500,6 @@ public void jsonTypeRules() throws Exception { assertFalse(rules.contains("test.Plain_ForyJsonCodec$Factory"), rules); assertFalse(rules.contains("-keep,allowoptimization,allowobfuscation class test.Plain"), rules); assertTrue(result.hasGeneratedSource("test/Plain_ForyJsonCodec.java")); - assertEquals( - result.generatedResource( - NATIVE_IMAGE_PREFIX + "test.Plain_ForyJsonCodec/native-image.properties"), - "Args=--initialize-at-build-time=test.Plain_ForyJsonCodec$Factory\n"); } @Test @@ -553,14 +544,6 @@ public void generatedAccessors() throws Exception { anySetter.put(model, "answer", 42); Field extra = modelType.getField("extra"); assertEquals(((Map) extra.get(model)).get("answer"), 42); - - GeneratedJsonCodecFactory factory = - (GeneratedJsonCodecFactory) - loader - .loadClass("test.GeneratedModel_ForyJsonCodec$Factory") - .getConstructor() - .newInstance(); - assertEquals(factory.create().type(), modelType); } @Test diff --git a/java/fory-json/README.md b/java/fory-json/README.md index eb7aa1d086..fb3f97ec24 100644 --- a/java/fory-json/README.md +++ b/java/fory-json/README.md @@ -376,8 +376,11 @@ transport boundary when parsing untrusted input. Builder mutation after `build()` does not modify an existing `ForyJson` runtime. -On Android and in a GraalVM native image, runtime code generation and asynchronous compilation are -automatically disabled. Every other builder option keeps the behavior described above. +On Android, runtime code generation and asynchronous compilation are disabled. In a GraalVM native +image, runtime compilation is unavailable; configurations returned by a reachable +`ForyJsonProvider` use codecs generated while the image is built, and other configurations use +interpreted codecs with build-time-prepared access handles. Every other builder option keeps the +behavior described above. ## JSON annotations @@ -388,13 +391,15 @@ and `JsonValue`. `JsonType` is a separate build-time generation marker. They are not Jackson, Gson, or Fory binary-protocol compatibility annotations. `JsonType` asks the annotation processor to generate direct property and creator operations plus -exact retention rules. It is not inherited, so annotate each eligible concrete model that needs a -generated companion. A directly annotated `JsonValue` Record also receives a companion for its -value accessor and canonical constructor. Ordinary unannotated classes may still use reflection; on -Android they need application-authored exact R8 rules. Android-desugared Records require -processor-generated operations from either a direct `JsonType` declaration or a compiled exact -`JsonMixin` pair. A directly annotated model that uses the default object codec fails during codec -creation if its generated companion is missing. +exact retention rules on the JVM and Android. It is not inherited, so annotate each eligible +concrete model that needs a generated companion on those runtimes. A directly annotated +`JsonValue` Record also receives a companion for its value accessor and canonical constructor. +Ordinary unannotated classes may still use reflection; on Android they need application-authored +exact R8 rules. Android-desugared Records require processor-generated operations from either a +direct `JsonType` declaration or a compiled exact `JsonMixin` pair. Outside Native Image, a +directly annotated model that uses the default object codec fails during codec creation if its +generated companion is missing. GraalVM Native Image discovers `JsonType` directly and does not use +annotation-processor output. See the [GraalVM guide](../../docs/guide/java/graalvm-support.md) and [Android guide](../../docs/guide/java/android-support.md) for the platform workflows. @@ -454,9 +459,9 @@ A `JsonCodec` supplied by a Mixin is the target's effective annotation. An exact `registerCodec` registration still wins, while the effective type annotation wins over a built-in mapping. -On Android and GraalVM Native Image, compile non-empty Mixins with the Fory annotation processor -so required generated operations and platform configuration are available. See the platform guides -linked above. +On Android, compile non-empty Mixins with the Fory annotation processor so required generated +operations and platform configuration are available. GraalVM Native Image discovers reachable +Mixins directly. See the platform guides linked above. ### `JsonProperty` diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index d4841a6379..bc4112bf4c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -83,6 +83,7 @@ public final class ForyJson { public static final int DEFAULT_MAX_CACHED_FIELD_NAMES = 8192; private final int homeSlotMask; + private final JsonConfig config; private final PooledState[] slots; ForyJson(JsonConfig config) { @@ -90,6 +91,7 @@ public final class ForyJson { } ForyJson(JsonConfig config, JsonSharedRegistry sharedRegistry) { + this.config = config; int poolSize = config.concurrencyLevel(); homeSlotMask = Integer.highestOneBit(poolSize) - 1; // This fixed array is the only JsonState owner. Each state's three readers own their configured @@ -106,6 +108,10 @@ public static ForyJsonBuilder builder() { return new ForyJsonBuilder(); } + JsonConfig config() { + return config; + } + /** * Serializes {@code value} as one complete JSON document backed by a detached String. * 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 102b36b0a8..f9535197a1 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 @@ -76,7 +76,9 @@ public ForyJsonBuilder writeNullFields(boolean writeNullFields) { /** * Enables generated object codecs for supported classes. Enabled by default and automatically - * disabled on Android and in a GraalVM native image. + * disabled on Android. In a GraalVM native image, generated codecs are available only for + * configurations returned by a reachable {@link + * org.apache.fory.json.annotation.ForyJsonProvider}; other configurations use interpreted codecs. */ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { this.codegenEnabled = codegenEnabled; @@ -248,9 +250,9 @@ public ForyJson build() { fixedClassLoader = ForyJson.class.getClassLoader(); } } - boolean effectiveCodegen = - codegenEnabled && !AndroidSupport.IS_ANDROID && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE; - boolean effectiveAsyncCompilation = asyncCompilationEnabled && effectiveCodegen; + boolean effectiveCodegen = codegenEnabled && !AndroidSupport.IS_ANDROID; + boolean effectiveAsyncCompilation = + asyncCompilationEnabled && effectiveCodegen && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE; return new ForyJson( new JsonConfig( writeNullFields, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java new file mode 100644 index 0000000000..a7e8537518 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java @@ -0,0 +1,70 @@ +/* + * 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; + +import java.util.Objects; +import org.apache.fory.annotation.Internal; + +/** Immutable identity for settings which can change generated Fory JSON source. */ +@Internal +public final class JsonCodegenKey { + private final boolean writeNullFields; + private final boolean propertyDiscoveryEnabled; + private final PropertyNamingStrategy propertyNamingStrategy; + private final String codecRegistryKey; + private final String mixinKey; + + JsonCodegenKey( + boolean writeNullFields, + boolean propertyDiscoveryEnabled, + PropertyNamingStrategy propertyNamingStrategy, + String codecRegistryKey, + String mixinKey) { + this.writeNullFields = writeNullFields; + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + this.propertyNamingStrategy = propertyNamingStrategy; + this.codecRegistryKey = codecRegistryKey; + this.mixinKey = mixinKey; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof JsonCodegenKey)) { + return false; + } + JsonCodegenKey that = (JsonCodegenKey) other; + return writeNullFields == that.writeNullFields + && propertyDiscoveryEnabled == that.propertyDiscoveryEnabled + && propertyNamingStrategy == that.propertyNamingStrategy + && codecRegistryKey.equals(that.codecRegistryKey) + && mixinKey.equals(that.mixinKey); + } + + @Override + public int hashCode() { + int result = + Objects.hash( + writeNullFields, propertyDiscoveryEnabled, propertyNamingStrategy, codecRegistryKey); + return 31 * result + mixinKey.hashCode(); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java index 2d82636d67..9835d6afe7 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java @@ -35,12 +35,11 @@ /** * Build configuration used to create all pooled states of one {@link ForyJson} instance. * - *

Scalar settings are fixed at construction. The codec registry is builder-owned mutable input - * and is copied immediately by the runtime's shared registry; the JSON runtime never mutates it. - * {@link #getCodegenHash()} identifies only settings that can change generated source; runtime-only - * settings such as depth and asynchronous scheduling do not fragment generated class names. - * Concurrency, per-reader field-name cache, and retained writer-buffer limits are also runtime-only - * and do not fragment generated class names. + *

Scalar settings and the codec registry are snapshotted at construction; the JSON runtime never + * observes later builder mutation. {@link #getCodegenHash()} identifies only settings that can + * change generated source; runtime-only settings such as depth and asynchronous scheduling do not + * fragment generated class names. Concurrency, per-reader field-name cache, and retained + * writer-buffer limits are also runtime-only and do not fragment generated class names. */ public final class JsonConfig { private static final int MAX_CACHED_FIELD_NAMES = 1 << 29; @@ -59,7 +58,7 @@ public final class JsonConfig { private final Map, Class> mixins; private final JsonTypeChecker typeChecker; private final JsonTypeCheckContext typeCheckContext; - private final CodegenKey codegenKey; + private final JsonCodegenKey codegenKey; private transient int codegenHash; JsonConfig( @@ -88,13 +87,13 @@ public final class JsonConfig { this.maxCachedFieldNames = maxCachedFieldNames; this.concurrencyLevel = concurrencyLevel; this.bufferSizeLimitBytes = bufferSizeLimitBytes; - this.codecRegistry = codecRegistry; + this.codecRegistry = codecRegistry.copy(); this.mixins = immutableMixins(mixins); this.typeChecker = typeChecker; typeCheckContext = new JsonTypeCheckContext(); - String codecRegistryKey = codecRegistry.codegenKey(); + String codecRegistryKey = this.codecRegistry.codegenKey(); codegenKey = - new CodegenKey( + new JsonCodegenKey( writeNullFields, propertyDiscoveryEnabled, propertyNamingStrategy, @@ -200,7 +199,7 @@ private static String mixinKey(Map, Class> mixins) { // Equal generated source inputs share one map entry, following core generated-code naming model. // This process-wide map retains only immutable configuration text and integers, never user // classes, codec instances, class loaders, or generated classes. - private static final ConcurrentMap CODEGEN_ID_MAP = + private static final ConcurrentMap CODEGEN_ID_MAP = new ConcurrentHashMap<>(); public int getCodegenHash() { @@ -210,48 +209,9 @@ public int getCodegenHash() { return codegenHash; } - private static final class CodegenKey { - private final boolean writeNullFields; - private final boolean propertyDiscoveryEnabled; - private final PropertyNamingStrategy propertyNamingStrategy; - private final String codecRegistryKey; - private final String mixinKey; - - private CodegenKey( - boolean writeNullFields, - boolean propertyDiscoveryEnabled, - PropertyNamingStrategy propertyNamingStrategy, - String codecRegistryKey, - String mixinKey) { - this.writeNullFields = writeNullFields; - this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; - this.propertyNamingStrategy = propertyNamingStrategy; - this.codecRegistryKey = codecRegistryKey; - this.mixinKey = mixinKey; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - CodegenKey that = (CodegenKey) other; - return writeNullFields == that.writeNullFields - && propertyDiscoveryEnabled == that.propertyDiscoveryEnabled - && propertyNamingStrategy == that.propertyNamingStrategy - && Objects.equals(codecRegistryKey, that.codecRegistryKey) - && Objects.equals(mixinKey, that.mixinKey); - } - - @Override - public int hashCode() { - int result = - Objects.hash( - writeNullFields, propertyDiscoveryEnabled, propertyNamingStrategy, codecRegistryKey); - return 31 * result + mixinKey.hashCode(); - } + /** Returns the immutable generated-source identity for this configuration. */ + @Internal + public JsonCodegenKey codegenKey() { + return codegenKey; } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java new file mode 100644 index 0000000000..78a116afa2 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java @@ -0,0 +1,186 @@ +/* + * 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; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import org.apache.fory.annotation.Internal; +import org.apache.fory.json.resolver.JsonSharedRegistry.GeneratedClasses; +import org.apache.fory.reflect.TypeRef; + +/** Frozen Native Image mapping from JSON configuration semantics to generated classes. */ +@Internal +public final class JsonGeneratedClassRegistry { + private static Map pending = new HashMap<>(); + private static Map configurations = Collections.emptyMap(); + private static boolean frozen; + + private JsonGeneratedClassRegistry() {} + + static synchronized Set> register( + JsonCodegenKey key, GeneratedClasses generatedClasses) { + if (frozen) { + throw new IllegalStateException("Fory JSON generated class registry is frozen"); + } + MutableConfiguration configuration = pending.get(key); + if (configuration == null) { + configuration = new MutableConfiguration(); + pending.put(key, configuration); + } + LinkedHashSet> added = new LinkedHashSet<>(); + configuration.merge(generatedClasses, added); + configurations = snapshot(); + return added; + } + + static synchronized void freeze() { + if (frozen) { + return; + } + pending = null; + frozen = true; + } + + private static Map snapshot() { + Map snapshot = new HashMap<>(pending.size()); + for (Map.Entry entry : pending.entrySet()) { + snapshot.put(entry.getKey(), entry.getValue().freeze()); + } + return Collections.unmodifiableMap(snapshot); + } + + /** Returns the immutable generated classes for {@code key}, or {@code null}. */ + @Internal + public static Configuration configuration(JsonCodegenKey key) { + return configurations.get(key); + } + + /** Immutable generated classes for one configuration. */ + @Internal + public static final class Configuration { + private final Map, Class> stringWriters; + private final Map, Class> utf8Writers; + private final Map, Class> latin1Readers; + private final Map, Class> utf16Readers; + private final Map, Class> utf8Readers; + private final Map> utf8CollectionWriters; + private final Map> utf8CollectionReaders; + + private Configuration(MutableConfiguration source) { + stringWriters = immutable(source.stringWriters); + utf8Writers = immutable(source.utf8Writers); + latin1Readers = immutable(source.latin1Readers); + utf16Readers = immutable(source.utf16Readers); + utf8Readers = immutable(source.utf8Readers); + utf8CollectionWriters = immutable(source.utf8CollectionWriters); + utf8CollectionReaders = immutable(source.utf8CollectionReaders); + } + + public Class stringWriter(Class type) { + return stringWriters.get(type); + } + + public Class utf8Writer(Class type) { + return utf8Writers.get(type); + } + + public Class latin1Reader(Class type) { + return latin1Readers.get(type); + } + + public Class utf16Reader(Class type) { + return utf16Readers.get(type); + } + + public Class utf8Reader(Class type) { + return utf8Readers.get(type); + } + + public Class utf8CollectionWriter(Type type) { + return utf8CollectionWriters.get(typeKey(type)); + } + + public Class utf8CollectionReader(Type type) { + return utf8CollectionReaders.get(typeKey(type)); + } + + private static Map> immutable(Map> classes) { + return classes.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(classes)); + } + } + + private static final class MutableConfiguration { + private final Map, Class> stringWriters = new HashMap<>(); + private final Map, Class> utf8Writers = new HashMap<>(); + private final Map, Class> latin1Readers = new HashMap<>(); + private final Map, Class> utf16Readers = new HashMap<>(); + private final Map, Class> utf8Readers = new HashMap<>(); + private final Map> utf8CollectionWriters = new HashMap<>(); + private final Map> utf8CollectionReaders = new HashMap<>(); + + private void merge(GeneratedClasses source, Set> added) { + merge(source.stringWriters(), stringWriters, added); + merge(source.utf8Writers(), utf8Writers, added); + merge(source.latin1Readers(), latin1Readers, added); + merge(source.utf16Readers(), utf16Readers, added); + merge(source.utf8Readers(), utf8Readers, added); + mergeTypes(source.utf8CollectionWriters(), utf8CollectionWriters, added); + mergeTypes(source.utf8CollectionReaders(), utf8CollectionReaders, added); + } + + private Configuration freeze() { + return new Configuration(this); + } + + private static void merge( + Map> source, Map> target, Set> added) { + for (Map.Entry> entry : source.entrySet()) { + merge(entry.getKey(), entry.getValue(), target, added); + } + } + + private static void mergeTypes( + Map> source, Map> target, Set> added) { + for (Map.Entry> entry : source.entrySet()) { + merge(typeKey(entry.getKey()), entry.getValue(), target, added); + } + } + + private static void merge( + K key, Class generatedClass, Map> target, Set> added) { + Class previous = target.putIfAbsent(key, generatedClass); + if (previous == null) { + added.add(generatedClass); + } else if (previous != generatedClass) { + throw new IllegalStateException("Conflicting generated Fory JSON classes for " + key); + } + } + } + + private static String typeKey(Type type) { + return TypeRef.of(type).getTypeKey(); + } +} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java new file mode 100644 index 0000000000..1c84b0809a --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java @@ -0,0 +1,40 @@ +/* + * 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.apache.fory.json.ForyJson; + +/** + * Supplies Fory JSON configurations for GraalVM Native Image hosted code generation. + * + *

Annotate a reachable public concrete class with a public no-argument constructor. Every + * effective public, non-static, zero-argument instance method whose exact return type is {@link + * ForyJson} is invoked once while the native image is built. This includes inherited superclass + * methods and public interface default methods. The returned configurations select the generated + * object codecs included in the image; configurations not returned by a provider continue to use + * interpreted codecs. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ForyJsonProvider {} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java index 743b359570..7ac3f672bb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/JsonType.java @@ -31,11 +31,12 @@ *

The Fory annotation processor generates a type-owned JSON companion for an eligible concrete * object model or a Record with an effective {@link JsonValue}, together with exact R8 rules for * the model, companion, and codec classes selected by its {@link JsonCodec} declarations. The - * companion provides direct member access and creator invocation on the JVM, Android, and GraalVM - * Native Image. A concrete subtype listed only by a class-literal {@link JsonSubTypes} entry - * receives retention metadata but needs its own direct {@code JsonType} annotation to receive a - * companion. A directly annotated model that reaches the default object codec fails during codec - * creation when its generated companion is missing. + * companion provides direct member access and creator invocation on the JVM and Android. GraalVM + * Native Image discovers this annotation directly and does not use annotation-processor output. A + * concrete subtype listed only by a class-literal {@link JsonSubTypes} entry receives retention + * metadata but needs its own direct {@code JsonType} annotation to receive a companion on the JVM + * or Android. Outside Native Image, a directly annotated model that reaches the default object + * codec fails during codec creation when its generated companion is missing. * *

This annotation does not change the JSON schema and is intentionally not inherited. An * ordinary mutable class may omit it and use reflection, with application-authored exact R8 rules diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index 034a0dea2b..1be0b08435 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -19,6 +19,7 @@ package org.apache.fory.json.codec; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; @@ -54,6 +55,8 @@ import org.apache.fory.json.writer.JsonWriter; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; /** @@ -261,6 +264,16 @@ private static CollectionFactory collectionFactory(Class rawType, Class el } return CollectionFactory.ARRAY_LIST; } + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle constructor = ReflectionUtils.getCtrHandle(rawType, new Class[0]); + return () -> { + try { + return (Collection) constructor.invoke(); + } catch (Throwable e) { + throw new ForyJsonException("Cannot create collection " + rawType, e); + } + }; + } return () -> { try { return (Collection) rawType.newInstance(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java deleted file mode 100644 index 35a0eb15ca..0000000000 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/GeneratedJsonCodecFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 org.apache.fory.annotation.Internal; - -/** Stateless GraalVM runtime factory for a generated JSON companion. */ -@Internal -public interface GeneratedJsonCodecFactory { - GeneratedJsonCodec create(); -} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java index 305df441bf..74b6bdde3e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java @@ -19,6 +19,7 @@ package org.apache.fory.json.codec; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; @@ -46,6 +47,8 @@ import org.apache.fory.json.writer.JsonWriter; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; /** @@ -315,6 +318,16 @@ private static MapFactory mapFactory(Class rawType, Class keyRawType) { } return () -> new LinkedHashMap<>(0); } + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle constructor = ReflectionUtils.getCtrHandle(rawType, new Class[0]); + return () -> { + try { + return (Map) constructor.invoke(); + } catch (Throwable e) { + throw new ForyJsonException("Cannot create map " + rawType, e); + } + }; + } return () -> { try { return (Map) rawType.newInstance(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java index ace30ddc9c..1dc9ac5318 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java @@ -25,6 +25,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; @@ -50,6 +52,7 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.ObjectInstantiator; import org.apache.fory.reflect.TypeRef; @@ -156,6 +159,36 @@ static ObjectCodec createCodec( instantiator); } + /** Prepares one {@code JsonAnySetter} handle for Native Image runtime metadata construction. */ + @Internal + public static void prepareNativeAnySetter(Method method) { + AnyInfo.prepareNativeSetter(method); + } + + /** Freezes all Native Image {@code JsonAnySetter} handles after hosted analysis. */ + @Internal + public static void freezeNativeAnySetters() { + AnyInfo.freezeNativeSetters(); + } + + /** Returns whether hosted metadata must retain this method for object discovery. */ + @Internal + public static boolean usesJsonMetadata(Method method, boolean record) { + return ObjectCodecBuilder.usesJsonMetadata(method, record); + } + + /** Returns whether hosted metadata must retain this method's return type. */ + @Internal + public static boolean usesJsonReturn(Method method) { + return ObjectCodecBuilder.usesJsonReturn(method); + } + + /** Returns whether hosted metadata must retain this method's parameter types. */ + @Internal + public static boolean usesJsonParameters(Method method) { + return ObjectCodecBuilder.usesJsonParameters(method); + } + public final Class type() { return type; } @@ -1392,6 +1425,9 @@ private void writeAnyMembers(Utf8JsonWriter writer, T value, int written) { @Internal public static final class AnyInfo { + private static Map nativeSetterHandles = new HashMap<>(); + private static boolean nativeSetterHandlesFrozen; + private final Field writeField; private final Method writeGetter; private final Field readField; @@ -1566,12 +1602,38 @@ private void put(Object target, String name, Object value) { } private static MethodHandle methodHandle(Method method) { + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle handle = nativeSetterHandles.get(method); + if (handle == null) { + throw new ForyJsonException( + "Missing Native Image Fory JSON Any setter metadata for " + method); + } + return handle; + } try { return _JDKAccess._trustedLookup(method.getDeclaringClass()).unreflect(method); } catch (IllegalAccessException e) { throw new ForyJsonException("Cannot access @JsonAnySetter " + method, e); } } + + private static synchronized void prepareNativeSetter(Method method) { + if (!GraalvmSupport.isGraalBuildTime() || nativeSetterHandlesFrozen) { + throw new IllegalStateException("Fory JSON native Any setter cache is not writable"); + } + nativeSetterHandles.putIfAbsent(method, methodHandle(method)); + } + + private static synchronized void freezeNativeSetters() { + if (nativeSetterHandlesFrozen) { + return; + } + nativeSetterHandles = + nativeSetterHandles.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(nativeSetterHandles)); + nativeSetterHandlesFrozen = true; + } } /** Owns one parameterized POJO binding whose child types differ from the raw-class binding. */ 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 a872d05f91..89e2cfd1fd 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 @@ -63,6 +63,7 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.resolver.JsonSharedRegistry; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.reflect.ObjectInstantiator; import org.apache.fory.reflect.ObjectInstantiators; import org.apache.fory.reflect.TypeRef; @@ -340,7 +341,11 @@ boolean record = annotations) : null; ObjectInstantiator instantiator = - creatorInfo == null ? ObjectInstantiators.createObjectInstantiator(type) : null; + creatorInfo == null + ? GraalvmSupport.isGraalRuntime() + ? ObjectInstantiators.getObjectInstantiator(type) + : ObjectInstantiators.createObjectInstantiator(type) + : null; String[] skipped = hasAny ? skippedNames.toArray(new String[0]) : null; JsonUnwrappedInfo unwrappedInfo = hasUnwrapped diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java index 3344aacbab..80d0f92372 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java @@ -19,6 +19,7 @@ package org.apache.fory.json.codec; +import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.util.Date; import java.util.IdentityHashMap; @@ -28,6 +29,8 @@ 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.platform.GraalvmSupport; +import org.apache.fory.reflect.ReflectionUtils; /** * Optional codecs for {@code java.sql} date/time values represented as epoch milliseconds. @@ -67,13 +70,22 @@ private static Class loadClass(String className) { } private static final class SqlMillisCodec implements JsonValueCodec { + private final Class type; private final Constructor constructor; + private final MethodHandle constructorHandle; private SqlMillisCodec(Class type) { - try { - constructor = type.getConstructor(long.class); - } catch (NoSuchMethodException e) { - throw new ForyJsonException("Cannot access SQL JSON type " + type.getName(), e); + this.type = type; + if (GraalvmSupport.isGraalRuntime()) { + constructor = null; + constructorHandle = ReflectionUtils.getCtrHandle(type, long.class); + } else { + try { + constructor = type.getConstructor(long.class); + constructorHandle = null; + } catch (NoSuchMethodException e) { + throw new ForyJsonException("Cannot access SQL JSON type " + type.getName(), e); + } } } @@ -110,12 +122,16 @@ public T readUtf8(Utf8JsonReader reader) { return reader.tryReadNullToken() ? null : newSqlValue(reader.readLong()); } + @SuppressWarnings("unchecked") private T newSqlValue(long millis) { try { - return constructor.newInstance(millis); + return constructorHandle == null + ? constructor.newInstance(millis) + : (T) constructorHandle.invoke(millis); } catch (ReflectiveOperationException e) { - throw new ForyJsonException( - "Cannot create SQL JSON type " + constructor.getDeclaringClass(), e); + throw new ForyJsonException("Cannot create SQL JSON type " + type, e); + } catch (Throwable e) { + throw new ForyJsonException("Cannot create SQL JSON type " + type, e); } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java index 81a9c2e5e5..a0cb3382e6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java @@ -26,11 +26,15 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; /** @@ -44,6 +48,9 @@ */ @Internal public final class JsonCreatorInfo { + private static Map nativeInvokers = new HashMap<>(); + private static boolean nativeInvokersFrozen; + private final Class ownerType; private final Executable executable; private final JsonCreatorFieldInfo[] fields; @@ -164,6 +171,14 @@ private static MethodHandle buildInvoker( executable.setAccessible(true); return null; } + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle invoker = nativeInvokers.get(executable); + if (invoker == null) { + throw new ForyJsonException( + "Missing Native Image Fory JSON creator metadata for " + executable); + } + return invoker; + } try { MethodHandle target = executable instanceof Constructor @@ -179,4 +194,37 @@ private static MethodHandle buildInvoker( throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e); } } + + /** Prepares one object creator handle for Native Image runtime metadata construction. */ + @Internal + public static synchronized void prepareNativeInvoker(Class ownerType, Executable executable) { + if (!GraalvmSupport.isGraalBuildTime() || nativeInvokersFrozen) { + throw new IllegalStateException("Fory JSON native creator cache is not writable"); + } + nativeInvokers.putIfAbsent( + executable, buildInvoker(ownerType, executable, executable.getParameterCount())); + } + + /** Caches one generated constructor invoker for Native Image runtime metadata construction. */ + @Internal + public static synchronized void prepareNativeConstructor( + Constructor constructor, MethodHandle invoker) { + if (!GraalvmSupport.isGraalBuildTime() || nativeInvokersFrozen) { + throw new IllegalStateException("Fory JSON native creator cache is not writable"); + } + nativeInvokers.putIfAbsent(constructor, invoker); + } + + /** Freezes all Native Image object creator handles after hosted analysis. */ + @Internal + public static synchronized void freezeNativeInvokers() { + if (nativeInvokersFrozen) { + return; + } + nativeInvokers = + nativeInvokers.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(nativeInvokers)); + nativeInvokersFrozen = true; + } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index a6cd06ee17..a988c257ec 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -23,8 +23,13 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; @@ -37,6 +42,11 @@ * generated codecs consume the original field or method metadata and emit direct expressions. */ public abstract class JsonFieldAccessor { + private static Map nativeFields = new HashMap<>(); + private static Map nativeGetters = new HashMap<>(); + private static Map nativeSetters = new HashMap<>(); + private static boolean nativeCachesFrozen; + public Object getObject(Object target) { throw new UnsupportedOperationException(); } @@ -126,17 +136,94 @@ public void putChar(Object target, char value) { } public static JsonFieldAccessor forField(Field field) { + if (GraalvmSupport.isGraalRuntime()) { + return requireNativeAccessor(nativeFields.get(field), field); + } return new FieldJsonAccessor(FieldAccessor.createAccessor(field)); } public static JsonFieldAccessor forGetter(Method getter) { + if (GraalvmSupport.isGraalRuntime()) { + return requireNativeAccessor(nativeGetters.get(getter), getter); + } return new GetterJsonAccessor(getter); } public static JsonFieldAccessor forSetter(Method setter) { + if (GraalvmSupport.isGraalRuntime()) { + return requireNativeAccessor(nativeSetters.get(setter), setter); + } return new SetterJsonAccessor(setter); } + /** Prepares one field accessor for Native Image runtime metadata construction. */ + @Internal + public static synchronized void prepareField(Field field) { + requireNativeBuildTime(); + // Core field access intentionally falls back to Method.invoke for record backing fields in a + // native image. JSON retains the semantic field identity but must cache the component accessor + // MethodHandle here so interpreted codecs never perform runtime reflection. + JsonFieldAccessor accessor = + field.getDeclaringClass().isRecord() + ? new RecordFieldJsonAccessor(field) + : new FieldJsonAccessor(FieldAccessor.createAccessor(field)); + putPrepared(nativeFields, field, accessor); + } + + /** Prepares one getter accessor for Native Image runtime metadata construction. */ + @Internal + public static synchronized void prepareGetter(Method getter) { + requireNativeBuildTime(); + putPrepared(nativeGetters, getter, new GetterJsonAccessor(getter)); + } + + /** Prepares one setter accessor for Native Image runtime metadata construction. */ + @Internal + public static synchronized void prepareSetter(Method setter) { + requireNativeBuildTime(); + putPrepared(nativeSetters, setter, new SetterJsonAccessor(setter)); + } + + /** Freezes all Native Image accessors after hosted analysis. */ + @Internal + public static synchronized void freezeNativeAccessors() { + if (nativeCachesFrozen) { + return; + } + nativeFields = immutable(nativeFields); + nativeGetters = immutable(nativeGetters); + nativeSetters = immutable(nativeSetters); + nativeCachesFrozen = true; + } + + private static void requireNativeBuildTime() { + if (!GraalvmSupport.isGraalBuildTime() || nativeCachesFrozen) { + throw new IllegalStateException("Fory JSON native accessor cache is not writable"); + } + } + + private static void putPrepared( + Map accessors, M member, JsonFieldAccessor accessor) { + JsonFieldAccessor previous = accessors.putIfAbsent(member, accessor); + if (previous != null && previous.getClass() != accessor.getClass()) { + throw new IllegalStateException("Conflicting Fory JSON accessor for " + member); + } + } + + private static Map immutable(Map accessors) { + return accessors.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(accessors)); + } + + private static JsonFieldAccessor requireNativeAccessor( + JsonFieldAccessor accessor, Object member) { + if (accessor == null) { + throw new ForyJsonException("Missing Native Image Fory JSON accessor metadata for " + member); + } + return accessor; + } + private static final class FieldJsonAccessor extends JsonFieldAccessor { private final FieldAccessor accessor; @@ -245,6 +332,34 @@ public void putChar(Object target, char value) { } } + private static final class RecordFieldJsonAccessor extends JsonFieldAccessor { + private final Field field; + private final MethodHandle getterHandle; + + private RecordFieldJsonAccessor(Field field) { + this.field = field; + try { + getterHandle = methodHandle(field.getDeclaringClass().getDeclaredMethod(field.getName())); + } catch (NoSuchMethodException e) { + throw new ForyJsonException("Cannot find JSON record accessor for " + field, e); + } + } + + @Override + public Field field() { + return field; + } + + @Override + public Object getObject(Object target) { + try { + return getterHandle.invoke(target); + } catch (Throwable e) { + throw new ForyJsonException("Cannot access JSON record field " + field, e); + } + } + } + private static final class GetterJsonAccessor extends JsonFieldAccessor { private final Method getter; private final MethodHandle getterHandle; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java index 4e382b3bda..7f272bc46c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java @@ -20,7 +20,6 @@ package org.apache.fory.json.resolver; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.Latin1ReaderCodec; @@ -32,7 +31,7 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.ReflectionUtils; /** Invokes generated codec constructor contracts selected and owned by {@link JsonTypeResolver}. */ final class GeneratedCodecInstantiator { @@ -49,11 +48,7 @@ static StringWriterCodec instantiateStringWriter( return (StringWriterCodec) constructor.newInstance(fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, JsonFieldInfo[].class, StringWriterCodec[].class)); + ReflectionUtils.getCtrHandle(type, JsonFieldInfo[].class, StringWriterCodec[].class); return (StringWriterCodec) constructor.invoke(fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON String writer", e); @@ -75,14 +70,8 @@ static StringWriterCodec instantiateAnyStringWriter( return (StringWriterCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - StringWriterCodec[].class)); + ReflectionUtils.getCtrHandle( + type, ObjectCodec.class, JsonFieldInfo[].class, StringWriterCodec[].class); return (StringWriterCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any String writer", e); @@ -108,15 +97,12 @@ static StringWriterCodec instantiateAnyStringWriter( return (StringWriterCodec) constructor.newInstance(owner, fields, codecs, anyCodec); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - StringWriterCodec[].class, - StringWriterCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldInfo[].class, + StringWriterCodec[].class, + StringWriterCodec.class); return (StringWriterCodec) constructor.invoke(owner, fields, codecs, anyCodec); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any String writer", e); @@ -134,11 +120,7 @@ static Utf8WriterCodec instantiateUtf8Writer( return (Utf8WriterCodec) constructor.newInstance(fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, JsonFieldInfo[].class, Utf8WriterCodec[].class)); + ReflectionUtils.getCtrHandle(type, JsonFieldInfo[].class, Utf8WriterCodec[].class); return (Utf8WriterCodec) constructor.invoke(fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 writer", e); @@ -160,14 +142,8 @@ static Utf8WriterCodec instantiateAnyUtf8Writer( return (Utf8WriterCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - Utf8WriterCodec[].class)); + ReflectionUtils.getCtrHandle( + type, ObjectCodec.class, JsonFieldInfo[].class, Utf8WriterCodec[].class); return (Utf8WriterCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 writer", e); @@ -193,15 +169,12 @@ static Utf8WriterCodec instantiateAnyUtf8Writer( return (Utf8WriterCodec) constructor.newInstance(owner, fields, codecs, anyCodec); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - Utf8WriterCodec[].class, - Utf8WriterCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldInfo[].class, + Utf8WriterCodec[].class, + Utf8WriterCodec.class); return (Utf8WriterCodec) constructor.invoke(owner, fields, codecs, anyCodec); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 writer", e); @@ -223,14 +196,8 @@ static Latin1ReaderCodec instantiateLatin1Reader( return (Latin1ReaderCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - Latin1ReaderCodec[].class)); + ReflectionUtils.getCtrHandle( + type, ObjectCodec.class, JsonFieldInfo[].class, Latin1ReaderCodec[].class); return (Latin1ReaderCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Latin1 reader", e); @@ -259,16 +226,13 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldTable.class, - JsonFieldInfo[].class, - Latin1ReaderCodec[].class, - Latin1ReaderCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldTable.class, + JsonFieldInfo[].class, + Latin1ReaderCodec[].class, + Latin1ReaderCodec.class); return (Latin1ReaderCodec) constructor.invoke(owner, readTable, fields, codecs, selfReader); } catch (Throwable e) { @@ -300,17 +264,14 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldTable.class, - JsonFieldInfo[].class, - Latin1ReaderCodec[].class, - Latin1ReaderCodec.class, - Latin1ReaderCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldTable.class, + JsonFieldInfo[].class, + Latin1ReaderCodec[].class, + Latin1ReaderCodec.class, + Latin1ReaderCodec.class); return (Latin1ReaderCodec) constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec); } catch (Throwable e) { @@ -333,14 +294,8 @@ static Utf16ReaderCodec instantiateUtf16Reader( return (Utf16ReaderCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - Utf16ReaderCodec[].class)); + ReflectionUtils.getCtrHandle( + type, ObjectCodec.class, JsonFieldInfo[].class, Utf16ReaderCodec[].class); return (Utf16ReaderCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF16 reader", e); @@ -369,16 +324,13 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldTable.class, - JsonFieldInfo[].class, - Utf16ReaderCodec[].class, - Utf16ReaderCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldTable.class, + JsonFieldInfo[].class, + Utf16ReaderCodec[].class, + Utf16ReaderCodec.class); return (Utf16ReaderCodec) constructor.invoke(owner, readTable, fields, codecs, selfReader); } catch (Throwable e) { @@ -410,17 +362,14 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldTable.class, - JsonFieldInfo[].class, - Utf16ReaderCodec[].class, - Utf16ReaderCodec.class, - Utf16ReaderCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldTable.class, + JsonFieldInfo[].class, + Utf16ReaderCodec[].class, + Utf16ReaderCodec.class, + Utf16ReaderCodec.class); return (Utf16ReaderCodec) constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec); } catch (Throwable e) { @@ -443,14 +392,8 @@ static Utf8ReaderCodec instantiateUtf8Reader( return (Utf8ReaderCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldInfo[].class, - Utf8ReaderCodec[].class)); + ReflectionUtils.getCtrHandle( + type, ObjectCodec.class, JsonFieldInfo[].class, Utf8ReaderCodec[].class); return (Utf8ReaderCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 reader", e); @@ -466,9 +409,7 @@ static Utf8WriterCodec instantiateUtf8CollectionWriter( constructor.setAccessible(true); return (Utf8WriterCodec) constructor.newInstance(fallback); } - MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor(type, MethodType.methodType(void.class, Utf8WriterCodec.class)); + MethodHandle constructor = ReflectionUtils.getCtrHandle(type, Utf8WriterCodec.class); return (Utf8WriterCodec) constructor.invoke(fallback); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e); @@ -486,10 +427,7 @@ static Utf8WriterCodec instantiateUtf8CollectionWriter( return (Utf8WriterCodec) constructor.newInstance(fallback, elementWriter); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType(void.class, Utf8WriterCodec.class, Utf8WriterCodec.class)); + ReflectionUtils.getCtrHandle(type, Utf8WriterCodec.class, Utf8WriterCodec.class); return (Utf8WriterCodec) constructor.invoke(fallback, elementWriter); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e); @@ -505,9 +443,7 @@ static Utf8ReaderCodec instantiateUtf8CollectionReader( constructor.setAccessible(true); return (Utf8ReaderCodec) constructor.newInstance(elementReader); } - MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor(type, MethodType.methodType(void.class, Utf8ReaderCodec.class)); + MethodHandle constructor = ReflectionUtils.getCtrHandle(type, Utf8ReaderCodec.class); return (Utf8ReaderCodec) constructor.invoke(elementReader); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection reader", e); @@ -536,16 +472,13 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldTable.class, - JsonFieldInfo[].class, - Utf8ReaderCodec[].class, - Utf8ReaderCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldTable.class, + JsonFieldInfo[].class, + Utf8ReaderCodec[].class, + Utf8ReaderCodec.class); return (Utf8ReaderCodec) constructor.invoke(owner, readTable, fields, codecs, selfReader); } catch (Throwable e) { @@ -577,17 +510,14 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = - _JDKAccess._trustedLookup(type) - .findConstructor( - type, - MethodType.methodType( - void.class, - ObjectCodec.class, - JsonFieldTable.class, - JsonFieldInfo[].class, - Utf8ReaderCodec[].class, - Utf8ReaderCodec.class, - Utf8ReaderCodec.class)); + ReflectionUtils.getCtrHandle( + type, + ObjectCodec.class, + JsonFieldTable.class, + JsonFieldInfo[].class, + Utf8ReaderCodec[].class, + Utf8ReaderCodec.class, + Utf8ReaderCodec.class); return (Utf8ReaderCodec) constructor.invoke(owner, readTable, fields, codecs, selfReader, anyCodec); } catch (Throwable e) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java deleted file mode 100644 index f5d479d9f3..0000000000 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedJsonCodecFactories.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.resolver; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.apache.fory.annotation.Internal; -import org.apache.fory.json.ForyJsonException; -import org.apache.fory.json.codec.GeneratedJsonCodecFactory; - -/** Frozen GraalVM runtime reconstruction table for generated JSON companions. */ -@Internal -public final class GeneratedJsonCodecFactories { - private static Map factories = new HashMap<>(); - private static boolean frozen; - - private GeneratedJsonCodecFactories() {} - - /** Publishes one hosted-analysis factory for an exact target and optional Mixin pair. */ - public static synchronized void register( - Class type, Class mixinType, GeneratedJsonCodecFactory factory) { - if (frozen) { - throw new ForyJsonException("Generated JSON codec factory table is already frozen"); - } - Key key = new Key(type, mixinType); - GeneratedJsonCodecFactory previous = factories.get(key); - if (previous == null) { - factories.put(key, factory); - } else if (previous.getClass() != factory.getClass()) { - throw new ForyJsonException( - "Conflicting generated JSON codec factories for " - + type.getName() - + (mixinType == null ? "" : " and " + mixinType.getName()) - + ": " - + previous.getClass().getName() - + " and " - + factory.getClass().getName()); - } - } - - /** Freezes the hosted table into the native image heap. */ - public static synchronized void freeze() { - if (!frozen) { - factories = Collections.unmodifiableMap(new HashMap<>(factories)); - frozen = true; - } - } - - /** Returns the exact pair factory, or {@code null} when no companion was generated. */ - public static synchronized GeneratedJsonCodecFactory get(Class type, Class mixinType) { - return factories.get(new Key(type, mixinType)); - } - - private static final class Key { - private final Class type; - private final Class mixinType; - - private Key(Class type, Class mixinType) { - this.type = type; - this.mixinType = mixinType; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (other == null || getClass() != other.getClass()) { - return false; - } - Key that = (Key) other; - return type == that.type && mixinType == that.mixinType; - } - - @Override - public int hashCode() { - return 31 * System.identityHashCode(type) + System.identityHashCode(mixinType); - } - } -} 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 2fe6f7f735..ce3d5697b6 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 @@ -98,7 +98,10 @@ import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.exception.InsecureException; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.JsonCodegenKey; import org.apache.fory.json.JsonConfig; +import org.apache.fory.json.JsonGeneratedClassRegistry; +import org.apache.fory.json.JsonGeneratedClassRegistry.Configuration; import org.apache.fory.json.JsonTypeCheckContext; import org.apache.fory.json.JsonTypeChecker; import org.apache.fory.json.PropertyNamingStrategy; @@ -110,7 +113,6 @@ import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.GeneratedJsonCodec; -import org.apache.fory.json.codec.GeneratedJsonCodecFactory; import org.apache.fory.json.codec.GuavaCodecs; import org.apache.fory.json.codec.JsonSubTypesInfo; import org.apache.fory.json.codec.JsonValueCodec; @@ -172,6 +174,7 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap typeCheckCache; private final Object typeCheckCacheLock; private final JsonCodegen codegen; + private final JsonCodegenKey nativeCodegenKey; private final boolean asyncCompilationEnabled; private final ExecutorService compilationService; private final boolean propertyDiscoveryEnabled; @@ -202,10 +205,15 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap cachedFieldNames; public JsonSharedRegistry(JsonConfig config) { - this(config, null); + this(config, null, false); } JsonSharedRegistry(JsonConfig config, ExecutorService compilationService) { + this(config, compilationService, false); + } + + private JsonSharedRegistry( + JsonConfig config, ExecutorService compilationService, boolean hostedCodegen) { this.customCodecs = config.codecRegistry().copy(); typeChecker = config.typeChecker(); typeCheckContext = config.typeCheckContext(); @@ -237,12 +245,119 @@ public JsonSharedRegistry(JsonConfig config) { utf8CollectionReaderClasses = new ConcurrentHashMap<>(); cachedFieldNames = new ConcurrentHashMap<>(); boolean codegenEnabled = config.codegenEnabled(); - codegen = codegenEnabled ? new JsonCodegen(config.getCodegenHash(), classLoader) : null; - asyncCompilationEnabled = codegenEnabled && config.asyncCompilationEnabled(); + boolean createCompiler = + codegenEnabled && (hostedCodegen || !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE); + codegen = createCompiler ? new JsonCodegen(config.getCodegenHash(), classLoader) : null; + nativeCodegenKey = + codegenEnabled && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? config.codegenKey() : null; + asyncCompilationEnabled = createCompiler && !hostedCodegen && config.asyncCompilationEnabled(); this.compilationService = compilationService; registerExactCodecs(); } + /** Creates the transient synchronous compiler registry owned by Native Image hosted analysis. */ + @Internal + public static JsonSharedRegistry forHostedCodegen(JsonConfig config) { + if (!GraalvmSupport.isGraalBuildTime()) { + throw new IllegalStateException("Hosted Fory JSON code generation requires image build time"); + } + if (!config.codegenEnabled()) { + throw new IllegalArgumentException("Hosted Fory JSON configuration has codegen disabled"); + } + return new JsonSharedRegistry(config, null, true); + } + + /** Returns a complete immutable snapshot of every synchronously generated class. */ + @Internal + public GeneratedClasses generatedClasses() { + if (codegen == null || asyncCompilationEnabled) { + throw new IllegalStateException("Generated class snapshots require synchronous codegen"); + } + return new GeneratedClasses( + completedClasses(stringWriterClasses), + completedClasses(utf8WriterClasses), + completedClasses(latin1ReaderClasses), + completedClasses(utf16ReaderClasses), + completedClasses(utf8ReaderClasses), + completedClasses(utf8CollectionWriterClasses), + completedClasses(utf8CollectionReaderClasses)); + } + + private static Map> completedClasses( + Map>> futures) { + Map> classes = new HashMap<>(futures.size()); + for (Map.Entry>> entry : futures.entrySet()) { + CompletableFuture> future = entry.getValue(); + if (!future.isDone() || future.isCompletedExceptionally()) { + throw new IllegalStateException( + "Fory JSON generated class is incomplete: " + entry.getKey()); + } + Class generatedClass = future.getNow(null); + if (generatedClass == null) { + throw new IllegalStateException("Fory JSON generated class is null: " + entry.getKey()); + } + classes.put(entry.getKey(), generatedClass); + } + return Collections.unmodifiableMap(classes); + } + + /** Immutable hosted snapshot of generated classes for one configuration. */ + @Internal + public static final class GeneratedClasses { + private final Map, Class> stringWriters; + private final Map, Class> utf8Writers; + private final Map, Class> latin1Readers; + private final Map, Class> utf16Readers; + private final Map, Class> utf8Readers; + private final Map> utf8CollectionWriters; + private final Map> utf8CollectionReaders; + + private GeneratedClasses( + Map, Class> stringWriters, + Map, Class> utf8Writers, + Map, Class> latin1Readers, + Map, Class> utf16Readers, + Map, Class> utf8Readers, + Map> utf8CollectionWriters, + Map> utf8CollectionReaders) { + this.stringWriters = stringWriters; + this.utf8Writers = utf8Writers; + this.latin1Readers = latin1Readers; + this.utf16Readers = utf16Readers; + this.utf8Readers = utf8Readers; + this.utf8CollectionWriters = utf8CollectionWriters; + this.utf8CollectionReaders = utf8CollectionReaders; + } + + public Map, Class> stringWriters() { + return stringWriters; + } + + public Map, Class> utf8Writers() { + return utf8Writers; + } + + public Map, Class> latin1Readers() { + return latin1Readers; + } + + public Map, Class> utf16Readers() { + return utf16Readers; + } + + public Map, Class> utf8Readers() { + return utf8Readers; + } + + public Map> utf8CollectionWriters() { + return utf8CollectionWriters; + } + + public Map> utf8CollectionReaders() { + return utf8CollectionReaders; + } + } + CompletableFuture> stringWriterClass(ObjectCodec owner, JsonTypeResolver resolver) { return generatedClassFuture( stringWriterClasses, owner.type(), () -> codegen.compileStringWriter(owner, resolver)); @@ -284,6 +399,59 @@ CompletableFuture> utf8CollectionReaderClass( () -> codegen.compileUtf8CollectionReader(declaredType, owner)); } + boolean generatedCapabilitiesEnabled() { + return codegen != null || nativeConfiguration() != null; + } + + boolean missingNativeConfiguration() { + return nativeCodegenKey != null && nativeConfiguration() == null; + } + + boolean nativeGeneratedClasses() { + return nativeCodegenKey != null && codegen == null && nativeConfiguration() != null; + } + + Class nativeStringWriterClass(Class type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.stringWriter(type); + } + + Class nativeUtf8WriterClass(Class type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.utf8Writer(type); + } + + Class nativeLatin1ReaderClass(Class type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.latin1Reader(type); + } + + Class nativeUtf16ReaderClass(Class type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.utf16Reader(type); + } + + Class nativeUtf8ReaderClass(Class type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.utf8Reader(type); + } + + Class nativeUtf8CollectionWriterClass(Type type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.utf8CollectionWriter(type); + } + + Class nativeUtf8CollectionReaderClass(Type type) { + Configuration configuration = nativeConfiguration(); + return configuration == null ? null : configuration.utf8CollectionReader(type); + } + + private Configuration nativeConfiguration() { + return nativeCodegenKey == null + ? null + : JsonGeneratedClassRegistry.configuration(nativeCodegenKey); + } + private CompletableFuture> generatedClassFuture( ConcurrentHashMap>> classes, K key, @@ -361,6 +529,9 @@ public boolean matches(int length, long candidateWord0, long candidateWord1) { } GeneratedJsonCodec generatedCodec(Class type) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return null; + } Class mixinType = mixinType(type); boolean directGenerated = type.getDeclaredAnnotation(JsonType.class) != null; if (!directGenerated && mixinType == null) { @@ -418,10 +589,6 @@ private static ForyJsonException missingGeneratedCodec( } private GeneratedJsonCodec loadGeneratedCodec(Class type, Class mixinType) { - if (GraalvmSupport.isGraalRuntime()) { - GeneratedJsonCodecFactory factory = GeneratedJsonCodecFactories.get(type, mixinType); - return factory == null ? null : validateGeneratedCodec(type, factory.create()); - } String generatedName = mixinType == null ? generatedCodecBinaryName(type) @@ -991,7 +1158,8 @@ JsonValueDeclaration valueDeclaration(Class targetType) { } Class mixinType = mixinType(targetType); boolean loadGeneratedCodec = - targetType.getDeclaredAnnotation(JsonType.class) != null || mixinType != null; + !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + && (targetType.getDeclaredAnnotation(JsonType.class) != null || mixinType != null); GeneratedJsonCodec generatedCodec = loadGeneratedCodec ? generatedCodecIfPresent(targetType, mixinType) : null; JsonValueDeclaration resolved = @@ -1161,8 +1329,16 @@ private static MapKeyCodec newMapKeyCodec(Class codecClas return newCodec(codecClass, "JSON map key codec"); } + @SuppressWarnings("unchecked") private static T newCodec(Class codecClass, String role) { validateCodecClass(codecClass, role); + if (GraalvmSupport.isGraalRuntime()) { + try { + return (T) ReflectionUtils.getCtrHandle(codecClass, new Class[0]).invoke(); + } catch (Throwable e) { + throw invalidCodecClass(codecClass, role, "constructor failed", e); + } + } Constructor constructor; try { constructor = codecClass.getConstructor(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java index 1cb5cab47b..e3cadf4e03 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java @@ -25,6 +25,10 @@ import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonValueCodec; @@ -35,10 +39,13 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.reflect.ReflectionUtils; /** Complete String representation selected by one effective {@code JsonValue} member. */ -final class JsonStringValueCodec implements JsonValueCodec { +@Internal +public final class JsonStringValueCodec implements JsonValueCodec { private final Class ownerType; private final JsonFieldAccessor accessor; private final ValueCreator creator; @@ -132,6 +139,9 @@ private Object read(String value) { } private abstract static class ValueCreator { + private static Map nativeInvokers = new HashMap<>(); + private static boolean nativeInvokersFrozen; + final Class ownerType; private ValueCreator(Class ownerType) { @@ -175,11 +185,18 @@ static ValueCreator forExecutable( } private static MethodHandle buildInvoker(Class ownerType, Executable executable) { + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle invoker = nativeInvokers.get(executable); + if (invoker == null) { + throw new ForyJsonException( + "Missing Native Image Fory JSON String creator metadata for " + executable); + } + return invoker; + } try { MethodHandle target = executable instanceof Constructor - ? _JDKAccess._trustedLookup(ownerType) - .unreflectConstructor((Constructor) executable) + ? ReflectionUtils.getCtrHandle(ownerType, executable.getParameterTypes()) : _JDKAccess._trustedLookup(ownerType).unreflect((Method) executable); return target.asType(MethodType.methodType(Object.class, String.class)); } catch (IllegalAccessException e) { @@ -188,6 +205,39 @@ private static MethodHandle buildInvoker(Class ownerType, Executable executab } } + /** Prepares one complete-String creator handle for Native Image runtime metadata construction. */ + @Internal + public static synchronized void prepareNativeCreator(Class ownerType, Executable executable) { + if (!GraalvmSupport.isGraalBuildTime() || ValueCreator.nativeInvokersFrozen) { + throw new IllegalStateException("Fory JSON native String creator cache is not writable"); + } + ValueCreator.nativeInvokers.putIfAbsent( + executable, ValueCreator.buildInvoker(ownerType, executable)); + } + + /** Caches one generated one-String constructor invoker for Native Image runtime use. */ + @Internal + public static synchronized void prepareNativeConstructor( + Constructor constructor, MethodHandle invoker) { + if (!GraalvmSupport.isGraalBuildTime() || ValueCreator.nativeInvokersFrozen) { + throw new IllegalStateException("Fory JSON native String creator cache is not writable"); + } + ValueCreator.nativeInvokers.putIfAbsent(constructor, invoker); + } + + /** Freezes all Native Image complete-String creator handles after hosted analysis. */ + @Internal + public static synchronized void freezeNativeCreators() { + if (ValueCreator.nativeInvokersFrozen) { + return; + } + ValueCreator.nativeInvokers = + ValueCreator.nativeInvokers.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(ValueCreator.nativeInvokers)); + ValueCreator.nativeInvokersFrozen = true; + } + private static final class GeneratedCreator extends ValueCreator { private final GeneratedJsonCodec generatedCodec; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index c908b6f72c..5caa16e6e0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -67,6 +67,8 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.meta.JsonFieldTable; +import org.apache.fory.logging.Logger; +import org.apache.fory.logging.LoggerFactory; import org.apache.fory.reflect.TypeRef; /** @@ -88,6 +90,12 @@ * raw-class JIT dispatch. */ public final class JsonTypeResolver { + private static final Logger LOG = LoggerFactory.getLogger(JsonTypeResolver.class); + private static final String NATIVE_INTERPRETER_MESSAGE = + "Fory JSON is using interpreted codecs because the current configuration was not included " + + "in this native image. Return this configuration from a reachable " + + "@ForyJsonProvider to enable generated codecs."; + private final Map> objectCodecs; private final Map typeInfos; private final JsonSharedRegistry sharedRegistry; @@ -646,7 +654,7 @@ private void endResolution() { } private void completeResolution(ResolutionSnapshot snapshot) { - if (snapshot == null || codegen == null) { + if (snapshot == null) { return; } ArrayList roots = new ArrayList<>(); @@ -655,9 +663,25 @@ private void completeResolution(ResolutionSnapshot snapshot) { roots.add(entry.getValue()); } } - if (!roots.isEmpty()) { - requestCapabilities(roots); + if (roots.isEmpty()) { + return; } + if (!sharedRegistry.generatedCapabilitiesEnabled()) { + if (sharedRegistry.missingNativeConfiguration() && containsObjectModel(roots)) { + LOG.warnOnce(NATIVE_INTERPRETER_MESSAGE); + } + return; + } + requestCapabilities(roots); + } + + private boolean containsObjectModel(ArrayList roots) { + for (int i = 0; i < roots.size(); i++) { + if (canonicalObjectOwner(roots.get(i)) != null) { + return true; + } + } + return false; } private void rollbackResolution(ResolutionSnapshot snapshot) { @@ -1468,9 +1492,39 @@ private boolean reachesReader( } private boolean canCompile(ObjectCodec owner, CapabilityKind kind) { - return kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER - ? codegen.canCompileWriter(owner) - : codegen.canCompileReader(owner); + if (codegen != null) { + return kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER + ? codegen.canCompileWriter(owner) + : codegen.canCompileReader(owner); + } + return nativeObjectClass(owner.type(), kind) != null; + } + + private boolean canCompileCollection(JsonTypeInfo typeInfo, CapabilityKind kind) { + if (codegen != null) { + return true; + } + Type type = typeInfo.type(); + return kind == CapabilityKind.UTF8_WRITER + ? sharedRegistry.nativeUtf8CollectionWriterClass(type) != null + : sharedRegistry.nativeUtf8CollectionReaderClass(type) != null; + } + + private Class nativeObjectClass(Class type, CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + return sharedRegistry.nativeStringWriterClass(type); + case UTF8_WRITER: + return sharedRegistry.nativeUtf8WriterClass(type); + case LATIN1_READER: + return sharedRegistry.nativeLatin1ReaderClass(type); + case UTF16_READER: + return sharedRegistry.nativeUtf16ReaderClass(type); + case UTF8_READER: + return sharedRegistry.nativeUtf8ReaderClass(type); + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } } private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { @@ -1519,6 +1573,22 @@ private CompletableFuture> generatedClass(CapabilityNode node, Capabili } } + private Class nativeGeneratedClass(CapabilityNode node, CapabilityKind kind) { + if (node.subtypeOwner != null) { + throw new IllegalStateException("Inline subtype readers reuse child generated classes"); + } + if (node.collectionOwner != null) { + if (kind == CapabilityKind.UTF8_WRITER) { + return sharedRegistry.nativeUtf8CollectionWriterClass(node.typeInfo.type()); + } + if (kind == CapabilityKind.UTF8_READER) { + return sharedRegistry.nativeUtf8CollectionReaderClass(node.typeInfo.type()); + } + throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); + } + return nativeObjectClass(node.objectOwner.type(), kind); + } + private Object newCapability( CapabilityNode node, Class generatedClass, @@ -1719,6 +1789,11 @@ private void requestCapabilities(ArrayList roots) { } private void requestGraph(CapabilityGraph graph) { + if (sharedRegistry.nativeGeneratedClasses()) { + graph.loadNativeClasses(); + graph.publish(); + return; + } jitContext.registerJITFuture( () -> graph.classesReady().thenApply(ignored -> graph), new JsonJITContext.JITCallback() { @@ -1845,7 +1920,7 @@ private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { return existing.complete; } JsonTypeInfo element = declaredCollectionElement(typeInfo); - if (element == null) { + if (element == null || !canCompileCollection(typeInfo, kind)) { return false; } CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); @@ -1871,6 +1946,19 @@ private CompletableFuture classesReady() { return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } + private void loadNativeClasses() { + for (int i = 0; i < ordered.size(); i++) { + CapabilityNode node = ordered.get(i); + if (node.subtypeOwner == null) { + node.generatedClass = nativeGeneratedClass(node, kind); + if (node.generatedClass == null) { + throw new IllegalStateException( + "Missing generated Fory JSON class for " + node.typeInfo.type()); + } + } + } + } + private void publish() { requireJITLock(); IdentityMap capabilities = new IdentityMap<>(); @@ -1889,7 +1977,8 @@ private void publish() { } Class generatedClass = null; if (node.subtypeOwner == null) { - generatedClass = node.classFuture.getNow(null); + generatedClass = + node.generatedClass == null ? node.classFuture.getNow(null) : node.generatedClass; if (generatedClass == null) { throw new IllegalStateException("Generated JSON class is not ready"); } @@ -1921,6 +2010,7 @@ private final class CapabilityNode { private final Object initial; private boolean complete; private CompletableFuture> classFuture; + private Class generatedClass; private Object instance; private CapabilityNode(JsonTypeInfo typeInfo, ObjectCodec owner, Object initial) { 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/ForyJsonGraalVMFeature.java similarity index 57% rename from java/fory-json/src/main/java17/org/apache/fory/json/codec/ForyJsonGraalVMFeature.java rename to java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 1f99b4cba4..a50f0dceab 100644 --- 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/ForyJsonGraalVMFeature.java @@ -17,29 +17,39 @@ * under the License. */ -package org.apache.fory.json.codec; +package org.apache.fory.json; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; +import java.lang.reflect.InvocationTargetException; 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.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; 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.ForyJsonProvider; +import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonBase64; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.annotation.JsonCreator; @@ -48,17 +58,24 @@ import org.apache.fory.json.annotation.JsonType; import org.apache.fory.json.annotation.JsonUnwrapped; import org.apache.fory.json.annotation.JsonValue; -import org.apache.fory.json.resolver.GeneratedJsonCodecFactories; +import org.apache.fory.json.codec.Base64ByteArrayCodec; +import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.meta.JsonCreatorInfo; +import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry.JsonMixinView; +import org.apache.fory.json.resolver.JsonStringValueCodec; +import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.reflect.ObjectInstantiators; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; +import org.apache.fory.util.record.RecordUtils; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; import org.graalvm.nativeimage.hosted.RuntimeReflection; -/** Registers reachable Fory JSON models for GraalVM native image reflection. */ +/** Prepares reachable Fory JSON models and provider-selected codecs for Native Image. */ final class ForyJsonGraalVMFeature implements Feature { private static final String[] SQL_TYPES = { "java.sql.Date", "java.sql.Time", "java.sql.Timestamp" @@ -69,22 +86,36 @@ final class ForyJsonGraalVMFeature implements Feature { private final Set> processedDeclarations = ConcurrentHashMap.newKeySet(); private final Set> processedModels = ConcurrentHashMap.newKeySet(); private final Set> processedMixins = ConcurrentHashMap.newKeySet(); + private final Map, Set>> reachableMixins = new LinkedHashMap<>(); + private final Set> processedProviders = ConcurrentHashMap.newKeySet(); private final Set> processedCodecs = ConcurrentHashMap.newKeySet(); private final Set> processedContainers = ConcurrentHashMap.newKeySet(); + private final Map> creators = new LinkedHashMap<>(); + private final Set preparedCreators = new LinkedHashSet<>(); + private final Set> generatedConstructors = new LinkedHashSet<>(); + private final Set> preparedGeneratedConstructors = new LinkedHashSet<>(); + private final Map hostedConfigurations = + new LinkedHashMap<>(); + private final Set processedGenerations = new LinkedHashSet<>(); @Override public String getDescription() { - return "Registers reachable Fory JSON models for GraalVM native image"; + return "Prepares reachable Fory JSON models for GraalVM Native Image"; } @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeClassInitialization.initializeAtBuildTime(GeneratedJsonCodecFactories.class); - // Exact target-Mixin keys are retained in the frozen factory table and therefore become image - // heap objects together with the table. Derive the private key name from its enclosing class so - // relocated/shaded packages remain valid. + RuntimeClassInitialization.initializeAtBuildTime(JsonCodegenKey.class); + RuntimeClassInitialization.initializeAtBuildTime(JsonGeneratedClassRegistry.class); RuntimeClassInitialization.initializeAtBuildTime( - GeneratedJsonCodecFactories.class.getName() + "$Key"); + JsonGeneratedClassRegistry.class.getDeclaredClasses()); + RuntimeClassInitialization.initializeAtBuildTime(JsonFieldAccessor.class); + RuntimeClassInitialization.initializeAtBuildTime(JsonFieldAccessor.class.getDeclaredClasses()); + RuntimeClassInitialization.initializeAtBuildTime(JsonCreatorInfo.class); + RuntimeClassInitialization.initializeAtBuildTime(JsonStringValueCodec.class); + RuntimeClassInitialization.initializeAtBuildTime( + JsonStringValueCodec.class.getDeclaredClasses()); + RuntimeClassInitialization.initializeAtBuildTime(ObjectCodec.AnyInfo.class); // Hosted Mixin discovery deliberately reuses the runtime's structural resolver so build-time // reachability and runtime semantics cannot drift. Its static state contains only the immutable // supported-annotation set and is therefore safe to initialize in the image builder. @@ -102,8 +133,10 @@ public void duringAnalysis(DuringAnalysisAccess access) { if (!reachableTypes.contains(ForyJson.class)) { return; } - boolean changed = false; - for (Class type : reachableTypes) { + boolean changed = prepareNativeHandles(); + List> orderedTypes = new ArrayList<>(reachableTypes); + orderedTypes.sort(Comparator.comparing(Class::getName)); + for (Class type : orderedTypes) { if (processedReachableTypes.add(type)) { changed |= registerContainer(type); changed |= registerDeclarations(type); @@ -114,17 +147,218 @@ public void duringAnalysis(DuringAnalysisAccess access) { if (mixin != null) { changed |= registerMixin(access, type, mixin.target()); } + if (type.getDeclaredAnnotation(ForyJsonProvider.class) != null) { + changed |= registerProvider(type); + } if (type == ForyJson.class) { registerBuiltInTypes(access); changed = true; } } } + changed |= generateConfigurations(access); if (changed) { access.requireAnalysisIteration(); } } + private boolean registerProvider(Class providerClass) { + if (!processedProviders.add(providerClass)) { + return false; + } + int modifiers = providerClass.getModifiers(); + if (!Modifier.isPublic(modifiers) + || Modifier.isAbstract(modifiers) + || providerClass.isInterface() + || providerClass.isEnum()) { + throw providerFailure( + providerClass, "must be a public concrete class", null); + } + Constructor constructor; + try { + constructor = providerClass.getConstructor(); + } catch (NoSuchMethodException e) { + throw providerFailure(providerClass, "must have a public no-argument constructor", e); + } + Object provider; + try { + provider = constructor.newInstance(); + } catch (ReflectiveOperationException | RuntimeException e) { + throw providerFailure(providerClass, "cannot be constructed", unwrap(e)); + } + List methods = providerMethods(providerClass); + if (methods.isEmpty()) { + throw providerFailure(providerClass, "does not declare an effective ForyJson method", null); + } + boolean changed = false; + for (Method method : methods) { + if (Modifier.isStatic(method.getModifiers()) || method.getParameterCount() != 0) { + throw providerFailure( + providerClass, + "method must be a non-static zero-argument instance method: " + method, + null); + } + ForyJson json; + try { + json = (ForyJson) method.invoke(provider); + } catch (ReflectiveOperationException | RuntimeException e) { + throw providerFailure( + providerClass, "cannot invoke provider method " + method, unwrap(e)); + } + if (json == null) { + throw providerFailure(providerClass, "provider method returned null: " + method, null); + } + JsonConfig config = json.config(); + if (!config.codegenEnabled()) { + throw providerFailure( + providerClass, "provider method returned a codegen-disabled ForyJson: " + method, null); + } + JsonCodegenKey key = config.codegenKey(); + if (!hostedConfigurations.containsKey(key)) { + hostedConfigurations.put(key, new HostedConfiguration(config)); + changed = true; + } + } + return changed; + } + + private static List providerMethods(Class providerClass) { + Map effective = new HashMap<>(); + for (Method method : providerClass.getMethods()) { + if (method.isBridge() + || method.isSynthetic() + || method.getReturnType() != ForyJson.class) { + continue; + } + MethodSignature signature = new MethodSignature(method); + Method previous = effective.get(signature); + if (previous == null + || previous.getDeclaringClass().isAssignableFrom(method.getDeclaringClass())) { + effective.put(signature, method); + } else if (!method.getDeclaringClass().isAssignableFrom(previous.getDeclaringClass()) + && method.getDeclaringClass().getName().compareTo(previous.getDeclaringClass().getName()) + < 0) { + effective.put(signature, method); + } + } + ArrayList methods = new ArrayList<>(effective.values()); + methods.sort( + Comparator.comparing(Method::getName) + .thenComparing(method -> method.getDeclaringClass().getName()) + .thenComparing(Method::toGenericString)); + return methods; + } + + private boolean generateConfigurations(DuringAnalysisAccess access) { + boolean changed = false; + for (Map.Entry entry : + hostedConfigurations.entrySet()) { + HostedConfiguration configuration = entry.getValue(); + LinkedHashSet> selectedModels = new LinkedHashSet<>(processedModels); + for (Map.Entry, Set>> mixin : reachableMixins.entrySet()) { + if (mixin.getValue().contains(configuration.mixins.get(mixin.getKey()))) { + selectedModels.add(mixin.getKey()); + } + } + ArrayList> models = new ArrayList<>(selectedModels); + models.sort(Comparator.comparing(Class::getName)); + for (Class model : models) { + GenerationKey generation = new GenerationKey(entry.getKey(), model); + if (!processedGenerations.add(generation)) { + continue; + } + try { + configuration.resolver.getTypeInfo(model, model); + } catch (RuntimeException | LinkageError e) { + throw new IllegalStateException( + "Cannot generate Fory JSON codecs for " + model.getName(), e); + } + Set> generatedClasses = + JsonGeneratedClassRegistry.register( + entry.getKey(), configuration.registry.generatedClasses()); + for (Class generatedClass : generatedClasses) { + registerGeneratedClass(generatedClass); + } + changed = true; + } + } + return changed; + } + + private void registerGeneratedClass(Class generatedClass) { + // Generated codecs contain only instance state and are defined during analysis, after native + // image class-initialization policy has been fixed. + RuntimeReflection.register(generatedClass); + Constructor[] constructors = generatedClass.getDeclaredConstructors(); + if (constructors.length == 0) { + throw new IllegalStateException( + "Generated Fory JSON class has no constructor: " + generatedClass.getName()); + } + RuntimeReflection.register(constructors); + for (Constructor constructor : constructors) { + RuntimeReflection.registerConstructorLookup( + generatedClass, constructor.getParameterTypes()); + generatedConstructors.add(constructor); + } + } + + private boolean prepareNativeHandles() { + boolean changed = false; + for (Map.Entry> entry : creators.entrySet()) { + Executable executable = entry.getKey(); + if (!preparedCreators.add(executable)) { + continue; + } + Class ownerType = entry.getValue(); + boolean stringCreator = + executable.getParameterCount() == 1 + && executable.getParameterTypes()[0] == String.class; + if (executable instanceof Constructor) { + JsonCreatorCodegen.Invokers invokers = + JsonCreatorCodegen.create((Constructor) executable); + RuntimeReflection.register(invokers.type); + RuntimeReflection.register(invokers.creatorMethod); + JsonCreatorInfo.prepareNativeConstructor( + (Constructor) executable, invokers.creator); + if (stringCreator) { + RuntimeReflection.register(invokers.stringMethod); + JsonStringValueCodec.prepareNativeConstructor( + (Constructor) executable, invokers.stringCreator); + } + } else { + JsonCreatorInfo.prepareNativeInvoker(ownerType, executable); + if (stringCreator) { + JsonStringValueCodec.prepareNativeCreator(ownerType, executable); + } + } + RuntimeReflection.register(executable); + changed = true; + } + for (Constructor constructor : generatedConstructors) { + if (preparedGeneratedConstructors.add(constructor)) { + ReflectionUtils.getCtrHandle( + constructor.getDeclaringClass(), constructor.getParameterTypes()); + RuntimeReflection.register(constructor); + changed = true; + } + } + return changed; + } + + private static IllegalStateException providerFailure( + Class providerClass, String reason, Throwable cause) { + String message = "Invalid @ForyJsonProvider " + providerClass.getName() + ": " + reason; + return cause == null + ? new IllegalStateException(message) + : new IllegalStateException(message, cause); + } + + private static Throwable unwrap(Throwable throwable) { + return throwable instanceof InvocationTargetException && throwable.getCause() != null + ? throwable.getCause() + : throwable; + } + private boolean registerModel(DuringAnalysisAccess access, Class type) { if (!processedModels.add(type)) { return false; @@ -136,11 +370,15 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { && !Collection.class.isAssignableFrom(type) && !Map.class.isAssignableFrom(type)) { registerModelHierarchy(access, type); - if (!type.isRecord() && GraalvmSupport.needReflectionRegisterForCreation(type)) { - RuntimeReflection.registerForReflectiveInstantiation(type); + if (type.isRecord()) { + prepareRecord(type); + } else if (!Modifier.isAbstract(type.getModifiers())) { + ObjectInstantiators.getObjectInstantiator(type); + if (GraalvmSupport.needReflectionRegisterForCreation(type)) { + RuntimeReflection.registerForReflectiveInstantiation(type); + } } } - registerGeneratedCodec(access, type, null); registerSubtypes(access, type); return true; } @@ -150,6 +388,7 @@ private boolean registerMixin( if (!processedMixins.add(mixinType)) { return false; } + reachableMixins.computeIfAbsent(targetType, ignored -> new LinkedHashSet<>()).add(mixinType); JsonMixinView annotations = JsonSharedRegistry.resolveMixin(targetType, mixinType); RuntimeReflection.register(mixinType); if (annotations.isEmpty()) { @@ -179,12 +418,15 @@ private boolean registerMixin( // effective annotations visible here as hosted reachability facts. if (!intrinsicTarget && !hasTypeCodec && !hasJsonValue && subTypes == null) { registerModelHierarchy(access, targetType, annotations); - if (!targetType.isRecord() - && GraalvmSupport.needReflectionRegisterForCreation(targetType)) { - RuntimeReflection.registerForReflectiveInstantiation(targetType); + if (targetType.isRecord()) { + prepareRecord(targetType); + } else if (!Modifier.isAbstract(targetType.getModifiers())) { + ObjectInstantiators.getObjectInstantiator(targetType); + if (GraalvmSupport.needReflectionRegisterForCreation(targetType)) { + RuntimeReflection.registerForReflectiveInstantiation(targetType); + } } } - registerGeneratedCodec(access, targetType, mixinType); if (!hasTypeCodec && !hasJsonValue) { registerSubtypes(access, targetType, annotations); } @@ -226,6 +468,9 @@ private boolean registerJsonValueDeclarations( if (annotation(annotations, field, JsonValue.class) != null) { hasValue = true; RuntimeReflection.register(field); + // JsonValueDeclaration deliberately coalesces a Record component's propagated field and + // accessor annotations to the backing field on the interpreted path. + JsonFieldAccessor.prepareField(field); if (Runtime.version().feature() <= 24) { access.registerAsUnsafeAccessed(field); } @@ -237,6 +482,7 @@ private boolean registerJsonValueDeclarations( if (annotation(annotations, method, JsonValue.class) != null) { hasValue = true; RuntimeReflection.register(method); + JsonFieldAccessor.prepareGetter(method); registerOccurrenceCodecs(annotations, method); } } @@ -248,6 +494,7 @@ private boolean registerJsonValueDeclarations( && annotation(annotations, method, JsonValue.class) != null) { hasValue = true; RuntimeReflection.register(method); + JsonFieldAccessor.prepareGetter(method); registerOccurrenceCodecs(annotations, method); } } @@ -255,14 +502,17 @@ && annotation(annotations, method, JsonValue.class) != null) { if (!hasValue) { return false; } + if (type.isRecord()) { + prepareRecord(type); + } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - RuntimeReflection.register(constructor); + registerCreator(type, constructor); } } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - RuntimeReflection.register(method); + registerCreator(type, method); } } return true; @@ -282,61 +532,13 @@ private void registerReflectiveDeclarations(Set declarations) } } - private void registerGeneratedCodec( - DuringAnalysisAccess access, Class type, Class mixinType) { - String codecName = - mixinType == null - ? JsonSharedRegistry.generatedCodecBinaryName(type) - : JsonSharedRegistry.generatedMixinCodecBinaryName(mixinType, type); - Class codecClass = access.findClassByName(codecName); - if (codecClass == null) { - return; - } - if (!GeneratedJsonCodec.class.isAssignableFrom(codecClass)) { - throw new IllegalStateException( - codecName + " does not extend " + GeneratedJsonCodec.class.getName()); - } - try { - if (!Modifier.isPublic(codecClass.getModifiers())) { - throw new IllegalStateException("Generated JSON codec must be public: " + codecName); - } - codecClass.getConstructor(); - } catch (NoSuchMethodException e) { - throw new IllegalStateException( - "Generated JSON codec must have a public no-argument constructor: " + codecName, e); - } - String factoryName = codecName + "$Factory"; - Class factoryClass = access.findClassByName(factoryName); - if (factoryClass == null || !GeneratedJsonCodecFactory.class.isAssignableFrom(factoryClass)) { - throw new IllegalStateException( - "Missing generated JSON codec factory " + factoryName + " for " + type.getName()); - } - int modifiers = factoryClass.getModifiers(); - if (!Modifier.isPublic(modifiers) - || !Modifier.isStatic(modifiers) - || !Modifier.isFinal(modifiers)) { - throw new IllegalStateException( - "Generated JSON codec factory must be public static final: " + factoryName); - } - try { - GeneratedJsonCodecFactory factory = - (GeneratedJsonCodecFactory) ReflectionUtils.getCtrHandle(factoryClass).invoke(); - GeneratedJsonCodec codec = - JsonSharedRegistry.validateGeneratedCodec(type, factory.create()); - if (codec.validatedRecord() != type.isRecord()) { - throw new IllegalStateException( - "Generated JSON codec Record metadata does not match " + type.getName()); - } - GeneratedJsonCodecFactories.register(type, mixinType, factory); - } catch (Throwable e) { - throw new IllegalStateException( - "Cannot initialize generated JSON codec factory " + factoryName, e); - } - } - @Override public void afterAnalysis(AfterAnalysisAccess access) { - GeneratedJsonCodecFactories.freeze(); + JsonGeneratedClassRegistry.freeze(); + JsonFieldAccessor.freezeNativeAccessors(); + JsonCreatorInfo.freezeNativeInvokers(); + JsonStringValueCodec.freezeNativeCreators(); + ObjectCodec.freezeNativeAnySetters(); } private void registerModelHierarchy(DuringAnalysisAccess access, Class type) { @@ -350,16 +552,17 @@ boolean record = type.isRecord(); for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { - // ObjectCodecBuilder still reads semantic annotations, generic types, and exact creator - // signatures at image runtime. Register that metadata here instead of routing JSON models - // through the core Fory feature, whose Record registration initializes RecordUtils and would - // reintroduce the native-Record path that generated JSON companions replace. + // Runtime configurations still select members from semantic reflection metadata. Accessor + // construction itself is hosted and cached below. RuntimeReflection.register(current); RuntimeReflection.register(current.getDeclaredFields()); RuntimeReflection.register(current.getDeclaredMethods()); RuntimeReflection.register(current.getDeclaredConstructors()); for (Field field : current.getDeclaredFields()) { if (isJsonField(field)) { + if (!record) { + JsonFieldAccessor.prepareField(field); + } if (!current.isRecord() && Runtime.version().feature() <= 24) { access.registerAsUnsafeAccessed(field); } @@ -371,15 +574,20 @@ boolean record = type.isRecord(); } } } + for (Method method : current.getDeclaredMethods()) { + if (annotation(annotations, method, JsonValue.class) != null) { + prepareMethodAccessors(annotations, method); + } + } } for (Method method : type.getMethods()) { boolean mixinSelector = hasMixinSelector(annotations, method); - if (ObjectCodecBuilder.usesJsonMetadata(method, record) - || mixinSelector) { + if (ObjectCodec.usesJsonMetadata(method, record) || mixinSelector) { + prepareMethodAccessors(annotations, method); if (method.getDeclaringClass().isInterface()) { RuntimeReflection.register(method); } - if (ObjectCodecBuilder.usesJsonReturn(method) + if (ObjectCodec.usesJsonReturn(method) || mixinSelector && method.getReturnType() != void.class) { registerOccurrenceCodecs(annotations, method); Type resolvedType = ownerType.resolveType(method.getGenericReturnType()).getType(); @@ -388,7 +596,7 @@ boolean record = type.isRecord(); registerNestedModel(access, resolvedType); } } - if (ObjectCodecBuilder.usesJsonParameters(method) + if (ObjectCodec.usesJsonParameters(method) || mixinSelector && method.getParameterCount() != 0) { registerParameterCodecs(annotations, method.getParameters()); registerResolvedParameterTypes(ownerType, method.getParameters()); @@ -397,8 +605,8 @@ boolean record = type.isRecord(); } } for (Constructor constructor : type.getDeclaredConstructors()) { - if (annotation(annotations, constructor, JsonCreator.class) != null - || hasMixinSelector(annotations, constructor)) { + if (annotation(annotations, constructor, JsonCreator.class) != null) { + registerCreator(type, constructor); registerParameterCodecs(annotations, constructor.getParameters()); registerResolvedParameterTypes(ownerType, constructor.getParameters()); registerUnwrappedParameters( @@ -406,8 +614,8 @@ boolean record = type.isRecord(); } } for (Method method : type.getDeclaredMethods()) { - if (annotation(annotations, method, JsonCreator.class) != null - || hasMixinSelector(annotations, method)) { + if (annotation(annotations, method, JsonCreator.class) != null) { + registerCreator(type, method); registerParameterCodecs(annotations, method.getParameters()); registerResolvedParameterTypes(ownerType, method.getParameters()); registerUnwrappedParameters(access, ownerType, annotations, method.getParameters()); @@ -415,6 +623,47 @@ boolean record = type.isRecord(); } } + private void prepareRecord(Class type) { + RuntimeReflection.registerAllRecordComponents(type); + RecordUtils.prepareRecordComponentGetters(type); + for (RecordComponent component : type.getRecordComponents()) { + JsonFieldAccessor.prepareGetter(component.getAccessor()); + } + Constructor constructor = RecordUtils.getRecordConstructor(type).f0; + registerCreator(type, constructor); + } + + private void registerCreator(Class ownerType, Executable executable) { + RuntimeReflection.register(executable); + if (executable instanceof Constructor) { + RuntimeReflection.registerConstructorLookup(ownerType, executable.getParameterTypes()); + } else { + Method method = (Method) executable; + RuntimeReflection.registerMethodLookup( + method.getDeclaringClass(), method.getName(), method.getParameterTypes()); + } + Class previous = creators.putIfAbsent(executable, ownerType); + if (previous != null && previous != ownerType) { + throw new IllegalStateException( + "Conflicting Fory JSON creator owners for " + executable); + } + } + + private static void prepareMethodAccessors(JsonMixinView annotations, Method method) { + int modifiers = method.getModifiers(); + if (Modifier.isStatic(modifiers) || method.isSynthetic() || method.isBridge()) { + return; + } + if (method.getParameterCount() == 0 && method.getReturnType() != void.class) { + JsonFieldAccessor.prepareGetter(method); + } else if (method.getParameterCount() == 1 && method.getReturnType() == void.class) { + JsonFieldAccessor.prepareSetter(method); + } + if (annotation(annotations, method, JsonAnySetter.class) != null) { + ObjectCodec.prepareNativeAnySetter(method); + } + } + private void registerNestedModel(DuringAnalysisAccess access, Type type) { Class rawType = rawType(type); if (rawType != null && rawType != Object.class) { @@ -555,7 +804,9 @@ private void registerCodec(Class codecClass) { } RuntimeReflection.register(codecClass); try { - RuntimeReflection.register(codecClass.getConstructor()); + Constructor constructor = codecClass.getConstructor(); + RuntimeReflection.register(constructor); + ReflectionUtils.getCtrHandle(codecClass, new Class[0]); } catch (NoSuchMethodException e) { throw new IllegalStateException( "JSON codec class must have a public no-argument constructor: " + codecClass.getName(), @@ -573,7 +824,9 @@ private boolean registerContainer(Type type) { return false; } try { - RuntimeReflection.register(rawType.getConstructor()); + Constructor constructor = rawType.getConstructor(); + RuntimeReflection.register(constructor); + ReflectionUtils.getCtrHandle(rawType, new Class[0]); return true; } catch (NoSuchMethodException ignored) { // CollectionCodec and MapCodec preserve the same runtime failure for a concrete container @@ -606,7 +859,9 @@ private void registerSqlTypes(DuringAnalysisAccess access) { if (type != null) { RuntimeReflection.register(type); try { - RuntimeReflection.register(type.getConstructor(long.class)); + Constructor constructor = type.getConstructor(long.class); + RuntimeReflection.register(constructor); + ReflectionUtils.getCtrHandle(type, long.class); } catch (NoSuchMethodException e) { throw new IllegalStateException("Missing Fory JSON SQL constructor for " + className, e); } @@ -658,4 +913,70 @@ private static Class rawType(Type type) { } return null; } + + private static final class HostedConfiguration { + private final JsonSharedRegistry registry; + private final JsonTypeResolver resolver; + private final Map, Class> mixins; + + private HostedConfiguration(JsonConfig config) { + registry = JsonSharedRegistry.forHostedCodegen(config); + resolver = new JsonTypeResolver(registry); + mixins = config.mixins(); + } + } + + private static final class MethodSignature { + private final String name; + private final Class[] parameterTypes; + + private MethodSignature(Method method) { + name = method.getName(); + parameterTypes = method.getParameterTypes(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodSignature)) { + return false; + } + MethodSignature that = (MethodSignature) other; + return name.equals(that.name) && Arrays.equals(parameterTypes, that.parameterTypes); + } + + @Override + public int hashCode() { + return 31 * name.hashCode() + Arrays.hashCode(parameterTypes); + } + } + + private static final class GenerationKey { + private final JsonCodegenKey configuration; + private final Class model; + + private GenerationKey(JsonCodegenKey configuration, Class model) { + this.configuration = configuration; + this.model = model; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof GenerationKey)) { + return false; + } + GenerationKey that = (GenerationKey) other; + return configuration.equals(that.configuration) && model == that.model; + } + + @Override + public int hashCode() { + return 31 * configuration.hashCode() + System.identityHashCode(model); + } + } } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java b/java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java new file mode 100644 index 0000000000..b7e44597d4 --- /dev/null +++ b/java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java @@ -0,0 +1,442 @@ +/* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.fory.platform.internal.DefineClass; +import org.apache.fory.platform.internal._JDKAccess; + +/** Generates direct constructor invokers while Native Image analysis is still mutable. */ +final class JsonCreatorCodegen { + private static final int ACC_PUBLIC = 0x0001; + private static final int ACC_STATIC = 0x0008; + private static final int ACC_FINAL = 0x0010; + private static final int ACC_SUPER = 0x0020; + private static final int ACC_SYNTHETIC = 0x1000; + + private JsonCreatorCodegen() {} + + static Invokers create(Constructor constructor) { + Class ownerType = constructor.getDeclaringClass(); + Class invokerClass = + DefineClass.defineHiddenNestmate(ownerType, new ClassBytes(constructor).build()); + try { + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(invokerClass); + MethodHandle creator = + lookup.findStatic( + invokerClass, + "invoke", + MethodType.methodType(Object.class, Object[].class)); + Method creatorMethod = invokerClass.getDeclaredMethod("invoke", Object[].class); + if (constructor.getParameterCount() != 1 + || constructor.getParameterTypes()[0] != String.class) { + return new Invokers(invokerClass, creator, creatorMethod, null, null); + } + MethodHandle stringCreator = + lookup.findStatic( + invokerClass, + "invoke", + MethodType.methodType(Object.class, String.class)); + Method stringMethod = invokerClass.getDeclaredMethod("invoke", String.class); + return new Invokers(invokerClass, creator, creatorMethod, stringCreator, stringMethod); + } catch (ReflectiveOperationException cause) { + throw new IllegalStateException( + "Cannot resolve generated Fory JSON creator for " + constructor, cause); + } + } + + static final class Invokers { + final Class type; + final MethodHandle creator; + final Method creatorMethod; + final MethodHandle stringCreator; + final Method stringMethod; + + private Invokers( + Class type, + MethodHandle creator, + Method creatorMethod, + MethodHandle stringCreator, + Method stringMethod) { + this.type = type; + this.creator = creator; + this.creatorMethod = creatorMethod; + this.stringCreator = stringCreator; + this.stringMethod = stringMethod; + } + } + + private static final class ClassBytes { + private static final String OBJECT = "java/lang/Object"; + private static final String OBJECT_ARRAY_INVOKE = "([Ljava/lang/Object;)Ljava/lang/Object;"; + private static final String STRING_INVOKE = "(Ljava/lang/String;)Ljava/lang/Object;"; + + private final Constructor constructor; + private final Class[] parameterTypes; + private final String owner; + private final String generatedName; + private final boolean stringCreator; + private final ConstantPool constants = new ConstantPool(); + + private ClassBytes(Constructor constructor) { + this.constructor = constructor; + parameterTypes = constructor.getParameterTypes(); + owner = internalName(constructor.getDeclaringClass()); + generatedName = owner + "$$ForyJsonCreator"; + stringCreator = parameterTypes.length == 1 && parameterTypes[0] == String.class; + } + + private byte[] build() { + try { + int thisClass = constants.classInfo(generatedName); + int superClass = constants.classInfo(OBJECT); + MethodCode arrayInvoker = arrayInvoker(); + MethodCode stringInvoker = stringCreator ? stringInvoker() : null; + arrayInvoker.register(constants); + if (stringCreator) { + stringInvoker.register(constants); + } + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(bytes); + output.writeInt(0xCAFEBABE); + output.writeShort(0); + output.writeShort(52); + constants.write(output); + output.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER | ACC_SYNTHETIC); + output.writeShort(thisClass); + output.writeShort(superClass); + output.writeShort(0); + output.writeShort(0); + output.writeShort(stringCreator ? 2 : 1); + arrayInvoker.write(output, constants); + if (stringCreator) { + stringInvoker.write(output, constants); + } + output.writeShort(0); + output.flush(); + return bytes.toByteArray(); + } catch (IOException cause) { + throw new IllegalStateException("Cannot generate Fory JSON creator for " + constructor, cause); + } + } + + private MethodCode arrayInvoker() throws IOException { + Code code = new Code(constants); + code.type(0xbb, owner); + code.op(0x59); + for (int i = 0; i < parameterTypes.length; i++) { + code.op(0x2a); + code.integer(i); + code.op(0x32); + code.convert(parameterTypes[i]); + } + code.method(0xb7, owner, "", constructorDescriptor(parameterTypes)); + code.op(0xb0); + return new MethodCode( + ACC_PUBLIC | ACC_STATIC, + "invoke", + OBJECT_ARRAY_INVOKE, + 4 + parameterTypes.length * 2, + 1, + code.bytes()); + } + + private MethodCode stringInvoker() throws IOException { + Code code = new Code(constants); + code.type(0xbb, owner); + code.op(0x59); + code.op(0x2a); + code.method(0xb7, owner, "", constructorDescriptor(parameterTypes)); + code.op(0xb0); + return new MethodCode( + ACC_PUBLIC | ACC_STATIC, "invoke", STRING_INVOKE, 3, 1, code.bytes()); + } + } + + private static final class MethodCode { + private final int access; + private final String name; + private final String descriptor; + private final int maxStack; + private final int maxLocals; + private final byte[] code; + + private MethodCode( + int access, String name, String descriptor, int maxStack, int maxLocals, byte[] code) { + this.access = access; + this.name = name; + this.descriptor = descriptor; + this.maxStack = maxStack; + this.maxLocals = maxLocals; + this.code = code; + } + + private void write(DataOutputStream output, ConstantPool constants) throws IOException { + output.writeShort(access); + output.writeShort(constants.utf8(name)); + output.writeShort(constants.utf8(descriptor)); + output.writeShort(1); + output.writeShort(constants.utf8("Code")); + output.writeInt(12 + code.length); + output.writeShort(maxStack); + output.writeShort(maxLocals); + output.writeInt(code.length); + output.write(code); + output.writeShort(0); + output.writeShort(0); + } + + private void register(ConstantPool constants) { + constants.utf8(name); + constants.utf8(descriptor); + constants.utf8("Code"); + } + } + + private static final class Code { + private static final Map, Class> BOX_TYPES = boxTypes(); + private final ConstantPool constants; + private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + private final DataOutputStream output = new DataOutputStream(bytes); + + private Code(ConstantPool constants) { + this.constants = constants; + } + + private void op(int opcode) throws IOException { + output.writeByte(opcode); + } + + private void type(int opcode, String type) throws IOException { + output.writeByte(opcode); + output.writeShort(constants.classInfo(type)); + } + + private void method(int opcode, String owner, String name, String descriptor) + throws IOException { + output.writeByte(opcode); + output.writeShort(constants.methodRef(owner, name, descriptor)); + } + + private void integer(int value) throws IOException { + if (value <= 5) { + output.writeByte(0x03 + value); + } else if (value <= Byte.MAX_VALUE) { + output.writeByte(0x10); + output.writeByte(value); + } else { + output.writeByte(0x11); + output.writeShort(value); + } + } + + private void convert(Class type) throws IOException { + if (!type.isPrimitive()) { + if (type != Object.class) { + this.type(0xc0, internalName(type)); + } + return; + } + Class boxType = BOX_TYPES.get(type); + this.type(0xc0, internalName(boxType)); + method( + 0xb6, + internalName(boxType), + type.getName() + "Value", + "()" + descriptor(type)); + } + + private byte[] bytes() throws IOException { + output.flush(); + return bytes.toByteArray(); + } + + private static Map, Class> boxTypes() { + Map, Class> types = new LinkedHashMap<>(); + types.put(boolean.class, Boolean.class); + types.put(byte.class, Byte.class); + types.put(short.class, Short.class); + types.put(int.class, Integer.class); + types.put(long.class, Long.class); + types.put(float.class, Float.class); + types.put(double.class, Double.class); + types.put(char.class, Character.class); + return types; + } + } + + private static final class ConstantPool { + private final List entries = new ArrayList<>(); + private final Map indices = new LinkedHashMap<>(); + + private int utf8(String value) { + return add("U" + value, new Utf8Constant(value)); + } + + private int classInfo(String internalName) { + int name = utf8(internalName); + return add("C" + internalName, new IndexConstant(7, name)); + } + + private int nameAndType(String name, String descriptor) { + int nameIndex = utf8(name); + int descriptorIndex = utf8(descriptor); + return add( + "N" + name + descriptor, new PairConstant(12, nameIndex, descriptorIndex)); + } + + private int methodRef(String owner, String name, String descriptor) { + int ownerIndex = classInfo(owner); + int nameAndType = nameAndType(name, descriptor); + return add( + "M" + owner + '.' + name + descriptor, + new PairConstant(10, ownerIndex, nameAndType)); + } + + private int add(String key, Constant constant) { + Integer index = indices.get(key); + if (index != null) { + return index; + } + int newIndex = entries.size() + 1; + entries.add(constant); + indices.put(key, newIndex); + return newIndex; + } + + private void write(DataOutputStream output) throws IOException { + output.writeShort(entries.size() + 1); + for (Constant entry : entries) { + entry.write(output); + } + } + } + + private interface Constant { + void write(DataOutputStream output) throws IOException; + } + + private static final class Utf8Constant implements Constant { + private final String value; + + private Utf8Constant(String value) { + this.value = value; + } + + @Override + public void write(DataOutputStream output) throws IOException { + output.writeByte(1); + output.writeUTF(value); + } + } + + private static final class IndexConstant implements Constant { + private final int tag; + private final int index; + + private IndexConstant(int tag, int index) { + this.tag = tag; + this.index = index; + } + + @Override + public void write(DataOutputStream output) throws IOException { + output.writeByte(tag); + output.writeShort(index); + } + } + + private static final class PairConstant implements Constant { + private final int tag; + private final int first; + private final int second; + + private PairConstant(int tag, int first, int second) { + this.tag = tag; + this.first = first; + this.second = second; + } + + @Override + public void write(DataOutputStream output) throws IOException { + output.writeByte(tag); + output.writeShort(first); + output.writeShort(second); + } + } + + private static String constructorDescriptor(Class[] parameterTypes) { + StringBuilder descriptor = new StringBuilder("("); + for (Class parameterType : parameterTypes) { + descriptor.append(descriptor(parameterType)); + } + return descriptor.append(")V").toString(); + } + + private static String descriptor(Class type) { + if (type == void.class) { + return "V"; + } + if (type == boolean.class) { + return "Z"; + } + if (type == byte.class) { + return "B"; + } + if (type == char.class) { + return "C"; + } + if (type == short.class) { + return "S"; + } + if (type == int.class) { + return "I"; + } + if (type == long.class) { + return "J"; + } + if (type == float.class) { + return "F"; + } + if (type == double.class) { + return "D"; + } + if (type.isArray()) { + return type.getName().replace('.', '/'); + } + return 'L' + internalName(type) + ';'; + } + + private static String internalName(Class type) { + return type.getName().replace('.', '/'); + } +} 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 index b3943fab78..6f1e1ddc6f 100644 --- 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 @@ -15,4 +15,4 @@ # specific language governing permissions and limitations # under the License. -Args=--features=org.apache.fory.json.codec.ForyJsonGraalVMFeature +Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java index dc74e0e05d..722bbf7eaf 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java @@ -37,14 +37,13 @@ /** 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_NAME = "org.apache.fory.json.ForyJsonGraalVMFeature"; private static final String FEATURE_CLASS_FILE = - "org/apache/fory/json/codec/ForyJsonGraalVMFeature.class"; + "org/apache/fory/json/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/json/codec/ForyJsonGraalVMFeature.java"; + "org/apache/fory/json/ForyJsonGraalVMFeature.java"; private static final String VERSION_17_FEATURE_SOURCE = "META-INF/versions/17/" + FEATURE_SOURCE_FILE; private static final String NATIVE_IMAGE_PROPERTIES = From e7324600db1bd9949c71a1f37d011f9787f37d38 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 01:52:58 +0800 Subject: [PATCH 02/15] feat(json): generate GraalVM codecs at image build time --- docs/guide/java/graalvm-support.md | 13 +- docs/guide/java/json-support.md | 9 +- .../apache/fory/graalvm/ForyJsonExample.java | 34 +- .../graalvm/ForyJsonNoProviderExample.java | 122 ++++- .../graalvm/closed/ClosedJsonConfigs.java | 43 ++ .../fory-core/native-image.properties | 3 + java/fory-json/README.md | 9 +- .../java/org/apache/fory/json/ForyJson.java | 7 +- .../org/apache/fory/json/JsonCodegenKey.java | 6 +- .../fory/json/JsonGeneratedClassRegistry.java | 34 +- .../json/annotation/ForyJsonProvider.java | 4 +- .../fory/json/meta/JsonCreatorInfo.java | 131 ++++-- .../fory/json/resolver/CodecRegistry.java | 8 +- .../resolver/GeneratedCodecInstantiator.java | 55 ++- .../json/resolver/JsonSharedRegistry.java | 17 +- .../json/resolver/JsonStringValueCodec.java | 74 +-- .../fory/json/resolver/JsonTypeResolver.java | 4 + .../fory/json/ForyJsonGraalVMFeature.java | 153 +++--- .../apache/fory/json/JsonCreatorCodegen.java | 442 ------------------ 19 files changed, 436 insertions(+), 732 deletions(-) create mode 100644 integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java delete mode 100644 java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java diff --git a/docs/guide/java/graalvm-support.md b/docs/guide/java/graalvm-support.md index 96701e611e..12f8be76e1 100644 --- a/docs/guide/java/graalvm-support.md +++ b/docs/guide/java/graalvm-support.md @@ -69,9 +69,9 @@ public class JsonExample { ``` This is sufficient for correct native execution. During image construction, Fory JSON retains the -model metadata and prepares its field, property, creator, record, and `JsonAnySetter` access -handles. At runtime, `ForyJson.builder().build()` can therefore use interpreted codecs without -application reflection configuration or build-time initialization. +model metadata and prepares its field, property, creator, record, and `JsonAnySetter` access. At +runtime, `ForyJson.builder().build()` can therefore use interpreted codecs without application +reflection configuration, package exports or opens, or build-time initialization. To include generated codecs for a configuration, return that completed configuration from a reachable `@ForyJsonProvider`: @@ -106,7 +106,8 @@ configurations are generated once. Provider objects exist only while the image is built. Prefer a dedicated configuration class with instance fields and methods as shown above; no application `native-image.properties` entry is -needed. Static provider methods and fields are not supported. +needed, and the provider package does not need to be exported or opened to Fory. Static provider +methods and fields are not supported. Only configurations returned by a provider receive generated codecs. The default configuration is not generated implicitly. If a codegen-enabled runtime configuration was not included, Fory JSON @@ -139,8 +140,8 @@ public class JsonExample { `JsonMixin` is a build-time entry point for its exact declared target, so the target does not need `JsonType` solely to use the Mixin. The registered Mixin class literal must be reachable from the -application. The Native Image Feature retains the target metadata and prepares the same access -handles as it does for a direct `JsonType` model. A provider configuration generates the Mixin +application. The Native Image Feature retains the target metadata and prepares the same access as +it does for a direct `JsonType` model. A provider configuration generates the Mixin target only when that exact Mixin is registered in the returned `ForyJson`. Only one source is enabled for an exact target in a built `ForyJson`. Later registration replaces diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md index 311c95159e..e26fe5c35f 100644 --- a/docs/guide/java/json-support.md +++ b/docs/guide/java/json-support.md @@ -302,7 +302,7 @@ an existing runtime. In a GraalVM native image, runtime compilation and asynchronous compilation are unavailable. Configurations returned by a reachable `ForyJsonProvider` use codecs generated while the image is -built; other configurations use interpreted codecs with build-time-prepared access handles. Every +built; other configurations use interpreted codecs with build-time-prepared access metadata. Every other builder option keeps the behavior described above. ## Annotations @@ -1176,9 +1176,10 @@ no-argument constructor. One instance is shared by all annotated sites and concu the built `ForyJson`, so it must be thread-safe. Use `registerCodec(Target.class, instance)` when a complete-value codec needs configuration. -In a named Java module, export or open the codec package to `org.apache.fory.json`. When an inherited -type-declaration codec is used for a more specific target, every decoded value must be null or -assignable to that target. +Outside GraalVM Native Image, a named Java module must export or open the codec package to +`org.apache.fory.json`. Native Image prepares annotation-codec constructors during image +construction and does not require that package access. When an inherited type-declaration codec is +used for a more specific target, every decoded value must be null or assignable to that target. The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules. diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java index 42cf5a00d4..b91fd55438 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -37,6 +37,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; +import org.apache.fory.graalvm.closed.ClosedJsonConfigs; import org.apache.fory.graalvm.closed.ClosedJsonRecord; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; @@ -64,7 +65,7 @@ import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.util.Preconditions; -/** Native-image acceptance coverage for the complete interpreted Fory JSON path. */ +/** Native-image acceptance coverage for hosted code generation and interpreter fallback. */ public final class ForyJsonExample { private static final String NATIVE_INTERPRETER_MESSAGE = "Fory JSON is using interpreted codecs because the current configuration was not included " @@ -79,7 +80,8 @@ public static void main(String[] args) { try (PrintStream testOut = new PrintStream(captured, true, StandardCharsets.UTF_8)) { System.setOut(testOut); try { - Preconditions.checkArgument(JsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class)); + Preconditions.checkArgument( + ClosedJsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class)); if (GraalvmSupport.isGraalRuntime()) { testHostedCodegenConfigurations(); } @@ -142,12 +144,15 @@ private static void exerciseCodegenConfiguration(ForyJson json, boolean generate CodegenProbeModel value = new CodegenProbeModel(); value.id = 41; value.probe = new CodegenProbeValue("probe"); + value.children.add(new CodegenProbeChild("child")); String encoded = json.toJson(value); Preconditions.checkArgument(encoded.contains("probe")); String utf8 = new String(json.toJsonBytes(value), StandardCharsets.UTF_8); Preconditions.checkArgument(utf8.contains("probe")); Preconditions.checkArgument( json.fromJson(encoded, CodegenProbeModel.class).probe.value.equals("probe")); + Preconditions.checkArgument( + json.fromJson(encoded, CodegenProbeModel.class).children.get(0).name.equals("child")); String utf16 = encoded.replace(":\"probe\"", ":\"\u4f60\""); Preconditions.checkArgument( json.fromJson(utf16, CodegenProbeModel.class).probe.value.equals("\u4f60")); @@ -459,18 +464,6 @@ default ForyJson duplicateConfiguration() { } } - public static class ParentJsonConfigs { - public ForyJson generatedConfiguration() { - return newProviderJson(); - } - } - - @ForyJsonProvider - public static final class JsonConfigs extends ParentJsonConfigs - implements InheritedJsonConfig { - public JsonConfigs() {} - } - @JsonType public static final class CodegenProbeModel { public int id; @@ -478,9 +471,22 @@ public static final class CodegenProbeModel { @JsonCodec(CodegenProbeCodec.class) public CodegenProbeValue probe; + public List children = new ArrayList<>(); + public CodegenProbeModel() {} } + @JsonType + public static final class CodegenProbeChild { + public String name; + + public CodegenProbeChild() {} + + public CodegenProbeChild(String name) { + this.name = name; + } + } + public static final class CodegenProbeValue { private final String value; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java index 17e81ec789..3051ddf687 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java @@ -22,7 +22,11 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; import org.apache.fory.json.ForyJson; +import org.apache.fory.json.annotation.JsonAnyGetter; +import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.annotation.JsonCreator; import org.apache.fory.json.annotation.JsonProperty; @@ -53,7 +57,6 @@ public static void main(String[] args) { System.setOut(testOut); try { exercise(ForyJson.builder().build()); - exercise(ForyJson.builder().withFieldMode(true).build()); } finally { System.setOut(originalOut); } @@ -73,7 +76,12 @@ public static void main(String[] args) { } private static void exercise(ForyJson json) { - Model value = new Model(7, new Probe("value")); + Bean bean = new Bean(); + bean.setName("bean"); + bean.putExtra("dynamic", "extra"); + Model value = + new Model( + 7, new Probe("value"), bean, new RecordValue(8, "record"), FactoryValue.create(9)); String encoded = json.toJson(value); Preconditions.checkArgument(json.toJsonBytes(value).length != 0); Preconditions.checkArgument(json.fromJson(encoded, Model.class).equals(value)); @@ -103,11 +111,22 @@ public static final class Model { @JsonCodec(ProbeCodec.class) private final Probe probe; + private final Bean bean; + private final RecordValue record; + private final FactoryValue factory; + @JsonCreator public Model( - @JsonProperty("id") int id, @JsonProperty("probe") Probe probe) { + @JsonProperty("id") int id, + @JsonProperty("probe") Probe probe, + @JsonProperty("bean") Bean bean, + @JsonProperty("record") RecordValue record, + @JsonProperty("factory") FactoryValue factory) { this.id = id; this.probe = probe; + this.bean = bean; + this.record = record; + this.factory = factory; } public int getId() { @@ -118,6 +137,18 @@ public Probe getProbe() { return probe; } + public Bean getBean() { + return bean; + } + + public RecordValue getRecord() { + return record; + } + + public FactoryValue getFactory() { + return factory; + } + @Override public boolean equals(Object other) { if (this == other) { @@ -127,12 +158,90 @@ public boolean equals(Object other) { return false; } Model that = (Model) other; - return id == that.id && probe.equals(that.probe); + return id == that.id + && probe.equals(that.probe) + && bean.equals(that.bean) + && record.equals(that.record) + && factory.equals(that.factory); + } + + @Override + public int hashCode() { + int result = 31 * id + probe.hashCode(); + result = 31 * result + bean.hashCode(); + result = 31 * result + record.hashCode(); + return 31 * result + factory.hashCode(); + } + } + + @JsonType + public static final class Bean { + private String name; + private final transient Map extra = new LinkedHashMap<>(); + + public Bean() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonAnyGetter + public Map extra() { + return extra; + } + + @JsonAnySetter + public void putExtra(String key, String value) { + extra.put(key, value); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Bean)) { + return false; + } + Bean that = (Bean) other; + return name.equals(that.name) && extra.equals(that.extra); + } + + @Override + public int hashCode() { + return 31 * name.hashCode() + extra.hashCode(); + } + } + + @JsonType + public record RecordValue(int rank, String text) {} + + @JsonType + public static final class FactoryValue { + private final int code; + + private FactoryValue(int code) { + this.code = code; + } + + @JsonCreator + public static FactoryValue create(@JsonProperty("code") int code) { + return new FactoryValue(code); + } + + public int getCode() { + return code; + } + + @Override + public boolean equals(Object other) { + return other instanceof FactoryValue && code == ((FactoryValue) other).code; } @Override public int hashCode() { - return 31 * id + probe.hashCode(); + return code; } } @@ -159,8 +268,7 @@ public ProbeCodec() {} @Override public void writeString(StringJsonWriter writer, Probe value) { - checkInterpreted( - writer.typeResolver().getTypeInfo(Model.class, Model.class).stringWriter()); + checkInterpreted(writer.typeResolver().getTypeInfo(Model.class, Model.class).stringWriter()); writer.writeString(value == null ? null : value.value); } diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java new file mode 100644 index 0000000000..41e1a5c6a3 --- /dev/null +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java @@ -0,0 +1,43 @@ +/* + * 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.closed; + +import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeCodec; +import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeValue; +import org.apache.fory.graalvm.ForyJsonExample.InheritedJsonConfig; +import org.apache.fory.json.ForyJson; +import org.apache.fory.json.PropertyNamingStrategy; +import org.apache.fory.json.annotation.ForyJsonProvider; + +/** Provider whose constructor and inherited method need no package export or open. */ +@ForyJsonProvider +public final class ClosedJsonConfigs extends ClosedJsonConfigParent implements InheritedJsonConfig { + public ClosedJsonConfigs() {} +} + +class ClosedJsonConfigParent { + public ForyJson generatedConfiguration() { + return ForyJson.builder() + .writeNullFields(true) + .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .build(); + } +} 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 f5e5f2c88e..8fd42e1fa3 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 @@ -263,7 +263,10 @@ Args=--features=org.apache.fory.platform.ForyGraalVMFeature \ org.apache.fory.reflect.TypeRef$ClassOwnership$1,\ org.apache.fory.reflect.TypeRef$ClassOwnership$2,\ org.apache.fory.reflect.TypeRef$ClassOwnership,\ + org.apache.fory.reflect.TypeRef$GenericArrayTypeImpl,\ + org.apache.fory.reflect.TypeRef$ParameterizedTypeImpl,\ org.apache.fory.reflect.TypeRef$TypeVariableKey,\ + org.apache.fory.reflect.TypeRef$WildcardTypeImpl,\ org.apache.fory.reflect.TypeRef,\ org.apache.fory.reflect.ObjectInstantiators,\ org.apache.fory.reflect.ObjectInstantiators$DeclaredNoArgCtrInstantiator,\ diff --git a/java/fory-json/README.md b/java/fory-json/README.md index fb3f97ec24..33ba4942c8 100644 --- a/java/fory-json/README.md +++ b/java/fory-json/README.md @@ -379,7 +379,7 @@ Builder mutation after `build()` does not modify an existing `ForyJson` runtime. On Android, runtime code generation and asynchronous compilation are disabled. In a GraalVM native image, runtime compilation is unavailable; configurations returned by a reachable `ForyJsonProvider` use codecs generated while the image is built, and other configurations use -interpreted codecs with build-time-prepared access handles. Every other builder option keeps the +interpreted codecs with build-time-prepared access metadata. Every other builder option keeps the behavior described above. ## JSON annotations @@ -1295,9 +1295,10 @@ no-argument constructor. One instance is shared by all annotated sites and concu the built `ForyJson`, so it must be thread-safe. Use `registerCodec(Target.class, instance)` when a complete-value codec needs configuration. -In a named Java module, export or open the codec package to `org.apache.fory.json`. When an inherited -type-declaration codec is used for a more specific target, every decoded value must be null or -assignable to that target. +Outside GraalVM Native Image, a named Java module must export or open the codec package to +`org.apache.fory.json`. Native Image prepares annotation-codec constructors during image +construction and does not require that package access. When an inherited type-declaration codec is +used for a more specific target, every decoded value must be null or assignable to that target. The annotation has the same FIELD, METHOD, and PARAMETER behavior on the JVM, Android, and GraalVM Native Image. Ordinary Android classes may omit `JsonType` and provide equivalent exact rules. diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index bc4112bf4c..a4b456980c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -36,6 +36,7 @@ import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.StringSerializer; @@ -91,7 +92,7 @@ public final class ForyJson { } ForyJson(JsonConfig config, JsonSharedRegistry sharedRegistry) { - this.config = config; + this.config = GraalvmSupport.isGraalBuildTime() ? config : null; int poolSize = config.concurrencyLevel(); homeSlotMask = Integer.highestOneBit(poolSize) - 1; // This fixed array is the only JsonState owner. Each state's three readers own their configured @@ -109,6 +110,10 @@ public static ForyJsonBuilder builder() { } JsonConfig config() { + if (config == null) { + throw new IllegalStateException( + "Fory JSON configuration is available only during native-image build"); + } return config; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java index a7e8537518..bb0281bb5b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java @@ -27,7 +27,7 @@ public final class JsonCodegenKey { private final boolean writeNullFields; private final boolean propertyDiscoveryEnabled; - private final PropertyNamingStrategy propertyNamingStrategy; + private final String propertyNamingStrategy; private final String codecRegistryKey; private final String mixinKey; @@ -39,7 +39,7 @@ public final class JsonCodegenKey { String mixinKey) { this.writeNullFields = writeNullFields; this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; - this.propertyNamingStrategy = propertyNamingStrategy; + this.propertyNamingStrategy = propertyNamingStrategy.name(); this.codecRegistryKey = codecRegistryKey; this.mixinKey = mixinKey; } @@ -55,7 +55,7 @@ public boolean equals(Object other) { JsonCodegenKey that = (JsonCodegenKey) other; return writeNullFields == that.writeNullFields && propertyDiscoveryEnabled == that.propertyDiscoveryEnabled - && propertyNamingStrategy == that.propertyNamingStrategy + && propertyNamingStrategy.equals(that.propertyNamingStrategy) && codecRegistryKey.equals(that.codecRegistryKey) && mixinKey.equals(that.mixinKey); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java index 78a116afa2..228f955fb1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java @@ -27,7 +27,6 @@ import java.util.Set; import org.apache.fory.annotation.Internal; import org.apache.fory.json.resolver.JsonSharedRegistry.GeneratedClasses; -import org.apache.fory.reflect.TypeRef; /** Frozen Native Image mapping from JSON configuration semantics to generated classes. */ @Internal @@ -84,8 +83,8 @@ public static final class Configuration { private final Map, Class> latin1Readers; private final Map, Class> utf16Readers; private final Map, Class> utf8Readers; - private final Map> utf8CollectionWriters; - private final Map> utf8CollectionReaders; + private final Map> utf8CollectionWriters; + private final Map> utf8CollectionReaders; private Configuration(MutableConfiguration source) { stringWriters = immutable(source.stringWriters); @@ -118,11 +117,11 @@ public Class utf8Reader(Class type) { } public Class utf8CollectionWriter(Type type) { - return utf8CollectionWriters.get(typeKey(type)); + return utf8CollectionWriters.get(type); } public Class utf8CollectionReader(Type type) { - return utf8CollectionReaders.get(typeKey(type)); + return utf8CollectionReaders.get(type); } private static Map> immutable(Map> classes) { @@ -138,8 +137,8 @@ private static final class MutableConfiguration { private final Map, Class> latin1Readers = new HashMap<>(); private final Map, Class> utf16Readers = new HashMap<>(); private final Map, Class> utf8Readers = new HashMap<>(); - private final Map> utf8CollectionWriters = new HashMap<>(); - private final Map> utf8CollectionReaders = new HashMap<>(); + private final Map> utf8CollectionWriters = new HashMap<>(); + private final Map> utf8CollectionReaders = new HashMap<>(); private void merge(GeneratedClasses source, Set> added) { merge(source.stringWriters(), stringWriters, added); @@ -147,12 +146,8 @@ private void merge(GeneratedClasses source, Set> added) { merge(source.latin1Readers(), latin1Readers, added); merge(source.utf16Readers(), utf16Readers, added); merge(source.utf8Readers(), utf8Readers, added); - mergeTypes(source.utf8CollectionWriters(), utf8CollectionWriters, added); - mergeTypes(source.utf8CollectionReaders(), utf8CollectionReaders, added); - } - - private Configuration freeze() { - return new Configuration(this); + merge(source.utf8CollectionWriters(), utf8CollectionWriters, added); + merge(source.utf8CollectionReaders(), utf8CollectionReaders, added); } private static void merge( @@ -162,13 +157,6 @@ private static void merge( } } - private static void mergeTypes( - Map> source, Map> target, Set> added) { - for (Map.Entry> entry : source.entrySet()) { - merge(typeKey(entry.getKey()), entry.getValue(), target, added); - } - } - private static void merge( K key, Class generatedClass, Map> target, Set> added) { Class previous = target.putIfAbsent(key, generatedClass); @@ -178,9 +166,9 @@ private static void merge( throw new IllegalStateException("Conflicting generated Fory JSON classes for " + key); } } - } - private static String typeKey(Type type) { - return TypeRef.of(type).getTypeKey(); + private Configuration freeze() { + return new Configuration(this); + } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java b/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java index 1c84b0809a..5384d872c5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/annotation/ForyJsonProvider.java @@ -19,6 +19,7 @@ package org.apache.fory.json.annotation; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -33,8 +34,9 @@ * ForyJson} is invoked once while the native image is built. This includes inherited superclass * methods and public interface default methods. The returned configurations select the generated * object codecs included in the image; configurations not returned by a provider continue to use - * interpreted codecs. + * interpreted codecs. The provider package does not need to be exported or opened to Fory. */ +@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ForyJsonProvider {} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java index a0cb3382e6..3fdd85c970 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java @@ -21,6 +21,7 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; @@ -49,7 +50,9 @@ @Internal public final class JsonCreatorInfo { private static Map nativeInvokers = new HashMap<>(); - private static boolean nativeInvokersFrozen; + private static Map nativeStringInvokers = new HashMap<>(); + private static Map> nativeConstructors = new HashMap<>(); + private static boolean nativeCreatorsFrozen; private final Class ownerType; private final Executable executable; @@ -57,6 +60,7 @@ public final class JsonCreatorInfo { private final Object[] defaults; private final long[] hashes; private final MethodHandle invoker; + private final Constructor nativeConstructor; private final GeneratedJsonCodec generatedCodec; public JsonCreatorInfo( @@ -70,8 +74,9 @@ public JsonCreatorInfo( this.fields = fields; this.defaults = defaults; this.generatedCodec = generatedCodec; + nativeConstructor = generatedCodec == null ? nativeConstructor(executable) : null; invoker = - generatedCodec == null + generatedCodec == null && nativeConstructor == null && !GraalvmSupport.isGraalBuildTime() ? buildInvoker(ownerType, executable, executable.getParameterCount()) : null; hashes = new long[fields.length]; @@ -125,7 +130,9 @@ public Object create(Object[] arguments) { } try { Object value; - if (executable instanceof Constructor) { + if (nativeConstructor != null) { + value = nativeConstructor.newInstance(arguments); + } else if (executable instanceof Constructor) { value = ((Constructor) executable).newInstance(arguments); } else { value = ((Method) executable).invoke(null, arguments); @@ -174,57 +181,121 @@ private static MethodHandle buildInvoker( if (GraalvmSupport.isGraalRuntime()) { MethodHandle invoker = nativeInvokers.get(executable); if (invoker == null) { - throw new ForyJsonException( - "Missing Native Image Fory JSON creator metadata for " + executable); + throw missingNativeCreator(executable); } return invoker; } + MethodHandle target = creatorTarget(ownerType, executable); + // The interpreted reader already owns one trusted fixed-size argument array. Spread that + // exact array into the creator without a second carrier or per-call reflective access check. + return target + .asSpreader(Object[].class, parameterCount) + .asType(MethodType.methodType(Object.class, Object[].class)); + } + + /** Returns the cached one-String-argument creator used by a JsonValue representation. */ + @Internal + public static MethodHandle stringCreatorHandle(Class ownerType, Executable executable) { + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle invoker = nativeStringInvokers.get(executable); + if (invoker == null) { + throw missingNativeCreator(executable); + } + return invoker; + } + return creatorTarget(ownerType, executable) + .asType(MethodType.methodType(Object.class, String.class)); + } + + /** Returns the prepared Native Image constructor, or {@code null} outside native runtime. */ + @Internal + public static Constructor nativeConstructor(Executable executable) { + if (!GraalvmSupport.isGraalRuntime() || !(executable instanceof Constructor)) { + return null; + } + Constructor constructor = nativeConstructors.get(executable); + if (constructor == null) { + throw missingNativeCreator(executable); + } + return constructor; + } + + private static MethodHandle creatorTarget(Class ownerType, Executable executable) { try { - MethodHandle target = - executable instanceof Constructor - ? _JDKAccess._trustedLookup(ownerType) - .unreflectConstructor((Constructor) executable) - : _JDKAccess._trustedLookup(ownerType).unreflect((Method) executable); - // The interpreted reader already owns one trusted fixed-size argument array. Spread that - // exact array into the creator without a second carrier or per-call reflective access check. - return target - .asSpreader(Object[].class, parameterCount) - .asType(MethodType.methodType(Object.class, Object[].class)); + // A target-class trusted lookup has full member access without requiring the application + // module to export or open its model package. Native Image retains final factory handles; + // constructor creators use the separately registered Constructor cache. + return executable instanceof Constructor + ? _JDKAccess._trustedLookup(ownerType).unreflectConstructor((Constructor) executable) + : _JDKAccess._trustedLookup(executable.getDeclaringClass()) + .unreflect((Method) executable); } catch (IllegalAccessException e) { throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e); } } - /** Prepares one object creator handle for Native Image runtime metadata construction. */ + private static ForyJsonException missingNativeCreator(Executable executable) { + return new ForyJsonException( + "Missing Native Image Fory JSON creator metadata for " + executable); + } + + /** Prepares the Native Image runtime access for one object creator. */ @Internal - public static synchronized void prepareNativeInvoker(Class ownerType, Executable executable) { - if (!GraalvmSupport.isGraalBuildTime() || nativeInvokersFrozen) { + public static synchronized void prepareNativeCreator(Class ownerType, Executable executable) { + if (!GraalvmSupport.isGraalBuildTime() || nativeCreatorsFrozen) { throw new IllegalStateException("Fory JSON native creator cache is not writable"); } + if (executable instanceof Constructor) { + Constructor constructor = (Constructor) executable; + makeAccessible(constructor); + nativeConstructors.putIfAbsent(executable, constructor); + return; + } + MethodHandle target = creatorTarget(ownerType, executable); nativeInvokers.putIfAbsent( - executable, buildInvoker(ownerType, executable, executable.getParameterCount())); + executable, + target + .asSpreader(Object[].class, executable.getParameterCount()) + .asType(MethodType.methodType(Object.class, Object[].class))); + if (executable.getParameterCount() == 1 && executable.getParameterTypes()[0] == String.class) { + nativeStringInvokers.putIfAbsent( + executable, target.asType(MethodType.methodType(Object.class, String.class))); + } } - /** Caches one generated constructor invoker for Native Image runtime metadata construction. */ - @Internal - public static synchronized void prepareNativeConstructor( - Constructor constructor, MethodHandle invoker) { - if (!GraalvmSupport.isGraalBuildTime() || nativeInvokersFrozen) { - throw new IllegalStateException("Fory JSON native creator cache is not writable"); + private static void makeAccessible(AccessibleObject member) { + try { + // setAccessible0 is the JDK's access-check-free operation. Invoking it through the trusted + // lookup preserves access to closed application modules without an exports/opens contract. + _JDKAccess._trustedLookup(AccessibleObject.class) + .findVirtual( + AccessibleObject.class, + "setAccessible0", + MethodType.methodType(boolean.class, boolean.class)) + .invoke(member, true); + } catch (Throwable e) { + throw new ForyJsonException("Cannot prepare Native Image JSON creator " + member, e); } - nativeInvokers.putIfAbsent(constructor, invoker); } - /** Freezes all Native Image object creator handles after hosted analysis. */ + /** Freezes all Native Image object creator access after hosted analysis. */ @Internal - public static synchronized void freezeNativeInvokers() { - if (nativeInvokersFrozen) { + public static synchronized void freezeNativeCreators() { + if (nativeCreatorsFrozen) { return; } nativeInvokers = nativeInvokers.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(nativeInvokers)); - nativeInvokersFrozen = true; + nativeStringInvokers = + nativeStringInvokers.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(nativeStringInvokers)); + nativeConstructors = + nativeConstructors.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(nativeConstructors)); + nativeCreatorsFrozen = true; } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java index 14773aaed7..e9088a547e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/CodecRegistry.java @@ -32,10 +32,10 @@ * Builder-side registry of exact user-supplied {@link JsonValueCodec} bindings. * *

Registration is keyed by class identity and replaces any previous codec for the exact class. A - * {@link JsonSharedRegistry} receives a copy when a runtime is built, separating later builder - * mutation from an existing {@code ForyJson}. The deterministic {@link #codegenKey()} describes - * codec classes that can affect generated source without retaining codec instances in process-wide - * code-generation naming state. + * {@code JsonConfig} receives a copy when a runtime is built, separating later builder mutation + * from an existing {@code ForyJson}. The runtime registry reads that owned snapshot directly. The + * deterministic {@link #codegenKey()} describes codec classes that can affect generated source + * without retaining codec instances in process-wide code-generation naming state. */ public final class CodecRegistry { private final ConcurrentMap, JsonValueCodec> codecs; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java index 7f272bc46c..06edaa87f1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecInstantiator.java @@ -20,6 +20,7 @@ package org.apache.fory.json.resolver; import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.Latin1ReaderCodec; @@ -31,12 +32,23 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.platform.AndroidSupport; +import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.ReflectionUtils; /** Invokes generated codec constructor contracts selected and owned by {@link JsonTypeResolver}. */ final class GeneratedCodecInstantiator { private GeneratedCodecInstantiator() {} + private static MethodHandle constructor(Class type, Class... parameterTypes) + throws NoSuchMethodException, IllegalAccessException { + if (GraalvmSupport.isGraalRuntime()) { + return ReflectionUtils.getCtrHandle(type, parameterTypes); + } + return _JDKAccess._trustedLookup(type) + .findConstructor(type, MethodType.methodType(void.class, parameterTypes)); + } + @SuppressWarnings("unchecked") static StringWriterCodec instantiateStringWriter( Class type, JsonFieldInfo[] fields, StringWriterCodec[] codecs) { @@ -48,7 +60,7 @@ static StringWriterCodec instantiateStringWriter( return (StringWriterCodec) constructor.newInstance(fields, codecs); } MethodHandle constructor = - ReflectionUtils.getCtrHandle(type, JsonFieldInfo[].class, StringWriterCodec[].class); + constructor(type, JsonFieldInfo[].class, StringWriterCodec[].class); return (StringWriterCodec) constructor.invoke(fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON String writer", e); @@ -70,8 +82,7 @@ static StringWriterCodec instantiateAnyStringWriter( return (StringWriterCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( - type, ObjectCodec.class, JsonFieldInfo[].class, StringWriterCodec[].class); + constructor(type, ObjectCodec.class, JsonFieldInfo[].class, StringWriterCodec[].class); return (StringWriterCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any String writer", e); @@ -97,7 +108,7 @@ static StringWriterCodec instantiateAnyStringWriter( return (StringWriterCodec) constructor.newInstance(owner, fields, codecs, anyCodec); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldInfo[].class, @@ -119,8 +130,7 @@ static Utf8WriterCodec instantiateUtf8Writer( constructor.setAccessible(true); return (Utf8WriterCodec) constructor.newInstance(fields, codecs); } - MethodHandle constructor = - ReflectionUtils.getCtrHandle(type, JsonFieldInfo[].class, Utf8WriterCodec[].class); + MethodHandle constructor = constructor(type, JsonFieldInfo[].class, Utf8WriterCodec[].class); return (Utf8WriterCodec) constructor.invoke(fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 writer", e); @@ -142,8 +152,7 @@ static Utf8WriterCodec instantiateAnyUtf8Writer( return (Utf8WriterCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( - type, ObjectCodec.class, JsonFieldInfo[].class, Utf8WriterCodec[].class); + constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Utf8WriterCodec[].class); return (Utf8WriterCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Any UTF8 writer", e); @@ -169,7 +178,7 @@ static Utf8WriterCodec instantiateAnyUtf8Writer( return (Utf8WriterCodec) constructor.newInstance(owner, fields, codecs, anyCodec); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldInfo[].class, @@ -196,8 +205,7 @@ static Latin1ReaderCodec instantiateLatin1Reader( return (Latin1ReaderCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( - type, ObjectCodec.class, JsonFieldInfo[].class, Latin1ReaderCodec[].class); + constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Latin1ReaderCodec[].class); return (Latin1ReaderCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON Latin1 reader", e); @@ -226,7 +234,7 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldTable.class, @@ -264,7 +272,7 @@ static Latin1ReaderCodec instantiateAnyLatin1Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldTable.class, @@ -294,8 +302,7 @@ static Utf16ReaderCodec instantiateUtf16Reader( return (Utf16ReaderCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( - type, ObjectCodec.class, JsonFieldInfo[].class, Utf16ReaderCodec[].class); + constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Utf16ReaderCodec[].class); return (Utf16ReaderCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF16 reader", e); @@ -324,7 +331,7 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldTable.class, @@ -362,7 +369,7 @@ static Utf16ReaderCodec instantiateAnyUtf16Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldTable.class, @@ -392,8 +399,7 @@ static Utf8ReaderCodec instantiateUtf8Reader( return (Utf8ReaderCodec) constructor.newInstance(owner, fields, codecs); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( - type, ObjectCodec.class, JsonFieldInfo[].class, Utf8ReaderCodec[].class); + constructor(type, ObjectCodec.class, JsonFieldInfo[].class, Utf8ReaderCodec[].class); return (Utf8ReaderCodec) constructor.invoke(owner, fields, codecs); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 reader", e); @@ -409,7 +415,7 @@ static Utf8WriterCodec instantiateUtf8CollectionWriter( constructor.setAccessible(true); return (Utf8WriterCodec) constructor.newInstance(fallback); } - MethodHandle constructor = ReflectionUtils.getCtrHandle(type, Utf8WriterCodec.class); + MethodHandle constructor = constructor(type, Utf8WriterCodec.class); return (Utf8WriterCodec) constructor.invoke(fallback); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e); @@ -426,8 +432,7 @@ static Utf8WriterCodec instantiateUtf8CollectionWriter( constructor.setAccessible(true); return (Utf8WriterCodec) constructor.newInstance(fallback, elementWriter); } - MethodHandle constructor = - ReflectionUtils.getCtrHandle(type, Utf8WriterCodec.class, Utf8WriterCodec.class); + MethodHandle constructor = constructor(type, Utf8WriterCodec.class, Utf8WriterCodec.class); return (Utf8WriterCodec) constructor.invoke(fallback, elementWriter); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection writer", e); @@ -443,7 +448,7 @@ static Utf8ReaderCodec instantiateUtf8CollectionReader( constructor.setAccessible(true); return (Utf8ReaderCodec) constructor.newInstance(elementReader); } - MethodHandle constructor = ReflectionUtils.getCtrHandle(type, Utf8ReaderCodec.class); + MethodHandle constructor = constructor(type, Utf8ReaderCodec.class); return (Utf8ReaderCodec) constructor.invoke(elementReader); } catch (Throwable e) { throw new ForyJsonException("Cannot instantiate generated JSON UTF8 collection reader", e); @@ -472,7 +477,7 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldTable.class, @@ -510,7 +515,7 @@ static Utf8ReaderCodec instantiateAnyUtf8Reader( constructor.newInstance(owner, readTable, fields, codecs, selfReader, anyCodec); } MethodHandle constructor = - ReflectionUtils.getCtrHandle( + constructor( type, ObjectCodec.class, JsonFieldTable.class, 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 ce3d5697b6..be258383b1 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 @@ -175,6 +175,7 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final Object typeCheckCacheLock; private final JsonCodegen codegen; private final JsonCodegenKey nativeCodegenKey; + private final boolean hostedCodegen; private final boolean asyncCompilationEnabled; private final ExecutorService compilationService; private final boolean propertyDiscoveryEnabled; @@ -214,7 +215,7 @@ public JsonSharedRegistry(JsonConfig config) { private JsonSharedRegistry( JsonConfig config, ExecutorService compilationService, boolean hostedCodegen) { - this.customCodecs = config.codecRegistry().copy(); + this.customCodecs = config.codecRegistry(); typeChecker = config.typeChecker(); typeCheckContext = config.typeCheckContext(); typeCheckCache = typeChecker == null ? null : new ConcurrentHashMap<>(); @@ -245,6 +246,7 @@ private JsonSharedRegistry( utf8CollectionReaderClasses = new ConcurrentHashMap<>(); cachedFieldNames = new ConcurrentHashMap<>(); boolean codegenEnabled = config.codegenEnabled(); + this.hostedCodegen = hostedCodegen; boolean createCompiler = codegenEnabled && (hostedCodegen || !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE); codegen = createCompiler ? new JsonCodegen(config.getCodegenHash(), classLoader) : null; @@ -403,6 +405,10 @@ boolean generatedCapabilitiesEnabled() { return codegen != null || nativeConfiguration() != null; } + boolean hostedCodegen() { + return hostedCodegen; + } + boolean missingNativeConfiguration() { return nativeCodegenKey != null && nativeConfiguration() == null; } @@ -539,10 +545,7 @@ GeneratedJsonCodec generatedCodec(Class type) { } try { GeneratedJsonCodec codec = generatedCodecIfPresent(type, mixinType); - if (codec == null - && (directGenerated - || mixinType != null - && (AndroidSupport.IS_ANDROID || GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE))) { + if (codec == null && (directGenerated || mixinType != null && AndroidSupport.IS_ANDROID)) { throw missingGeneratedCodec(type, mixinType, "JSON object model"); } return codec; @@ -693,9 +696,7 @@ record = codec.isRecord(); Executable creator = validateGeneratedCreator( type, memberAccessors, creatorNames, creatorTypes, creatorFactory, record); - if (!AndroidSupport.IS_ANDROID - && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE - && RecordUtils.isRecord(type) != record) { + if (!AndroidSupport.IS_ANDROID && RecordUtils.isRecord(type) != record) { throw invalidGeneratedCodec(type, "isRecord() does not match the runtime model class"); } codec.initializeValidated( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java index e3cadf4e03..c43026b1cb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java @@ -20,18 +20,14 @@ package org.apache.fory.json.resolver; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; @@ -40,12 +36,9 @@ import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.internal._JDKAccess; -import org.apache.fory.reflect.ReflectionUtils; /** Complete String representation selected by one effective {@code JsonValue} member. */ -@Internal -public final class JsonStringValueCodec implements JsonValueCodec { +final class JsonStringValueCodec implements JsonValueCodec { private final Class ownerType; private final JsonFieldAccessor accessor; private final ValueCreator creator; @@ -139,9 +132,6 @@ private Object read(String value) { } private abstract static class ValueCreator { - private static Map nativeInvokers = new HashMap<>(); - private static boolean nativeInvokersFrozen; - final Class ownerType; private ValueCreator(Class ownerType) { @@ -175,6 +165,15 @@ static ValueCreator forExecutable( if (generatedCodec != null) { return new GeneratedCreator(ownerType, generatedCodec); } + if (GraalvmSupport.isGraalBuildTime()) { + return executable instanceof Constructor + ? new ConstructorCreator(ownerType, (Constructor) executable) + : new FactoryCreator(ownerType, (Method) executable); + } + Constructor nativeConstructor = JsonCreatorInfo.nativeConstructor(executable); + if (nativeConstructor != null) { + return new ConstructorCreator(ownerType, nativeConstructor); + } if (!AndroidSupport.IS_ANDROID) { return new MethodHandleCreator(ownerType, buildInvoker(ownerType, executable)); } @@ -185,57 +184,8 @@ static ValueCreator forExecutable( } private static MethodHandle buildInvoker(Class ownerType, Executable executable) { - if (GraalvmSupport.isGraalRuntime()) { - MethodHandle invoker = nativeInvokers.get(executable); - if (invoker == null) { - throw new ForyJsonException( - "Missing Native Image Fory JSON String creator metadata for " + executable); - } - return invoker; - } - try { - MethodHandle target = - executable instanceof Constructor - ? ReflectionUtils.getCtrHandle(ownerType, executable.getParameterTypes()) - : _JDKAccess._trustedLookup(ownerType).unreflect((Method) executable); - return target.asType(MethodType.methodType(Object.class, String.class)); - } catch (IllegalAccessException e) { - throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e); - } - } - } - - /** Prepares one complete-String creator handle for Native Image runtime metadata construction. */ - @Internal - public static synchronized void prepareNativeCreator(Class ownerType, Executable executable) { - if (!GraalvmSupport.isGraalBuildTime() || ValueCreator.nativeInvokersFrozen) { - throw new IllegalStateException("Fory JSON native String creator cache is not writable"); - } - ValueCreator.nativeInvokers.putIfAbsent( - executable, ValueCreator.buildInvoker(ownerType, executable)); - } - - /** Caches one generated one-String constructor invoker for Native Image runtime use. */ - @Internal - public static synchronized void prepareNativeConstructor( - Constructor constructor, MethodHandle invoker) { - if (!GraalvmSupport.isGraalBuildTime() || ValueCreator.nativeInvokersFrozen) { - throw new IllegalStateException("Fory JSON native String creator cache is not writable"); - } - ValueCreator.nativeInvokers.putIfAbsent(constructor, invoker); - } - - /** Freezes all Native Image complete-String creator handles after hosted analysis. */ - @Internal - public static synchronized void freezeNativeCreators() { - if (ValueCreator.nativeInvokersFrozen) { - return; + return JsonCreatorInfo.stringCreatorHandle(ownerType, executable); } - ValueCreator.nativeInvokers = - ValueCreator.nativeInvokers.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(ValueCreator.nativeInvokers)); - ValueCreator.nativeInvokersFrozen = true; } private static final class GeneratedCreator extends ValueCreator { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index 5caa16e6e0..ac864357ee 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -1789,6 +1789,10 @@ private void requestCapabilities(ArrayList roots) { } private void requestGraph(CapabilityGraph graph) { + if (sharedRegistry.hostedCodegen()) { + graph.classesReady().join(); + return; + } if (sharedRegistry.nativeGeneratedClasses()) { graph.loadNativeClasses(); graph.publish(); diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index a50f0dceab..4b9d02c579 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -19,12 +19,12 @@ package org.apache.fory.json; +import java.lang.invoke.MethodHandle; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; @@ -60,13 +60,14 @@ import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.Base64ByteArrayCodec; import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry.JsonMixinView; -import org.apache.fory.json.resolver.JsonStringValueCodec; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.ObjectInstantiators; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; @@ -90,10 +91,6 @@ final class ForyJsonGraalVMFeature implements Feature { private final Set> processedProviders = ConcurrentHashMap.newKeySet(); private final Set> processedCodecs = ConcurrentHashMap.newKeySet(); private final Set> processedContainers = ConcurrentHashMap.newKeySet(); - private final Map> creators = new LinkedHashMap<>(); - private final Set preparedCreators = new LinkedHashSet<>(); - private final Set> generatedConstructors = new LinkedHashSet<>(); - private final Set> preparedGeneratedConstructors = new LinkedHashSet<>(); private final Map hostedConfigurations = new LinkedHashMap<>(); private final Set processedGenerations = new LinkedHashSet<>(); @@ -105,22 +102,27 @@ public String getDescription() { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeClassInitialization.initializeAtBuildTime(JsonCodegenKey.class); - RuntimeClassInitialization.initializeAtBuildTime(JsonGeneratedClassRegistry.class); + String jsonPackage = ForyJson.class.getPackage().getName(); + // GraalVM 21 requires the Fory JSON implementation used by hosted codegen to have an explicit + // build-time initialization policy. Keep application models and the ScalarCodecs temporal + // formatters out of that policy: the latter capture JDK chronology instances which GraalVM 25 + // initializes at runtime. RuntimeClassInitialization.initializeAtBuildTime( - JsonGeneratedClassRegistry.class.getDeclaredClasses()); - RuntimeClassInitialization.initializeAtBuildTime(JsonFieldAccessor.class); - RuntimeClassInitialization.initializeAtBuildTime(JsonFieldAccessor.class.getDeclaredClasses()); - RuntimeClassInitialization.initializeAtBuildTime(JsonCreatorInfo.class); - RuntimeClassInitialization.initializeAtBuildTime(JsonStringValueCodec.class); + ForyJson.class, + ForyJsonBuilder.class, + JsonCodegenKey.class, + JsonConfig.class, + JsonGeneratedClassRegistry.class, + JsonGeneratedClassRegistry.Configuration.class, + PropertyNamingStrategy.class); RuntimeClassInitialization.initializeAtBuildTime( - JsonStringValueCodec.class.getDeclaredClasses()); - RuntimeClassInitialization.initializeAtBuildTime(ObjectCodec.AnyInfo.class); - // Hosted Mixin discovery deliberately reuses the runtime's structural resolver so build-time - // reachability and runtime semantics cannot drift. Its static state contains only the immutable - // supported-annotation set and is therefore safe to initialize in the image builder. - RuntimeClassInitialization.initializeAtBuildTime( - ForyJson.class.getPackage().getName() + ".resolver.JsonMixinAnnotations"); + jsonPackage + ".codegen", + jsonPackage + ".codec", + jsonPackage + ".meta", + jsonPackage + ".reader", + jsonPackage + ".resolver", + jsonPackage + ".writer"); + RuntimeClassInitialization.initializeAtRunTime(ScalarCodecs.class); access.registerSubtypeReachabilityHandler(this::processReachableType, Object.class); } @@ -133,7 +135,7 @@ public void duringAnalysis(DuringAnalysisAccess access) { if (!reachableTypes.contains(ForyJson.class)) { return; } - boolean changed = prepareNativeHandles(); + boolean changed = false; List> orderedTypes = new ArrayList<>(reachableTypes); orderedTypes.sort(Comparator.comparing(Class::getName)); for (Class type : orderedTypes) { @@ -180,11 +182,12 @@ private boolean registerProvider(Class providerClass) { } catch (NoSuchMethodException e) { throw providerFailure(providerClass, "must have a public no-argument constructor", e); } + MethodHandle providerConstructor = providerConstructor(providerClass, constructor); Object provider; try { - provider = constructor.newInstance(); - } catch (ReflectiveOperationException | RuntimeException e) { - throw providerFailure(providerClass, "cannot be constructed", unwrap(e)); + provider = providerConstructor.invoke(); + } catch (Throwable e) { + throw providerFailure(providerClass, "cannot be constructed", e); } List methods = providerMethods(providerClass); if (methods.isEmpty()) { @@ -198,12 +201,12 @@ private boolean registerProvider(Class providerClass) { "method must be a non-static zero-argument instance method: " + method, null); } + MethodHandle providerMethod = providerMethod(providerClass, method); ForyJson json; try { - json = (ForyJson) method.invoke(provider); - } catch (ReflectiveOperationException | RuntimeException e) { - throw providerFailure( - providerClass, "cannot invoke provider method " + method, unwrap(e)); + json = (ForyJson) providerMethod.invoke(provider); + } catch (Throwable e) { + throw providerFailure(providerClass, "cannot invoke provider method " + method, e); } if (json == null) { throw providerFailure(providerClass, "provider method returned null: " + method, null); @@ -222,6 +225,23 @@ private boolean registerProvider(Class providerClass) { return changed; } + private static MethodHandle providerConstructor( + Class providerClass, Constructor constructor) { + try { + return _JDKAccess._trustedLookup(providerClass).unreflectConstructor(constructor); + } catch (IllegalAccessException e) { + throw providerFailure(providerClass, "cannot access its constructor", e); + } + } + + private static MethodHandle providerMethod(Class providerClass, Method method) { + try { + return _JDKAccess._trustedLookup(method.getDeclaringClass()).unreflect(method); + } catch (IllegalAccessException e) { + throw providerFailure(providerClass, "cannot access method " + method, e); + } + } + private static List providerMethods(Class providerClass) { Map effective = new HashMap<>(); for (Method method : providerClass.getMethods()) { @@ -286,65 +306,17 @@ private boolean generateConfigurations(DuringAnalysisAccess access) { } private void registerGeneratedClass(Class generatedClass) { - // Generated codecs contain only instance state and are defined during analysis, after native - // image class-initialization policy has been fixed. - RuntimeReflection.register(generatedClass); Constructor[] constructors = generatedClass.getDeclaredConstructors(); if (constructors.length == 0) { throw new IllegalStateException( "Generated Fory JSON class has no constructor: " + generatedClass.getName()); } - RuntimeReflection.register(constructors); for (Constructor constructor : constructors) { - RuntimeReflection.registerConstructorLookup( - generatedClass, constructor.getParameterTypes()); - generatedConstructors.add(constructor); + ReflectionUtils.getCtrHandle( + constructor.getDeclaringClass(), constructor.getParameterTypes()); } } - private boolean prepareNativeHandles() { - boolean changed = false; - for (Map.Entry> entry : creators.entrySet()) { - Executable executable = entry.getKey(); - if (!preparedCreators.add(executable)) { - continue; - } - Class ownerType = entry.getValue(); - boolean stringCreator = - executable.getParameterCount() == 1 - && executable.getParameterTypes()[0] == String.class; - if (executable instanceof Constructor) { - JsonCreatorCodegen.Invokers invokers = - JsonCreatorCodegen.create((Constructor) executable); - RuntimeReflection.register(invokers.type); - RuntimeReflection.register(invokers.creatorMethod); - JsonCreatorInfo.prepareNativeConstructor( - (Constructor) executable, invokers.creator); - if (stringCreator) { - RuntimeReflection.register(invokers.stringMethod); - JsonStringValueCodec.prepareNativeConstructor( - (Constructor) executable, invokers.stringCreator); - } - } else { - JsonCreatorInfo.prepareNativeInvoker(ownerType, executable); - if (stringCreator) { - JsonStringValueCodec.prepareNativeCreator(ownerType, executable); - } - } - RuntimeReflection.register(executable); - changed = true; - } - for (Constructor constructor : generatedConstructors) { - if (preparedGeneratedConstructors.add(constructor)) { - ReflectionUtils.getCtrHandle( - constructor.getDeclaringClass(), constructor.getParameterTypes()); - RuntimeReflection.register(constructor); - changed = true; - } - } - return changed; - } - private static IllegalStateException providerFailure( Class providerClass, String reason, Throwable cause) { String message = "Invalid @ForyJsonProvider " + providerClass.getName() + ": " + reason; @@ -353,16 +325,11 @@ private static IllegalStateException providerFailure( : new IllegalStateException(message, cause); } - private static Throwable unwrap(Throwable throwable) { - return throwable instanceof InvocationTargetException && throwable.getCause() != null - ? throwable.getCause() - : throwable; - } - private boolean registerModel(DuringAnalysisAccess access, Class type) { if (!processedModels.add(type)) { return false; } + GraalvmSupport.registerClass(type); RuntimeReflection.register(type); registerContainer(type); registerDeclarations(type); @@ -401,6 +368,7 @@ private boolean registerMixin( registerDeclarations(targetType); JsonCodec directTypeCodec = annotations.annotation(targetType, JsonCodec.class); registerCodecs(directTypeCodec); + GraalvmSupport.registerClass(targetType); RuntimeReflection.register(targetType); registerContainer(targetType); boolean intrinsicTarget = @@ -536,8 +504,7 @@ private void registerReflectiveDeclarations(Set declarations) public void afterAnalysis(AfterAnalysisAccess access) { JsonGeneratedClassRegistry.freeze(); JsonFieldAccessor.freezeNativeAccessors(); - JsonCreatorInfo.freezeNativeInvokers(); - JsonStringValueCodec.freezeNativeCreators(); + JsonCreatorInfo.freezeNativeCreators(); ObjectCodec.freezeNativeAnySetters(); } @@ -634,19 +601,9 @@ private void prepareRecord(Class type) { } private void registerCreator(Class ownerType, Executable executable) { + GraalvmSupport.registerClass(executable.getDeclaringClass()); RuntimeReflection.register(executable); - if (executable instanceof Constructor) { - RuntimeReflection.registerConstructorLookup(ownerType, executable.getParameterTypes()); - } else { - Method method = (Method) executable; - RuntimeReflection.registerMethodLookup( - method.getDeclaringClass(), method.getName(), method.getParameterTypes()); - } - Class previous = creators.putIfAbsent(executable, ownerType); - if (previous != null && previous != ownerType) { - throw new IllegalStateException( - "Conflicting Fory JSON creator owners for " + executable); - } + JsonCreatorInfo.prepareNativeCreator(ownerType, executable); } private static void prepareMethodAccessors(JsonMixinView annotations, Method method) { diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java b/java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java deleted file mode 100644 index b7e44597d4..0000000000 --- a/java/fory-json/src/main/java17/org/apache/fory/json/JsonCreatorCodegen.java +++ /dev/null @@ -1,442 +0,0 @@ -/* - * 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; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.apache.fory.platform.internal.DefineClass; -import org.apache.fory.platform.internal._JDKAccess; - -/** Generates direct constructor invokers while Native Image analysis is still mutable. */ -final class JsonCreatorCodegen { - private static final int ACC_PUBLIC = 0x0001; - private static final int ACC_STATIC = 0x0008; - private static final int ACC_FINAL = 0x0010; - private static final int ACC_SUPER = 0x0020; - private static final int ACC_SYNTHETIC = 0x1000; - - private JsonCreatorCodegen() {} - - static Invokers create(Constructor constructor) { - Class ownerType = constructor.getDeclaringClass(); - Class invokerClass = - DefineClass.defineHiddenNestmate(ownerType, new ClassBytes(constructor).build()); - try { - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(invokerClass); - MethodHandle creator = - lookup.findStatic( - invokerClass, - "invoke", - MethodType.methodType(Object.class, Object[].class)); - Method creatorMethod = invokerClass.getDeclaredMethod("invoke", Object[].class); - if (constructor.getParameterCount() != 1 - || constructor.getParameterTypes()[0] != String.class) { - return new Invokers(invokerClass, creator, creatorMethod, null, null); - } - MethodHandle stringCreator = - lookup.findStatic( - invokerClass, - "invoke", - MethodType.methodType(Object.class, String.class)); - Method stringMethod = invokerClass.getDeclaredMethod("invoke", String.class); - return new Invokers(invokerClass, creator, creatorMethod, stringCreator, stringMethod); - } catch (ReflectiveOperationException cause) { - throw new IllegalStateException( - "Cannot resolve generated Fory JSON creator for " + constructor, cause); - } - } - - static final class Invokers { - final Class type; - final MethodHandle creator; - final Method creatorMethod; - final MethodHandle stringCreator; - final Method stringMethod; - - private Invokers( - Class type, - MethodHandle creator, - Method creatorMethod, - MethodHandle stringCreator, - Method stringMethod) { - this.type = type; - this.creator = creator; - this.creatorMethod = creatorMethod; - this.stringCreator = stringCreator; - this.stringMethod = stringMethod; - } - } - - private static final class ClassBytes { - private static final String OBJECT = "java/lang/Object"; - private static final String OBJECT_ARRAY_INVOKE = "([Ljava/lang/Object;)Ljava/lang/Object;"; - private static final String STRING_INVOKE = "(Ljava/lang/String;)Ljava/lang/Object;"; - - private final Constructor constructor; - private final Class[] parameterTypes; - private final String owner; - private final String generatedName; - private final boolean stringCreator; - private final ConstantPool constants = new ConstantPool(); - - private ClassBytes(Constructor constructor) { - this.constructor = constructor; - parameterTypes = constructor.getParameterTypes(); - owner = internalName(constructor.getDeclaringClass()); - generatedName = owner + "$$ForyJsonCreator"; - stringCreator = parameterTypes.length == 1 && parameterTypes[0] == String.class; - } - - private byte[] build() { - try { - int thisClass = constants.classInfo(generatedName); - int superClass = constants.classInfo(OBJECT); - MethodCode arrayInvoker = arrayInvoker(); - MethodCode stringInvoker = stringCreator ? stringInvoker() : null; - arrayInvoker.register(constants); - if (stringCreator) { - stringInvoker.register(constants); - } - - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(bytes); - output.writeInt(0xCAFEBABE); - output.writeShort(0); - output.writeShort(52); - constants.write(output); - output.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER | ACC_SYNTHETIC); - output.writeShort(thisClass); - output.writeShort(superClass); - output.writeShort(0); - output.writeShort(0); - output.writeShort(stringCreator ? 2 : 1); - arrayInvoker.write(output, constants); - if (stringCreator) { - stringInvoker.write(output, constants); - } - output.writeShort(0); - output.flush(); - return bytes.toByteArray(); - } catch (IOException cause) { - throw new IllegalStateException("Cannot generate Fory JSON creator for " + constructor, cause); - } - } - - private MethodCode arrayInvoker() throws IOException { - Code code = new Code(constants); - code.type(0xbb, owner); - code.op(0x59); - for (int i = 0; i < parameterTypes.length; i++) { - code.op(0x2a); - code.integer(i); - code.op(0x32); - code.convert(parameterTypes[i]); - } - code.method(0xb7, owner, "", constructorDescriptor(parameterTypes)); - code.op(0xb0); - return new MethodCode( - ACC_PUBLIC | ACC_STATIC, - "invoke", - OBJECT_ARRAY_INVOKE, - 4 + parameterTypes.length * 2, - 1, - code.bytes()); - } - - private MethodCode stringInvoker() throws IOException { - Code code = new Code(constants); - code.type(0xbb, owner); - code.op(0x59); - code.op(0x2a); - code.method(0xb7, owner, "", constructorDescriptor(parameterTypes)); - code.op(0xb0); - return new MethodCode( - ACC_PUBLIC | ACC_STATIC, "invoke", STRING_INVOKE, 3, 1, code.bytes()); - } - } - - private static final class MethodCode { - private final int access; - private final String name; - private final String descriptor; - private final int maxStack; - private final int maxLocals; - private final byte[] code; - - private MethodCode( - int access, String name, String descriptor, int maxStack, int maxLocals, byte[] code) { - this.access = access; - this.name = name; - this.descriptor = descriptor; - this.maxStack = maxStack; - this.maxLocals = maxLocals; - this.code = code; - } - - private void write(DataOutputStream output, ConstantPool constants) throws IOException { - output.writeShort(access); - output.writeShort(constants.utf8(name)); - output.writeShort(constants.utf8(descriptor)); - output.writeShort(1); - output.writeShort(constants.utf8("Code")); - output.writeInt(12 + code.length); - output.writeShort(maxStack); - output.writeShort(maxLocals); - output.writeInt(code.length); - output.write(code); - output.writeShort(0); - output.writeShort(0); - } - - private void register(ConstantPool constants) { - constants.utf8(name); - constants.utf8(descriptor); - constants.utf8("Code"); - } - } - - private static final class Code { - private static final Map, Class> BOX_TYPES = boxTypes(); - private final ConstantPool constants; - private final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - private final DataOutputStream output = new DataOutputStream(bytes); - - private Code(ConstantPool constants) { - this.constants = constants; - } - - private void op(int opcode) throws IOException { - output.writeByte(opcode); - } - - private void type(int opcode, String type) throws IOException { - output.writeByte(opcode); - output.writeShort(constants.classInfo(type)); - } - - private void method(int opcode, String owner, String name, String descriptor) - throws IOException { - output.writeByte(opcode); - output.writeShort(constants.methodRef(owner, name, descriptor)); - } - - private void integer(int value) throws IOException { - if (value <= 5) { - output.writeByte(0x03 + value); - } else if (value <= Byte.MAX_VALUE) { - output.writeByte(0x10); - output.writeByte(value); - } else { - output.writeByte(0x11); - output.writeShort(value); - } - } - - private void convert(Class type) throws IOException { - if (!type.isPrimitive()) { - if (type != Object.class) { - this.type(0xc0, internalName(type)); - } - return; - } - Class boxType = BOX_TYPES.get(type); - this.type(0xc0, internalName(boxType)); - method( - 0xb6, - internalName(boxType), - type.getName() + "Value", - "()" + descriptor(type)); - } - - private byte[] bytes() throws IOException { - output.flush(); - return bytes.toByteArray(); - } - - private static Map, Class> boxTypes() { - Map, Class> types = new LinkedHashMap<>(); - types.put(boolean.class, Boolean.class); - types.put(byte.class, Byte.class); - types.put(short.class, Short.class); - types.put(int.class, Integer.class); - types.put(long.class, Long.class); - types.put(float.class, Float.class); - types.put(double.class, Double.class); - types.put(char.class, Character.class); - return types; - } - } - - private static final class ConstantPool { - private final List entries = new ArrayList<>(); - private final Map indices = new LinkedHashMap<>(); - - private int utf8(String value) { - return add("U" + value, new Utf8Constant(value)); - } - - private int classInfo(String internalName) { - int name = utf8(internalName); - return add("C" + internalName, new IndexConstant(7, name)); - } - - private int nameAndType(String name, String descriptor) { - int nameIndex = utf8(name); - int descriptorIndex = utf8(descriptor); - return add( - "N" + name + descriptor, new PairConstant(12, nameIndex, descriptorIndex)); - } - - private int methodRef(String owner, String name, String descriptor) { - int ownerIndex = classInfo(owner); - int nameAndType = nameAndType(name, descriptor); - return add( - "M" + owner + '.' + name + descriptor, - new PairConstant(10, ownerIndex, nameAndType)); - } - - private int add(String key, Constant constant) { - Integer index = indices.get(key); - if (index != null) { - return index; - } - int newIndex = entries.size() + 1; - entries.add(constant); - indices.put(key, newIndex); - return newIndex; - } - - private void write(DataOutputStream output) throws IOException { - output.writeShort(entries.size() + 1); - for (Constant entry : entries) { - entry.write(output); - } - } - } - - private interface Constant { - void write(DataOutputStream output) throws IOException; - } - - private static final class Utf8Constant implements Constant { - private final String value; - - private Utf8Constant(String value) { - this.value = value; - } - - @Override - public void write(DataOutputStream output) throws IOException { - output.writeByte(1); - output.writeUTF(value); - } - } - - private static final class IndexConstant implements Constant { - private final int tag; - private final int index; - - private IndexConstant(int tag, int index) { - this.tag = tag; - this.index = index; - } - - @Override - public void write(DataOutputStream output) throws IOException { - output.writeByte(tag); - output.writeShort(index); - } - } - - private static final class PairConstant implements Constant { - private final int tag; - private final int first; - private final int second; - - private PairConstant(int tag, int first, int second) { - this.tag = tag; - this.first = first; - this.second = second; - } - - @Override - public void write(DataOutputStream output) throws IOException { - output.writeByte(tag); - output.writeShort(first); - output.writeShort(second); - } - } - - private static String constructorDescriptor(Class[] parameterTypes) { - StringBuilder descriptor = new StringBuilder("("); - for (Class parameterType : parameterTypes) { - descriptor.append(descriptor(parameterType)); - } - return descriptor.append(")V").toString(); - } - - private static String descriptor(Class type) { - if (type == void.class) { - return "V"; - } - if (type == boolean.class) { - return "Z"; - } - if (type == byte.class) { - return "B"; - } - if (type == char.class) { - return "C"; - } - if (type == short.class) { - return "S"; - } - if (type == int.class) { - return "I"; - } - if (type == long.class) { - return "J"; - } - if (type == float.class) { - return "F"; - } - if (type == double.class) { - return "D"; - } - if (type.isArray()) { - return type.getName().replace('.', '/'); - } - return 'L' + internalName(type) + ';'; - } - - private static String internalName(Class type) { - return type.getName().replace('.', '/'); - } -} From 6378d05e4988663d1ccabe143c52346451efc2eb Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 02:21:58 +0800 Subject: [PATCH 03/15] fix(json): keep GraalVM metadata JSON-owned --- docs/guide/java/graalvm-support.md | 8 ++++---- .../org/apache/fory/json/ForyJsonGraalVMFeature.java | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/guide/java/graalvm-support.md b/docs/guide/java/graalvm-support.md index 12f8be76e1..824622f7a3 100644 --- a/docs/guide/java/graalvm-support.md +++ b/docs/guide/java/graalvm-support.md @@ -184,10 +184,10 @@ Child codecs act on one direct level. `elementCodec` supports `Collection`, Java `valueCodec` support Map keys and values. A complete `value` codec cannot be combined with a child codec. -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. +An annotation codec must have a public no-argument constructor. Fory prepares that constructor +during Native Image construction, so application modules do not need to export or open the codec +package. A codec instance supplied through `registerCodec` is constructed by the application and +needs no annotation-constructor metadata. ## Basic Usage diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 4b9d02c579..06333ffc83 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -329,7 +329,6 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { if (!processedModels.add(type)) { return false; } - GraalvmSupport.registerClass(type); RuntimeReflection.register(type); registerContainer(type); registerDeclarations(type); @@ -368,7 +367,6 @@ private boolean registerMixin( registerDeclarations(targetType); JsonCodec directTypeCodec = annotations.annotation(targetType, JsonCodec.class); registerCodecs(directTypeCodec); - GraalvmSupport.registerClass(targetType); RuntimeReflection.register(targetType); registerContainer(targetType); boolean intrinsicTarget = @@ -601,7 +599,6 @@ private void prepareRecord(Class type) { } private void registerCreator(Class ownerType, Executable executable) { - GraalvmSupport.registerClass(executable.getDeclaringClass()); RuntimeReflection.register(executable); JsonCreatorInfo.prepareNativeCreator(ownerType, executable); } From 77b1da12a3757f4806a2039160faeb49b7fd8ebf Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 02:33:06 +0800 Subject: [PATCH 04/15] fix(json): remove stale codec factory diagnostic --- .../java/org/apache/fory/json/resolver/JsonSharedRegistry.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 be258383b1..e78fb18a2d 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 @@ -660,8 +660,7 @@ private static GeneratedJsonCodec newGeneratedCodec( public static GeneratedJsonCodec validateGeneratedCodec( Class type, GeneratedJsonCodec codec) { if (codec == null) { - throw new ForyJsonException( - "Generated JSON codec factory returned null for " + type.getName()); + throw new ForyJsonException("Generated JSON codec is null for " + type.getName()); } Class declaredType; JsonFieldAccessor[] accessors; From 685a823d071d36fa314ef13669774c1aab80e3d1 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 02:52:14 +0800 Subject: [PATCH 05/15] fix(json): preserve Java 8 record compatibility --- .../main/java/org/apache/fory/json/meta/JsonFieldAccessor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index a988c257ec..0821f3018d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -32,6 +32,7 @@ import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; +import org.apache.fory.util.record.RecordUtils; /** * Uniform interpreted object-member access for fields, getters, and setters. @@ -164,7 +165,7 @@ public static synchronized void prepareField(Field field) { // native image. JSON retains the semantic field identity but must cache the component accessor // MethodHandle here so interpreted codecs never perform runtime reflection. JsonFieldAccessor accessor = - field.getDeclaringClass().isRecord() + RecordUtils.isRecord(field.getDeclaringClass()) ? new RecordFieldJsonAccessor(field) : new FieldJsonAccessor(FieldAccessor.createAccessor(field)); putPrepared(nativeFields, field, accessor); From 27d3dcf6fe45552b03f4698713f364b02cc7a806 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 03:45:47 +0800 Subject: [PATCH 06/15] fix(json): close GraalVM provider review findings --- .../graalvm/ForyJsonNoProviderExample.java | 42 +++++++++++++++++++ .../graalvm/closed/ClosedJsonConfigs.java | 17 ++++++++ .../json/resolver/JsonSharedRegistry.java | 7 +++- .../fory/json/ForyJsonGraalVMFeature.java | 27 +++++++----- 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java index 3051ddf687..e328a85d4c 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java @@ -31,6 +31,7 @@ import org.apache.fory.json.annotation.JsonCreator; import org.apache.fory.json.annotation.JsonProperty; import org.apache.fory.json.annotation.JsonType; +import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.reader.Latin1JsonReader; @@ -92,6 +93,14 @@ private static void exercise(ForyJson json) { .equals("\u4f60")); Preconditions.checkArgument( json.fromJson(encoded.getBytes(StandardCharsets.UTF_8), Model.class).equals(value)); + Preconditions.checkArgument( + json.toJson(new DirectValueRecord("record-value")).equals("\"record-value\"")); + Preconditions.checkArgument( + json.fromJson("\"decoded-record\"", DirectValueRecord.class) + .equals(new DirectValueRecord("decoded-record"))); + Preconditions.checkArgument(json.toJson(DirectValueEnum.READY).equals("\"ready\"")); + Preconditions.checkArgument( + json.fromJson("\"done\"", DirectValueEnum.class) == DirectValueEnum.DONE); } private static int countOccurrences(String value, String target) { @@ -217,6 +226,39 @@ public int hashCode() { @JsonType public record RecordValue(int rank, String text) {} + @JsonType + public record DirectValueRecord(@JsonValue String value) { + @JsonCreator + public DirectValueRecord {} + } + + @JsonType + public enum DirectValueEnum { + READY("ready"), + DONE("done"); + + private final String value; + + DirectValueEnum(String value) { + this.value = value; + } + + @JsonValue + public String value() { + return value; + } + + @JsonCreator + public static DirectValueEnum fromValue(String value) { + for (DirectValueEnum candidate : values()) { + if (candidate.value.equals(value)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown direct enum value " + value); + } + } + @JsonType public static final class FactoryValue { private final int code; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java index 41e1a5c6a3..b8fab9a5d9 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java @@ -33,6 +33,15 @@ public ClosedJsonConfigs() {} } class ClosedJsonConfigParent { + public ForyJson aRestrictedConfiguration() { + return ForyJson.builder() + .writeNullFields(true) + .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .withTypeChecker((className, context) -> false) + .build(); + } + public ForyJson generatedConfiguration() { return ForyJson.builder() .writeNullFields(true) @@ -40,4 +49,12 @@ public ForyJson generatedConfiguration() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .build(); } + + public ForyJson ignoredOverload(boolean ignored) { + throw new AssertionError("Provider overload must not be invoked"); + } + + public static ForyJson ignoredStaticMethod() { + throw new AssertionError("Static provider helper must not be invoked"); + } } 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 e78fb18a2d..7b0f57c473 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 @@ -216,8 +216,11 @@ public JsonSharedRegistry(JsonConfig config) { private JsonSharedRegistry( JsonConfig config, ExecutorService compilationService, boolean hostedCodegen) { this.customCodecs = config.codecRegistry(); - typeChecker = config.typeChecker(); - typeCheckContext = config.typeCheckContext(); + // Hosted compilation produces classes shared by configurations with the same source shape. + // Runtime type policy is intentionally not part of that shape and remains enforced by each + // runtime resolver before it installs a generated capability. + typeChecker = hostedCodegen ? null : config.typeChecker(); + typeCheckContext = hostedCodegen ? null : config.typeCheckContext(); typeCheckCache = typeChecker == null ? null : new ConcurrentHashMap<>(); typeCheckCacheLock = typeChecker == null ? null : new Object(); this.propertyDiscoveryEnabled = config.propertyDiscoveryEnabled(); diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 06333ffc83..61e0ce4f2c 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -195,12 +195,6 @@ private boolean registerProvider(Class providerClass) { } boolean changed = false; for (Method method : methods) { - if (Modifier.isStatic(method.getModifiers()) || method.getParameterCount() != 0) { - throw providerFailure( - providerClass, - "method must be a non-static zero-argument instance method: " + method, - null); - } MethodHandle providerMethod = providerMethod(providerClass, method); ForyJson json; try { @@ -247,6 +241,8 @@ private static List providerMethods(Class providerClass) { for (Method method : providerClass.getMethods()) { if (method.isBridge() || method.isSynthetic() + || Modifier.isStatic(method.getModifiers()) + || method.getParameterCount() != 0 || method.getReturnType() != ForyJson.class) { continue; } @@ -332,9 +328,18 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { RuntimeReflection.register(type); registerContainer(type); registerDeclarations(type); - if (!type.isEnum() - && !Collection.class.isAssignableFrom(type) - && !Map.class.isAssignableFrom(type)) { + JsonCodec directTypeCodec = type.getDeclaredAnnotation(JsonCodec.class); + boolean hasTypeCodec = + directTypeCodec != null || hasInheritedTypeCodec(type, null); + boolean hasJsonValue = + (!hasTypeCodec || isCompleteTypeCodec(directTypeCodec)) + && registerJsonValueDeclarations(access, type, null); + JsonSubTypes subTypes = type.getDeclaredAnnotation(JsonSubTypes.class); + boolean intrinsicType = + type.isEnum() + || Collection.class.isAssignableFrom(type) + || Map.class.isAssignableFrom(type); + if (!intrinsicType && !hasTypeCodec && !hasJsonValue && subTypes == null) { registerModelHierarchy(access, type); if (type.isRecord()) { prepareRecord(type); @@ -345,7 +350,9 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { } } } - registerSubtypes(access, type); + if (!hasTypeCodec && !hasJsonValue) { + registerSubtypes(access, type); + } return true; } From 67ecd73f478080a969cb5f449437d7d9af303b3d Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 05:49:15 +0800 Subject: [PATCH 07/15] fix(json): finalize GraalVM hosted codec access --- .../src/main/java/module-info.java | 3 +- .../apache/fory/graalvm/ForyJsonExample.java | 104 +++++++--- .../graalvm/closed/ClosedJsonConfigs.java | 3 + java/README.md | 7 +- .../fory/platform/internal/DefineClass.java | 9 +- .../fory/platform/internal/_JDKAccess.java | 17 +- .../apache/fory/json/codegen/JsonCodegen.java | 96 +++++++-- .../fory/json/meta/JsonCreatorInfo.java | 183 +++++++----------- .../json/resolver/JsonSharedRegistry.java | 5 +- .../json/resolver/JsonStringValueCodec.java | 27 +-- .../fory/json/resolver/JsonTypeResolver.java | 37 +++- .../fory/json/ForyJsonGraalVMFeature.java | 17 +- 12 files changed, 298 insertions(+), 210 deletions(-) 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 e514b7beb2..9e5f1f5ab6 100644 --- a/integration_tests/graalvm_tests/src/main/java/module-info.java +++ b/integration_tests/graalvm_tests/src/main/java/module-info.java @@ -22,7 +22,8 @@ requires org.apache.fory.json; requires java.sql; - // Fory-generated codecs and annotation codecs access the test models from library modules. + // Binary serialization acceptance retains its existing exported and opened model packages. + // The Fory JSON closed-package test intentionally uses neither directive. 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 index b91fd55438..0dff8bef3a 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -37,6 +37,8 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.locks.Lock; +import org.apache.fory.codegen.CompileState; import org.apache.fory.graalvm.closed.ClosedJsonConfigs; import org.apache.fory.graalvm.closed.ClosedJsonRecord; import org.apache.fory.json.ForyJson; @@ -46,6 +48,7 @@ import org.apache.fory.json.annotation.JsonBase64; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.annotation.JsonCreator; +import org.apache.fory.json.annotation.JsonIgnore; import org.apache.fory.json.annotation.JsonMixin; import org.apache.fory.json.annotation.JsonProperty; import org.apache.fory.json.annotation.JsonPropertyOrder; @@ -129,6 +132,8 @@ private static void testHostedCodegenConfigurations() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .build(), false); + testIndependentChildCodegen(); + testExternalModuleMixin(); } private static ForyJson newProviderJson() { @@ -136,11 +141,12 @@ private static ForyJson newProviderJson() { .writeNullFields(true) .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .registerMixin(CoreCompileStateMixin.class) .build(); } private static void exerciseCodegenConfiguration(ForyJson json, boolean generated) { - CodegenProbeCodec.expectGenerated = generated; + CodegenProbeCodec.expect(CodegenProbeModel.class, generated); CodegenProbeModel value = new CodegenProbeModel(); value.id = 41; value.probe = new CodegenProbeValue("probe"); @@ -174,10 +180,42 @@ private static int countOccurrences(String value, String target) { } private static void testClosedPackage() { - ForyJson json = ForyJson.builder().build(); ClosedJsonRecord value = new ClosedJsonRecord(17, "closed"); + ForyJson interpreted = ForyJson.builder().build(); + String interpretedJson = interpreted.toJson(value); + Preconditions.checkArgument( + interpreted.fromJson(interpretedJson, ClosedJsonRecord.class).equals(value)); + + ForyJson generated = newProviderJson(); + String generatedJson = generated.toJson(value); + Preconditions.checkArgument( + generated.fromJson(generatedJson, ClosedJsonRecord.class).equals(value)); + } + + private static void testIndependentChildCodegen() { + CodegenProbeCodec.expect(PublicChild.class, true); + ForyJson json = newProviderJson(); + PackagePrivateOwner value = new PackagePrivateOwner(); + value.child.name = "child"; + value.child.probe = new CodegenProbeValue("probe"); + String encoded = json.toJson(value); + Preconditions.checkArgument( + json.fromJson(encoded, PackagePrivateOwner.class).child.name.equals("child")); + String utf16 = encoded.replace(":\"probe\"", ":\"ä½ \""); + Preconditions.checkArgument( + json.fromJson(utf16, PackagePrivateOwner.class).child.probe.value.equals("ä½ ")); + byte[] utf8 = json.toJsonBytes(value); + Preconditions.checkArgument( + json.fromJson(utf8, PackagePrivateOwner.class).child.probe.value.equals("probe")); + } + + private static void testExternalModuleMixin() { + ForyJson json = newProviderJson(); + CompileState value = new CompileState(); + value.finished = true; String encoded = json.toJson(value); - Preconditions.checkArgument(json.fromJson(encoded, ClosedJsonRecord.class).equals(value)); + Preconditions.checkArgument(encoded.equals("{\"finished\":true}")); + Preconditions.checkArgument(json.fromJson(encoded, CompileState.class).finished); } private static void testMixin() { @@ -496,57 +534,43 @@ private CodegenProbeValue(String value) { } public static final class CodegenProbeCodec implements JsonValueCodec { + private static Class expectedType; private static boolean expectGenerated; public CodegenProbeCodec() {} + private static void expect(Class type, boolean generated) { + expectedType = type; + expectGenerated = generated; + } + @Override public void writeString(StringJsonWriter writer, CodegenProbeValue value) { - checkCapability( - writer - .typeResolver() - .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) - .stringWriter()); + checkCapability(writer.typeResolver().getTypeInfo(expectedType, expectedType).stringWriter()); writer.writeString(value == null ? null : value.value); } @Override public void writeUtf8(Utf8JsonWriter writer, CodegenProbeValue value) { - checkCapability( - writer - .typeResolver() - .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) - .utf8Writer()); + checkCapability(writer.typeResolver().getTypeInfo(expectedType, expectedType).utf8Writer()); writer.writeString(value == null ? null : value.value); } @Override public CodegenProbeValue readLatin1(Latin1JsonReader reader) { - checkCapability( - reader - .typeResolver() - .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) - .latin1Reader()); + checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).latin1Reader()); return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); } @Override public CodegenProbeValue readUtf16(Utf16JsonReader reader) { - checkCapability( - reader - .typeResolver() - .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) - .utf16Reader()); + checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).utf16Reader()); return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); } @Override public CodegenProbeValue readUtf8(Utf8JsonReader reader) { - checkCapability( - reader - .typeResolver() - .getTypeInfo(CodegenProbeModel.class, CodegenProbeModel.class) - .utf8Reader()); + checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).utf8Reader()); return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); } @@ -556,6 +580,30 @@ private static void checkCapability(Object capability) { } } + @JsonType + static final class PackagePrivateOwner { + public PublicChild child = new PublicChild(); + + PackagePrivateOwner() {} + } + + @JsonType + public static final class PublicChild { + public String name; + + @JsonCodec(CodegenProbeCodec.class) + public CodegenProbeValue probe; + + public PublicChild() {} + } + + @JsonMixin(target = CompileState.class) + public abstract static class CoreCompileStateMixin { + @JsonIgnore private Lock lock; + @JsonProperty private boolean finished; + @JsonIgnore private Map result; + } + public static class Parent { private int inheritedId = 10; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java index b8fab9a5d9..7c8ad2e5b3 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java @@ -21,6 +21,7 @@ import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeCodec; import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeValue; +import org.apache.fory.graalvm.ForyJsonExample.CoreCompileStateMixin; import org.apache.fory.graalvm.ForyJsonExample.InheritedJsonConfig; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; @@ -38,6 +39,7 @@ public ForyJson aRestrictedConfiguration() { .writeNullFields(true) .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .registerMixin(CoreCompileStateMixin.class) .withTypeChecker((className, context) -> false) .build(); } @@ -47,6 +49,7 @@ public ForyJson generatedConfiguration() { .writeNullFields(true) .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .registerMixin(CoreCompileStateMixin.class) .build(); } diff --git a/java/README.md b/java/README.md index b0fa0de7b8..237b8defd0 100644 --- a/java/README.md +++ b/java/README.md @@ -472,9 +472,10 @@ annotations, custom codecs, security controls, and platform setup. ## GraalVM Native Image Fory supports GraalVM Native Image without application reflection configuration. Binary -serialization generates serializers while the image is built; the Fory annotation processor -generates type-owned execution companions for Fory JSON `@JsonType` models. Build your native image -as follows: +serialization generates serializers while the image is built. Fory JSON discovers reachable +`@JsonType` and `@JsonMixin` models directly, without the annotation processor. Interpreted JSON +codecs work by default; return configurations from a reachable `@ForyJsonProvider` to generate +matching JSON codecs while the image is built. Build your native image as follows: ```bash # Generate serializers at build time diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java index adfd99a645..cf490bd1a4 100644 --- a/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java +++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/DefineClass.java @@ -49,10 +49,11 @@ public static Class defineClass( Preconditions.checkNotNull(loader); Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 8); if (neighbor != null && JdkVersion.MAJOR_VERSION >= 9) { - // classes in bytecode must be in same package as lookup class. - MethodHandles.Lookup lookup = MethodHandles.lookup(); - _JDKAccess.addReads(_JDKAccess.getModule(DefineClass.class), _JDKAccess.getModule(neighbor)); - lookup = _Lookup.privateLookupIn(neighbor, lookup); + // A normal privateLookupIn would make ordinary generated classes depend on the application + // package being open to Fory. The target-class trusted lookup defines the class in the + // neighbor's loader, runtime package, module, and protection domain without that user-facing + // module contract. Classes in bytecode must still be in the neighbor's package. + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(neighbor); return _Lookup.defineClass(lookup, bytecodes); } if (classloaderDefineClassHandle == null) { diff --git a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java index c989207e2c..d88bba3cf2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java +++ b/java/fory-core/src/main/java/org/apache/fory/platform/internal/_JDKAccess.java @@ -345,18 +345,25 @@ public static boolean isExported(Class cls) { } } - // caller sensitive, must use MethodHandle to walk around the check. private static volatile MethodHandle addReadsHandle; public static Object addReads(Object thisModule, Object otherModule) { Preconditions.checkArgument(JdkVersion.MAJOR_VERSION >= 9); try { if (addReadsHandle == null) { - Class cls = Class.forName("java.lang.Module"); - MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(cls); - addReadsHandle = lookup.findVirtual(cls, "addReads", MethodType.methodType(cls, cls)); + Class moduleClass = Class.forName("java.lang.Module"); + Class modulesClass = Class.forName("jdk.internal.module.Modules"); + MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(modulesClass); + // Module.addReads is caller-sensitive and rejects changes to another module even through a + // trusted handle. The JDK module-graph owner exposes the corresponding privileged update. + addReadsHandle = + lookup.findStatic( + modulesClass, + "addReads", + MethodType.methodType(void.class, moduleClass, moduleClass)); } - return addReadsHandle.invoke(thisModule, otherModule); + addReadsHandle.invoke(thisModule, otherModule); + return thisModule; } catch (Throwable e) { throw ExceptionUtils.throwException(e); } 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 68aa8690c6..99b6735a78 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 @@ -52,6 +52,8 @@ import org.apache.fory.json.meta.JsonFieldKind; import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.platform.internal.DefineClass; +import org.apache.fory.platform.internal._JDKAccess; /** * Generates concrete object and exact-collection capability classes. @@ -79,6 +81,7 @@ public final class JsonCodegen { private final int codegenHash; private final CodeGenerator codeGenerator; private final ClassLoader jsonLoader; + private final boolean hostedCodegen; static String generatedCodecType(CodegenContext ctx, Class codecType) { // Janino-generated serializers use erased types, matching Fory core code generation. Runtime @@ -91,9 +94,10 @@ static String generatedCodecArrayType(CodegenContext ctx, Class arrayType) { return ctx.type(arrayType); } - public JsonCodegen(int codegenHash, ClassLoader jsonLoader) { + public JsonCodegen(int codegenHash, ClassLoader jsonLoader, boolean hostedCodegen) { this.codegenHash = codegenHash; this.jsonLoader = jsonLoader; + this.hostedCodegen = hostedCodegen; codeGenerator = new CodeGenerator(jsonLoader); } @@ -218,7 +222,7 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv String code = new StringWriterCodegen(this, resolver) .genUnwrappedWriterCode(builder, type, codec, unwrapped); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.writeFields(); @@ -227,7 +231,7 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv new JsonGeneratedCodecBuilder(generatedPackage, className, type); String code = new StringWriterCodegen(this, resolver).genAnyWriterCode(builder, type, properties, any); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } Function source = groupEnds -> { @@ -237,7 +241,7 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv .genWriterCode(builder, type, properties, groupEnds); }; return compileWriterClass( - generatedPackage, className, properties, "writeString", "writeStringMembers", source); + type, generatedPackage, className, properties, "writeString", "writeStringMembers", source); } private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver) { @@ -251,7 +255,7 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver String code = new Utf8WriterCodegen(this, resolver, false) .genUnwrappedWriterCode(builder, type, codec, unwrapped); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.writeFields(); @@ -261,7 +265,7 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver String code = new Utf8WriterCodegen(this, resolver, false) .genAnyWriterCode(builder, type, properties, any); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } Function normalSource = groupEnds -> { @@ -293,11 +297,11 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver int expandedSize = methodSize(codeStats(generatedPackage, className, expandedSource), "writeUtf8"); if (expandedSize > HOT_INLINE_LIMIT) { - return compileCodecClass(generatedPackage, className, expandedSource); + return compileObjectCodecClass(type, generatedPackage, className, expandedSource); } } return compileUtf8WriterClass( - generatedPackage, className, properties, "writeUtf8", directSource, groupedSource); + type, generatedPackage, className, properties, "writeUtf8", directSource, groupedSource); } private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolver) { @@ -311,7 +315,7 @@ private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolv String code = new Latin1ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.readFields(); @@ -325,6 +329,7 @@ private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolv : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any); }; return compileReaderClass( + type, generatedPackage, className, properties.length, @@ -344,7 +349,7 @@ private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolve String code = new Utf16ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.readFields(); @@ -358,6 +363,7 @@ private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolve : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any); }; return compileReaderClass( + type, generatedPackage, className, properties.length, @@ -378,7 +384,7 @@ private Class buildUtf8Reader( String code = new Utf8ReaderCodegen(this, resolver, finalDependencies) .genUnwrappedReaderCode(builder, type, codec, unwrapped); - return compileCodecClass(generatedPackage, className, code); + return compileObjectCodecClass(type, generatedPackage, className, code); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.readFields(); @@ -393,6 +399,7 @@ private Class buildUtf8Reader( : reader.genAnyReaderCode(builder, type, properties, codec.creatorInfo(), any); }; return compileReaderClass( + type, generatedPackage, className, properties.length, @@ -402,6 +409,7 @@ private Class buildUtf8Reader( } private Class compileReaderClass( + Class ownerType, String generatedPackage, String className, int propertyCount, @@ -412,10 +420,11 @@ private Class compileReaderClass( groupable ? readerGroupEnds(generatedPackage, className, propertyCount, readMethod, source) : oneGroup(propertyCount); - return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); + return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(groupEnds)); } private Class compileWriterClass( + Class ownerType, String generatedPackage, String className, JsonFieldInfo[] properties, @@ -423,7 +432,7 @@ private Class compileWriterClass( String memberMethod, Function source) { if (properties.length < 2) { - return compileCodecClass(generatedPackage, className, source.apply(null)); + return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(null)); } // Group only the bytecode emitted in this generated class. A callee with its own stable // boundary contributes its invocation, not the body that C2 must keep in the callee. @@ -431,7 +440,7 @@ private Class compileWriterClass( JaninoUtils.CodeStats oneGroupStats = codeStats(generatedPackage, className, source.apply(oneGroup)); if (privateMethodSize(oneGroupStats, writeMethod + "Object") <= HOT_INLINE_LIMIT) { - return compileCodecClass(generatedPackage, className, source.apply(null)); + return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(null)); } int[] groupEnds = writerGroupEnds( @@ -442,10 +451,11 @@ private Class compileWriterClass( writeMethod, memberMethod, source); - return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); + return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(groupEnds)); } private Class compileUtf8WriterClass( + Class ownerType, String generatedPackage, String className, JsonFieldInfo[] properties, @@ -455,19 +465,19 @@ private Class compileUtf8WriterClass( if (properties.length < 2 || methodSize(codeStats(generatedPackage, className, directSource), writeMethod) <= HOT_INLINE_LIMIT) { - return compileCodecClass(generatedPackage, className, directSource); + return compileObjectCodecClass(ownerType, generatedPackage, className, directSource); } int firstGroupMember = JsonWriterCodegen.firstGroupMember(properties); if (properties.length - firstGroupMember < 2) { - return compileCodecClass(generatedPackage, className, directSource); + return compileObjectCodecClass(ownerType, generatedPackage, className, directSource); } int[] groupEnds = utf8WriterGroupEnds( generatedPackage, className, properties.length, firstGroupMember, writeMethod, source); if (groupEnds.length < 2) { - return compileCodecClass(generatedPackage, className, directSource); + return compileObjectCodecClass(ownerType, generatedPackage, className, directSource); } - return compileCodecClass(generatedPackage, className, source.apply(groupEnds)); + return compileObjectCodecClass(ownerType, generatedPackage, className, source.apply(groupEnds)); } private int[] utf8WriterGroupEnds( @@ -645,6 +655,22 @@ private int[] toIntArray(List values) { return result; } + private Class compileObjectCodecClass( + Class ownerType, String generatedPackage, String className, String code) { + if (!hostedCodegen || _JDKAccess.isExported(ownerType)) { + return compileCodecClass(generatedPackage, className, code); + } + try { + // A codec for a concealed model package must live beside the model to access its public + // members without an application export or open. Exported and bootstrap models stay in the + // generated loader, which also avoids changing their module graph. + CompileUnit unit = new CompileUnit(generatedPackage, className, code); + return compileHostedClass(ownerType, unit); + } catch (Throwable e) { + throw new ForyJsonException("Cannot compile generated JSON codec " + className, e); + } + } + private Class compileCodecClass(String generatedPackage, String className, String code) { try { CompileUnit unit = new CompileUnit(generatedPackage, className, code); @@ -655,6 +681,38 @@ private Class compileCodecClass(String generatedPackage, String className, St } } + private Class compileHostedClass(Class ownerType, CompileUnit unit) { + Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); + String mainClassName = unit.getQualifiedClassName(); + String mainClassPath = mainClassName.replace('.', '/') + ".class"; + byte[] mainBytecode = classes.get(mainClassPath); + if (mainBytecode == null) { + throw new ForyJsonException("Missing generated JSON codec bytecode " + mainClassName); + } + ClassLoader ownerLoader = ownerType.getClassLoader(); + if (ownerLoader == null) { + throw new ForyJsonException( + "Cannot define generated JSON codec beside bootstrap type " + ownerType.getName()); + } + Object ownerModule = _JDKAccess.getModule(ownerType); + // The generated source names APIs from both JSON and core. A concealed third-party model + // package may not already read either module, so establish only those two implementation + // dependencies before defining the ordinary class in the model module. + _JDKAccess.addReads(ownerModule, _JDKAccess.getModule(JsonCodegen.class)); + _JDKAccess.addReads(ownerModule, _JDKAccess.getModule(DefineClass.class)); + Class mainClass = + DefineClass.defineClass( + mainClassName, ownerType, ownerLoader, ownerType.getProtectionDomain(), mainBytecode); + for (Map.Entry entry : classes.entrySet()) { + if (!entry.getKey().equals(mainClassPath)) { + String className = CodeGenerator.fullClassNameFromClassFilePath(entry.getKey()); + DefineClass.defineClass( + className, ownerType, ownerLoader, ownerType.getProtectionDomain(), entry.getValue()); + } + } + return mainClass; + } + @Internal public boolean canCompileWriter(ObjectCodec codec) { if (!canCompileType(codec.type())) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java index 3fdd85c970..557722dfed 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java @@ -21,15 +21,11 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; -import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; @@ -37,6 +33,7 @@ import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; +import org.apache.fory.util.ExceptionUtils; /** * Immutable ordered construction metadata for one JSON object codec. @@ -49,10 +46,16 @@ */ @Internal public final class JsonCreatorInfo { - private static Map nativeInvokers = new HashMap<>(); - private static Map nativeStringInvokers = new HashMap<>(); - private static Map> nativeConstructors = new HashMap<>(); - private static boolean nativeCreatorsFrozen; + private static final MethodHandle NATIVE_CONSTRUCTOR_INVOKER = + prepareNativeInvoker( + Constructor.class, + "newInstanceWithCaller", + MethodType.methodType(Object.class, Object[].class, boolean.class, Class.class)); + private static final MethodHandle NATIVE_FACTORY_INVOKER = + prepareNativeInvoker( + Method.class, + "invoke", + MethodType.methodType(Object.class, Object.class, Object[].class, Class.class)); private final Class ownerType; private final Executable executable; @@ -60,7 +63,6 @@ public final class JsonCreatorInfo { private final Object[] defaults; private final long[] hashes; private final MethodHandle invoker; - private final Constructor nativeConstructor; private final GeneratedJsonCodec generatedCodec; public JsonCreatorInfo( @@ -74,9 +76,8 @@ public JsonCreatorInfo( this.fields = fields; this.defaults = defaults; this.generatedCodec = generatedCodec; - nativeConstructor = generatedCodec == null ? nativeConstructor(executable) : null; invoker = - generatedCodec == null && nativeConstructor == null && !GraalvmSupport.isGraalBuildTime() + generatedCodec == null && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? buildInvoker(ownerType, executable, executable.getParameterCount()) : null; hashes = new long[fields.length]; @@ -130,18 +131,16 @@ public Object create(Object[] arguments) { } try { Object value; - if (nativeConstructor != null) { - value = nativeConstructor.newInstance(arguments); - } else if (executable instanceof Constructor) { - value = ((Constructor) executable).newInstance(arguments); + if (executable instanceof Constructor) { + value = invokeConstructor((Constructor) executable, arguments); } else { - value = ((Method) executable).invoke(null, arguments); + value = invokeFactory((Method) executable, arguments); } return requireResult(value); - } catch (InstantiationException | IllegalAccessException e) { - throw new ForyJsonException("Failed to invoke JSON creator for " + ownerType.getName(), e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); + } catch (Throwable cause) { + if (cause instanceof InvocationTargetException) { + cause = cause.getCause(); + } if (cause instanceof Error) { throw (Error) cause; } @@ -178,13 +177,6 @@ private static MethodHandle buildInvoker( executable.setAccessible(true); return null; } - if (GraalvmSupport.isGraalRuntime()) { - MethodHandle invoker = nativeInvokers.get(executable); - if (invoker == null) { - throw missingNativeCreator(executable); - } - return invoker; - } MethodHandle target = creatorTarget(ownerType, executable); // The interpreted reader already owns one trusted fixed-size argument array. Spread that // exact array into the creator without a second carrier or per-call reflective access check. @@ -193,109 +185,82 @@ private static MethodHandle buildInvoker( .asType(MethodType.methodType(Object.class, Object[].class)); } - /** Returns the cached one-String-argument creator used by a JsonValue representation. */ + /** Returns the one-String-argument creator used by a JsonValue representation. */ @Internal public static MethodHandle stringCreatorHandle(Class ownerType, Executable executable) { - if (GraalvmSupport.isGraalRuntime()) { - MethodHandle invoker = nativeStringInvokers.get(executable); - if (invoker == null) { - throw missingNativeCreator(executable); - } - return invoker; - } return creatorTarget(ownerType, executable) .asType(MethodType.methodType(Object.class, String.class)); } - /** Returns the prepared Native Image constructor, or {@code null} outside native runtime. */ + /** Invokes a creator constructor using the prepared Native Image access path when required. */ @Internal - public static Constructor nativeConstructor(Executable executable) { - if (!GraalvmSupport.isGraalRuntime() || !(executable instanceof Constructor)) { - return null; - } - Constructor constructor = nativeConstructors.get(executable); - if (constructor == null) { - throw missingNativeCreator(executable); + public static Object invokeConstructor(Constructor constructor, Object[] arguments) { + if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + try { + return constructor.newInstance(arguments); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } } - return constructor; - } - - private static MethodHandle creatorTarget(Class ownerType, Executable executable) { try { - // A target-class trusted lookup has full member access without requiring the application - // module to export or open its model package. Native Image retains final factory handles; - // constructor creators use the separately registered Constructor cache. - return executable instanceof Constructor - ? _JDKAccess._trustedLookup(ownerType).unreflectConstructor((Constructor) executable) - : _JDKAccess._trustedLookup(executable.getDeclaringClass()) - .unreflect((Method) executable); - } catch (IllegalAccessException e) { - throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e); + // Creator validation already requires a public executable. Checking access as the declaring + // class preserves that contract without requiring its package to be exported or open. + Class caller = constructor.getDeclaringClass(); + return (Object) NATIVE_CONSTRUCTOR_INVOKER.invokeExact(constructor, arguments, true, caller); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); } } - private static ForyJsonException missingNativeCreator(Executable executable) { - return new ForyJsonException( - "Missing Native Image Fory JSON creator metadata for " + executable); - } - - /** Prepares the Native Image runtime access for one object creator. */ + /** Invokes a static creator method using the prepared Native Image access path when required. */ @Internal - public static synchronized void prepareNativeCreator(Class ownerType, Executable executable) { - if (!GraalvmSupport.isGraalBuildTime() || nativeCreatorsFrozen) { - throw new IllegalStateException("Fory JSON native creator cache is not writable"); - } - if (executable instanceof Constructor) { - Constructor constructor = (Constructor) executable; - makeAccessible(constructor); - nativeConstructors.putIfAbsent(executable, constructor); - return; + public static Object invokeFactory(Method factory, Object[] arguments) { + if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + try { + return factory.invoke(null, arguments); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); + } } - MethodHandle target = creatorTarget(ownerType, executable); - nativeInvokers.putIfAbsent( - executable, - target - .asSpreader(Object[].class, executable.getParameterCount()) - .asType(MethodType.methodType(Object.class, Object[].class))); - if (executable.getParameterCount() == 1 && executable.getParameterTypes()[0] == String.class) { - nativeStringInvokers.putIfAbsent( - executable, target.asType(MethodType.methodType(Object.class, String.class))); + try { + // Method.invoke is caller-sensitive; use the declaring class for the same module-access + // contract as constructor invocation above. + Class caller = factory.getDeclaringClass(); + return (Object) NATIVE_FACTORY_INVOKER.invokeExact(factory, (Object) null, arguments, caller); + } catch (Throwable e) { + throw ExceptionUtils.throwException(e); } } - private static void makeAccessible(AccessibleObject member) { + private static MethodHandle prepareNativeInvoker( + Class ownerType, String name, MethodType methodType) { + if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return null; + } try { - // setAccessible0 is the JDK's access-check-free operation. Invoking it through the trusted - // lookup preserves access to closed application modules without an exports/opens contract. - _JDKAccess._trustedLookup(AccessibleObject.class) - .findVirtual( - AccessibleObject.class, - "setAccessible0", - MethodType.methodType(boolean.class, boolean.class)) - .invoke(member, true); - } catch (Throwable e) { - throw new ForyJsonException("Cannot prepare Native Image JSON creator " + member, e); + return _JDKAccess._trustedLookup(ownerType).findVirtual(ownerType, name, methodType); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new ForyJsonException("Cannot prepare Native Image JSON creator invocation", e); } } - /** Freezes all Native Image object creator access after hosted analysis. */ - @Internal - public static synchronized void freezeNativeCreators() { - if (nativeCreatorsFrozen) { - return; + private static MethodHandle creatorTarget(Class ownerType, Executable executable) { + try { + // A target-class trusted lookup has full member access without requiring the application + // module to export or open its model package. + if (executable instanceof Constructor) { + return _JDKAccess._trustedLookup(ownerType) + .findConstructor( + ownerType, MethodType.methodType(void.class, executable.getParameterTypes())); + } + Method factory = (Method) executable; + return _JDKAccess._trustedLookup(factory.getDeclaringClass()) + .findStatic( + factory.getDeclaringClass(), + factory.getName(), + MethodType.methodType(factory.getReturnType(), factory.getParameterTypes())); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e); } - nativeInvokers = - nativeInvokers.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(nativeInvokers)); - nativeStringInvokers = - nativeStringInvokers.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(nativeStringInvokers)); - nativeConstructors = - nativeConstructors.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(nativeConstructors)); - nativeCreatorsFrozen = true; } } 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 7b0f57c473..31407c5b91 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 @@ -252,7 +252,10 @@ private JsonSharedRegistry( this.hostedCodegen = hostedCodegen; boolean createCompiler = codegenEnabled && (hostedCodegen || !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE); - codegen = createCompiler ? new JsonCodegen(config.getCodegenHash(), classLoader) : null; + codegen = + createCompiler + ? new JsonCodegen(config.getCodegenHash(), classLoader, hostedCodegen) + : null; nativeCodegenKey = codegenEnabled && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? config.codegenKey() : null; asyncCompilationEnabled = createCompiler && !hostedCodegen && config.asyncCompilationEnabled(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java index c43026b1cb..b41c5d2aa3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java @@ -148,11 +148,6 @@ final Object requireResult(Object result) { return result; } - final ForyJsonException invocationFailure(Throwable cause) { - return new ForyJsonException( - "Failed to invoke JSON creator for " + ownerType.getName(), cause); - } - final ForyJsonException creatorFailure(Throwable cause) { if (cause instanceof Error) { throw (Error) cause; @@ -165,15 +160,11 @@ static ValueCreator forExecutable( if (generatedCodec != null) { return new GeneratedCreator(ownerType, generatedCodec); } - if (GraalvmSupport.isGraalBuildTime()) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { return executable instanceof Constructor ? new ConstructorCreator(ownerType, (Constructor) executable) : new FactoryCreator(ownerType, (Method) executable); } - Constructor nativeConstructor = JsonCreatorInfo.nativeConstructor(executable); - if (nativeConstructor != null) { - return new ConstructorCreator(ownerType, nativeConstructor); - } if (!AndroidSupport.IS_ANDROID) { return new MethodHandleCreator(ownerType, buildInvoker(ownerType, executable)); } @@ -236,11 +227,9 @@ private ConstructorCreator(Class ownerType, Constructor constructor) { @Override Object create(String value) { try { - return requireResult(constructor.newInstance(value)); - } catch (InstantiationException | IllegalAccessException e) { - throw invocationFailure(e); - } catch (InvocationTargetException e) { - throw creatorFailure(e.getCause()); + return requireResult(JsonCreatorInfo.invokeConstructor(constructor, new Object[] {value})); + } catch (Throwable cause) { + throw creatorFailure(cause instanceof InvocationTargetException ? cause.getCause() : cause); } } } @@ -256,11 +245,9 @@ private FactoryCreator(Class ownerType, Method factory) { @Override Object create(String value) { try { - return requireResult(factory.invoke(null, value)); - } catch (IllegalAccessException e) { - throw invocationFailure(e); - } catch (InvocationTargetException e) { - throw creatorFailure(e.getCause()); + return requireResult(JsonCreatorInfo.invokeFactory(factory, new Object[] {value})); + } catch (Throwable cause) { + throw creatorFailure(cause instanceof InvocationTargetException ? cause.getCause() : cause); } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index ac864357ee..bb1a2877d9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -356,6 +356,21 @@ public JsonTypeInfo getTypeInfo(Type declaredType, Class fallback, JsonFormat } } + /** Generates every hosted capability rooted at an explicitly selected Native Image model. */ + @Internal + public void generateHostedCodecs(Class type) { + if (!sharedRegistry.hostedCodegen()) { + throw new IllegalStateException("Hosted JSON codec generation requires a hosted registry"); + } + JsonTypeInfo typeInfo = getTypeInfo(type, type); + ArrayList roots = new ArrayList<>(1); + roots.add(typeInfo); + // A preceding selected model may already have resolved this type inside an uncodegenable graph. + // Cached metadata is still a generation root; otherwise that earlier graph can suppress every + // capability for an independently eligible annotated model. + requestCapabilities(roots); + } + private JsonTypeInfo resolveTypeInfo(Type declaredType, Class rawType, Object key) { JsonTypeInfo typeInfo = customTypeInfo(declaredType, rawType); if (typeInfo != null) { @@ -1782,7 +1797,18 @@ private static void installSubtypeReaders( private void requestCapabilities(ArrayList roots) { for (CapabilityKind kind : CapabilityKind.values()) { CapabilityGraph graph = new CapabilityGraph(kind); - if (graph.addRoots(roots) && !graph.ordered.isEmpty()) { + for (int i = 0; i < roots.size(); i++) { + JsonTypeInfo root = roots.get(i); + // Probe each cold root independently so an interpreter-only graph cannot reject unrelated + // eligible roots. Successful roots are then rebuilt into one graph to preserve the existing + // atomic parent/child publication boundary. + CapabilityGraph candidate = new CapabilityGraph(kind); + if (candidate.addDependency(root) && !graph.addDependency(root)) { + throw new IllegalStateException( + "Cannot merge eligible JSON capability graph for " + root.type()); + } + } + if (!graph.ordered.isEmpty()) { requestGraph(graph); } } @@ -1831,15 +1857,6 @@ private CapabilityGraph(CapabilityKind kind) { this.kind = kind; } - private boolean addRoots(ArrayList roots) { - for (int i = 0; i < roots.size(); i++) { - if (!addDependency(roots.get(i))) { - return false; - } - } - return true; - } - private boolean addDependency(JsonTypeInfo typeInfo) { return addDependency(typeInfo, false); } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 61e0ce4f2c..02ca23d698 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -61,7 +61,6 @@ import org.apache.fory.json.codec.Base64ByteArrayCodec; import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.ScalarCodecs; -import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry.JsonMixinView; @@ -284,7 +283,7 @@ private boolean generateConfigurations(DuringAnalysisAccess access) { continue; } try { - configuration.resolver.getTypeInfo(model, model); + configuration.resolver.generateHostedCodecs(model); } catch (RuntimeException | LinkageError e) { throw new IllegalStateException( "Cannot generate Fory JSON codecs for " + model.getName(), e); @@ -480,12 +479,12 @@ && annotation(annotations, method, JsonValue.class) != null) { } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - registerCreator(type, constructor); + registerCreator(constructor); } } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - registerCreator(type, method); + registerCreator(method); } } return true; @@ -509,7 +508,6 @@ private void registerReflectiveDeclarations(Set declarations) public void afterAnalysis(AfterAnalysisAccess access) { JsonGeneratedClassRegistry.freeze(); JsonFieldAccessor.freezeNativeAccessors(); - JsonCreatorInfo.freezeNativeCreators(); ObjectCodec.freezeNativeAnySetters(); } @@ -578,7 +576,7 @@ boolean record = type.isRecord(); } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - registerCreator(type, constructor); + registerCreator(constructor); registerParameterCodecs(annotations, constructor.getParameters()); registerResolvedParameterTypes(ownerType, constructor.getParameters()); registerUnwrappedParameters( @@ -587,7 +585,7 @@ boolean record = type.isRecord(); } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - registerCreator(type, method); + registerCreator(method); registerParameterCodecs(annotations, method.getParameters()); registerResolvedParameterTypes(ownerType, method.getParameters()); registerUnwrappedParameters(access, ownerType, annotations, method.getParameters()); @@ -602,12 +600,11 @@ private void prepareRecord(Class type) { JsonFieldAccessor.prepareGetter(component.getAccessor()); } Constructor constructor = RecordUtils.getRecordConstructor(type).f0; - registerCreator(type, constructor); + registerCreator(constructor); } - private void registerCreator(Class ownerType, Executable executable) { + private void registerCreator(Executable executable) { RuntimeReflection.register(executable); - JsonCreatorInfo.prepareNativeCreator(ownerType, executable); } private static void prepareMethodAccessors(JsonMixinView annotations, Method method) { From 836e02d5c052665bf01bf8be0eca65f1f775bcd1 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 06:29:01 +0800 Subject: [PATCH 08/15] fix(json): preserve hosted loader coverage --- .../apache/fory/graalvm/ForyJsonExample.java | 37 ++++++ .../graalvm/ForyJsonNoProviderExample.java | 28 +++++ .../graalvm/closed/ClosedJsonConfigs.java | 5 + .../json/resolver/JsonSharedRegistry.java | 4 - .../fory/json/resolver/JsonTypeResolver.java | 32 ++++-- .../fory/json/ForyJsonGraalVMFeature.java | 106 ++++++++---------- 6 files changed, 139 insertions(+), 73 deletions(-) diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java index 0dff8bef3a..e5557ff620 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -132,6 +132,7 @@ private static void testHostedCodegenConfigurations() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .build(), false); + testEmptyMixinCodegen(); testIndependentChildCodegen(); testExternalModuleMixin(); } @@ -142,9 +143,20 @@ private static ForyJson newProviderJson() { .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) + .registerMixin(EmptyMixin.class) .build(); } + private static void testEmptyMixinCodegen() { + CodegenProbeCodec.expect(EmptyMixinTarget.class, true); + ForyJson json = newProviderJson(); + EmptyMixinTarget value = new EmptyMixinTarget(); + value.probe = new CodegenProbeValue("empty-mixin"); + String encoded = json.toJson(value); + Preconditions.checkArgument( + json.fromJson(encoded, EmptyMixinTarget.class).probe.value.equals("empty-mixin")); + } + private static void exerciseCodegenConfiguration(ForyJson json, boolean generated) { CodegenProbeCodec.expect(CodegenProbeModel.class, generated); CodegenProbeModel value = new CodegenProbeModel(); @@ -525,6 +537,31 @@ public CodegenProbeChild(String name) { } } + public static final class EmptyMixinTarget { + @JsonCodec(CodegenProbeCodec.class) + private CodegenProbeValue probe; + + public EmptyMixinTarget() {} + } + + @JsonMixin(target = EmptyMixinTarget.class) + public interface EmptyMixin {} + + /** Hosted-only loader which makes the first equivalent provider unable to compile one model. */ + public static final class CodegenRejectingClassLoader extends ClassLoader { + public CodegenRejectingClassLoader() { + super(ForyJsonExample.class.getClassLoader()); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.equals(CodegenProbeModel.class.getName())) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + } + public static final class CodegenProbeValue { private final String value; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java index e328a85d4c..e6fc47119c 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java @@ -29,6 +29,7 @@ import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.annotation.JsonCreator; +import org.apache.fory.json.annotation.JsonMixin; import org.apache.fory.json.annotation.JsonProperty; import org.apache.fory.json.annotation.JsonType; import org.apache.fory.json.annotation.JsonValue; @@ -101,6 +102,16 @@ private static void exercise(ForyJson json) { Preconditions.checkArgument(json.toJson(DirectValueEnum.READY).equals("\"ready\"")); Preconditions.checkArgument( json.fromJson("\"done\"", DirectValueEnum.class) == DirectValueEnum.DONE); + + ForyJson emptyMixinJson = ForyJson.builder().registerMixin(EmptyMixin.class).build(); + EmptyMixinTarget emptyMixin = new EmptyMixinTarget(); + emptyMixin.setName("empty-mixin"); + String emptyMixinEncoded = emptyMixinJson.toJson(emptyMixin); + Preconditions.checkArgument( + emptyMixinJson + .fromJson(emptyMixinEncoded, EmptyMixinTarget.class) + .getName() + .equals("empty-mixin")); } private static int countOccurrences(String value, String target) { @@ -113,6 +124,23 @@ private static int countOccurrences(String value, String target) { return count; } + public static final class EmptyMixinTarget { + private String name; + + public EmptyMixinTarget() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @JsonMixin(target = EmptyMixinTarget.class) + public interface EmptyMixin {} + @JsonType public static final class Model { private final int id; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java index 7c8ad2e5b3..ec0f4bd5ec 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/closed/ClosedJsonConfigs.java @@ -21,7 +21,9 @@ import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeCodec; import org.apache.fory.graalvm.ForyJsonExample.CodegenProbeValue; +import org.apache.fory.graalvm.ForyJsonExample.CodegenRejectingClassLoader; import org.apache.fory.graalvm.ForyJsonExample.CoreCompileStateMixin; +import org.apache.fory.graalvm.ForyJsonExample.EmptyMixin; import org.apache.fory.graalvm.ForyJsonExample.InheritedJsonConfig; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; @@ -40,6 +42,8 @@ public ForyJson aRestrictedConfiguration() { .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) + .registerMixin(EmptyMixin.class) + .withClassLoader(new CodegenRejectingClassLoader()) .withTypeChecker((className, context) -> false) .build(); } @@ -50,6 +54,7 @@ public ForyJson generatedConfiguration() { .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) + .registerMixin(EmptyMixin.class) .build(); } 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 31407c5b91..e2e29d9c98 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 @@ -1101,10 +1101,6 @@ private JsonMixinView(JsonMixinAnnotations.TargetOverlay overlay) { this.overlay = overlay; } - public boolean isEmpty() { - return overlay.isEmpty(); - } - public Set sourceDeclarations() { return overlay.sourceDeclarations(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java index bb1a2877d9..127557376e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeResolver.java @@ -1507,22 +1507,25 @@ private boolean reachesReader( } private boolean canCompile(ObjectCodec owner, CapabilityKind kind) { - if (codegen != null) { - return kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER - ? codegen.canCompileWriter(owner) - : codegen.canCompileReader(owner); + if (nativeObjectClass(owner.type(), kind) != null) { + return true; } - return nativeObjectClass(owner.type(), kind) != null; + return codegen != null + && (kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER + ? codegen.canCompileWriter(owner) + : codegen.canCompileReader(owner)); } private boolean canCompileCollection(JsonTypeInfo typeInfo, CapabilityKind kind) { - if (codegen != null) { + Type type = typeInfo.type(); + boolean generated = + kind == CapabilityKind.UTF8_WRITER + ? sharedRegistry.nativeUtf8CollectionWriterClass(type) != null + : sharedRegistry.nativeUtf8CollectionReaderClass(type) != null; + if (generated) { return true; } - Type type = typeInfo.type(); - return kind == CapabilityKind.UTF8_WRITER - ? sharedRegistry.nativeUtf8CollectionWriterClass(type) != null - : sharedRegistry.nativeUtf8CollectionReaderClass(type) != null; + return codegen != null; } private Class nativeObjectClass(Class type, CapabilityKind kind) { @@ -1961,6 +1964,15 @@ private CompletableFuture classesReady() { if (node.subtypeOwner != null) { continue; } + if (sharedRegistry.hostedCodegen()) { + // Another provider loader may already have generated this capability under the same + // source key. Reuse it while still walking the graph so this loader can add missing + // types. + node.generatedClass = nativeGeneratedClass(node, kind); + if (node.generatedClass != null) { + continue; + } + } node.classFuture = generatedClass(node, kind); futures.add(node.classFuture); } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 02ca23d698..22f78621a1 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -90,9 +90,10 @@ final class ForyJsonGraalVMFeature implements Feature { private final Set> processedProviders = ConcurrentHashMap.newKeySet(); private final Set> processedCodecs = ConcurrentHashMap.newKeySet(); private final Set> processedContainers = ConcurrentHashMap.newKeySet(); - private final Map hostedConfigurations = + // JsonCodegenKey stays loader-free so runtime configurations can reproduce it. Hosted resolvers + // remain loader-specific, while JsonGeneratedClassRegistry merges their generated capabilities. + private final Map> hostedConfigurations = new LinkedHashMap<>(); - private final Set processedGenerations = new LinkedHashSet<>(); @Override public String getDescription() { @@ -210,8 +211,21 @@ private boolean registerProvider(Class providerClass) { providerClass, "provider method returned a codegen-disabled ForyJson: " + method, null); } JsonCodegenKey key = config.codegenKey(); - if (!hostedConfigurations.containsKey(key)) { - hostedConfigurations.put(key, new HostedConfiguration(config)); + ArrayList configurations = hostedConfigurations.get(key); + if (configurations == null) { + configurations = new ArrayList<>(); + hostedConfigurations.put(key, configurations); + } + ClassLoader classLoader = config.classLoader(); + boolean knownLoader = false; + for (HostedConfiguration configuration : configurations) { + if (configuration.classLoader == classLoader) { + knownLoader = true; + break; + } + } + if (!knownLoader) { + configurations.add(new HostedConfiguration(config)); changed = true; } } @@ -266,35 +280,35 @@ private static List providerMethods(Class providerClass) { private boolean generateConfigurations(DuringAnalysisAccess access) { boolean changed = false; - for (Map.Entry entry : + for (Map.Entry> entry : hostedConfigurations.entrySet()) { - HostedConfiguration configuration = entry.getValue(); - LinkedHashSet> selectedModels = new LinkedHashSet<>(processedModels); - for (Map.Entry, Set>> mixin : reachableMixins.entrySet()) { - if (mixin.getValue().contains(configuration.mixins.get(mixin.getKey()))) { - selectedModels.add(mixin.getKey()); - } - } - ArrayList> models = new ArrayList<>(selectedModels); - models.sort(Comparator.comparing(Class::getName)); - for (Class model : models) { - GenerationKey generation = new GenerationKey(entry.getKey(), model); - if (!processedGenerations.add(generation)) { - continue; - } - try { - configuration.resolver.generateHostedCodecs(model); - } catch (RuntimeException | LinkageError e) { - throw new IllegalStateException( - "Cannot generate Fory JSON codecs for " + model.getName(), e); + for (HostedConfiguration configuration : entry.getValue()) { + LinkedHashSet> selectedModels = new LinkedHashSet<>(processedModels); + for (Map.Entry, Set>> mixin : reachableMixins.entrySet()) { + if (mixin.getValue().contains(configuration.mixins.get(mixin.getKey()))) { + selectedModels.add(mixin.getKey()); + } } - Set> generatedClasses = - JsonGeneratedClassRegistry.register( - entry.getKey(), configuration.registry.generatedClasses()); - for (Class generatedClass : generatedClasses) { - registerGeneratedClass(generatedClass); + ArrayList> models = new ArrayList<>(selectedModels); + models.sort(Comparator.comparing(Class::getName)); + for (Class model : models) { + if (!configuration.processedModels.add(model)) { + continue; + } + try { + configuration.resolver.generateHostedCodecs(model); + } catch (RuntimeException | LinkageError e) { + throw new IllegalStateException( + "Cannot generate Fory JSON codecs for " + model.getName(), e); + } + Set> generatedClasses = + JsonGeneratedClassRegistry.register( + entry.getKey(), configuration.registry.generatedClasses()); + for (Class generatedClass : generatedClasses) { + registerGeneratedClass(generatedClass); + } + changed = true; } - changed = true; } } return changed; @@ -363,9 +377,6 @@ private boolean registerMixin( reachableMixins.computeIfAbsent(targetType, ignored -> new LinkedHashSet<>()).add(mixinType); JsonMixinView annotations = JsonSharedRegistry.resolveMixin(targetType, mixinType); RuntimeReflection.register(mixinType); - if (annotations.isEmpty()) { - return true; - } registerReflectiveDeclarations(annotations.sourceDeclarations()); registerReflectiveDeclarations(annotations.targetDeclarations()); // Retain every directly declared hierarchy codec plus the exact Mixin replacement. Runtime @@ -873,11 +884,14 @@ private static Class rawType(Type type) { } private static final class HostedConfiguration { + private final ClassLoader classLoader; private final JsonSharedRegistry registry; private final JsonTypeResolver resolver; private final Map, Class> mixins; + private final Set> processedModels = new LinkedHashSet<>(); private HostedConfiguration(JsonConfig config) { + classLoader = config.classLoader(); registry = JsonSharedRegistry.forHostedCodegen(config); resolver = new JsonTypeResolver(registry); mixins = config.mixins(); @@ -911,30 +925,4 @@ public int hashCode() { } } - private static final class GenerationKey { - private final JsonCodegenKey configuration; - private final Class model; - - private GenerationKey(JsonCodegenKey configuration, Class model) { - this.configuration = configuration; - this.model = model; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof GenerationKey)) { - return false; - } - GenerationKey that = (GenerationKey) other; - return configuration.equals(that.configuration) && model == that.model; - } - - @Override - public int hashCode() { - return 31 * configuration.hashCode() + System.identityHashCode(model); - } - } } From 9f5e9a4710daa659d02e5978295bfdc0785be4ce Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 07:20:54 +0800 Subject: [PATCH 09/15] fix(json): cache native creator handles --- .../fory/json/meta/JsonCreatorInfo.java | 165 ++++++++++++------ .../apache/fory/json/reader/JsonReader.java | 20 ++- .../json/resolver/JsonStringValueCodec.java | 76 +++++--- .../fory/json/ForyJsonGraalVMFeature.java | 20 ++- 4 files changed, 192 insertions(+), 89 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java index 557722dfed..f68c3dd442 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java @@ -20,20 +20,24 @@ package org.apache.fory.json.meta; import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.internal._JDKAccess; -import org.apache.fory.util.ExceptionUtils; /** * Immutable ordered construction metadata for one JSON object codec. @@ -46,16 +50,11 @@ */ @Internal public final class JsonCreatorInfo { - private static final MethodHandle NATIVE_CONSTRUCTOR_INVOKER = - prepareNativeInvoker( - Constructor.class, - "newInstanceWithCaller", - MethodType.methodType(Object.class, Object[].class, boolean.class, Class.class)); - private static final MethodHandle NATIVE_FACTORY_INVOKER = - prepareNativeInvoker( - Method.class, - "invoke", - MethodType.methodType(Object.class, Object.class, Object[].class, Class.class)); + private static final MethodHandle CONSTRUCTOR_REFLECTION_INVOKER = + prepareConstructorReflectionInvoker(); + private static Map nativeInvokers = new HashMap<>(); + private static Map nativeStringInvokers = new HashMap<>(); + private static boolean nativeCreatorsFrozen; private final Class ownerType; private final Executable executable; @@ -77,7 +76,7 @@ public JsonCreatorInfo( this.defaults = defaults; this.generatedCodec = generatedCodec; invoker = - generatedCodec == null && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE + generatedCodec == null ? buildInvoker(ownerType, executable, executable.getParameterCount()) : null; hashes = new long[fields.length]; @@ -130,17 +129,15 @@ public Object create(Object[] arguments) { return invoke(arguments); } try { - Object value; - if (executable instanceof Constructor) { - value = invokeConstructor((Constructor) executable, arguments); - } else { - value = invokeFactory((Method) executable, arguments); - } + Object value = + executable instanceof Constructor + ? ((Constructor) executable).newInstance(arguments) + : ((Method) executable).invoke(null, arguments); return requireResult(value); - } catch (Throwable cause) { - if (cause instanceof InvocationTargetException) { - cause = cause.getCause(); - } + } catch (InstantiationException | IllegalAccessException e) { + throw new ForyJsonException("Failed to invoke JSON creator for " + ownerType.getName(), e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error) cause; } @@ -153,6 +150,7 @@ private Object invoke(Object[] arguments) { try { value = (Object) invoker.invokeExact(arguments); } catch (Throwable cause) { + cause = creatorCause(cause); if (cause instanceof Error) { throw (Error) cause; } @@ -161,6 +159,15 @@ private Object invoke(Object[] arguments) { return requireResult(value); } + private Throwable creatorCause(Throwable cause) { + return GraalvmSupport.isGraalRuntime() + && JdkVersion.MAJOR_VERSION >= 25 + && executable instanceof Constructor + && cause instanceof InvocationTargetException + ? cause.getCause() + : cause; + } + private Object requireResult(Object value) { if (value == null || value.getClass() != ownerType) { throw new ForyJsonException( @@ -177,6 +184,13 @@ private static MethodHandle buildInvoker( executable.setAccessible(true); return null; } + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle invoker = nativeInvokers.get(executable); + if (invoker == null) { + throw missingNativeCreator(executable); + } + return invoker; + } MethodHandle target = creatorTarget(ownerType, executable); // The interpreted reader already owns one trusted fixed-size argument array. Spread that // exact array into the creator without a second carrier or per-call reflective access check. @@ -188,59 +202,94 @@ private static MethodHandle buildInvoker( /** Returns the one-String-argument creator used by a JsonValue representation. */ @Internal public static MethodHandle stringCreatorHandle(Class ownerType, Executable executable) { + if (GraalvmSupport.isGraalRuntime()) { + MethodHandle invoker = nativeStringInvokers.get(executable); + if (invoker == null) { + throw missingNativeCreator(executable); + } + return invoker; + } return creatorTarget(ownerType, executable) .asType(MethodType.methodType(Object.class, String.class)); } - /** Invokes a creator constructor using the prepared Native Image access path when required. */ + /** Prepares the exact Native Image runtime handle for one registered creator. */ @Internal - public static Object invokeConstructor(Constructor constructor, Object[] arguments) { - if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - try { - return constructor.newInstance(arguments); - } catch (Throwable e) { - throw ExceptionUtils.throwException(e); - } + public static synchronized void prepareNativeCreator(Class ownerType, Executable executable) { + if (!GraalvmSupport.isGraalBuildTime() || nativeCreatorsFrozen) { + throw new IllegalStateException("Fory JSON native creator cache is not writable"); } - try { - // Creator validation already requires a public executable. Checking access as the declaring - // class preserves that contract without requiring its package to be exported or open. - Class caller = constructor.getDeclaringClass(); - return (Object) NATIVE_CONSTRUCTOR_INVOKER.invokeExact(constructor, arguments, true, caller); - } catch (Throwable e) { - throw ExceptionUtils.throwException(e); + MethodHandle target; + MethodHandle invoker; + if (executable instanceof Constructor && JdkVersion.MAJOR_VERSION >= 25) { + target = constructorReflectionTarget(ownerType, (Constructor) executable); + invoker = target; + } else { + target = creatorTarget(ownerType, executable); + invoker = + target + .asSpreader(Object[].class, executable.getParameterCount()) + .asType(MethodType.methodType(Object.class, Object[].class)); + } + nativeInvokers.putIfAbsent(executable, invoker); + if (executable.getParameterCount() == 1 && executable.getParameterTypes()[0] == String.class) { + if (!(executable instanceof Constructor) || JdkVersion.MAJOR_VERSION < 25) { + nativeStringInvokers.putIfAbsent( + executable, target.asType(MethodType.methodType(Object.class, String.class))); + } } } - /** Invokes a static creator method using the prepared Native Image access path when required. */ + /** Freezes all Native Image creator handles after hosted analysis. */ @Internal - public static Object invokeFactory(Method factory, Object[] arguments) { - if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - try { - return factory.invoke(null, arguments); - } catch (Throwable e) { - throw ExceptionUtils.throwException(e); - } - } - try { - // Method.invoke is caller-sensitive; use the declaring class for the same module-access - // contract as constructor invocation above. - Class caller = factory.getDeclaringClass(); - return (Object) NATIVE_FACTORY_INVOKER.invokeExact(factory, (Object) null, arguments, caller); - } catch (Throwable e) { - throw ExceptionUtils.throwException(e); + public static synchronized void freezeNativeCreators() { + if (nativeCreatorsFrozen) { + return; } + nativeInvokers = immutable(nativeInvokers); + nativeStringInvokers = immutable(nativeStringInvokers); + nativeCreatorsFrozen = true; + } + + private static Map immutable(Map invokers) { + return invokers.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(invokers)); + } + + private static ForyJsonException missingNativeCreator(Executable executable) { + return new ForyJsonException( + "Missing Native Image Fory JSON creator metadata for " + executable); + } + + /** Returns the prepared array-argument creator used by GraalVM 25 constructors. */ + @Internal + public static MethodHandle arrayCreatorHandle(Class ownerType, Executable executable) { + return buildInvoker(ownerType, executable, executable.getParameterCount()); + } + + private static MethodHandle constructorReflectionTarget( + Class ownerType, Constructor constructor) { + return MethodHandles.insertArguments( + CONSTRUCTOR_REFLECTION_INVOKER.bindTo(constructor), 1, true, ownerType); } - private static MethodHandle prepareNativeInvoker( - Class ownerType, String name, MethodType methodType) { - if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + private static MethodHandle prepareConstructorReflectionInvoker() { + if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE || JdkVersion.MAJOR_VERSION < 25) { return null; } try { - return _JDKAccess._trustedLookup(ownerType).findVirtual(ownerType, name, methodType); + // GraalVM 25 implements direct constructor MethodHandles through a reflection bridge whose + // synthetic caller cannot access concealed model packages. Preserve the executable's normal + // public access contract by supplying its declaring class as the caller. The resulting + // per-executable handle is prepared at image build time and performs no runtime lookup. + return _JDKAccess._trustedLookup(Constructor.class) + .findVirtual( + Constructor.class, + "newInstanceWithCaller", + MethodType.methodType(Object.class, Object[].class, boolean.class, Class.class)); } catch (NoSuchMethodException | IllegalAccessException e) { - throw new ForyJsonException("Cannot prepare Native Image JSON creator invocation", e); + throw new ForyJsonException("Cannot prepare GraalVM 25 JSON constructor invocation", e); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java index 53ac2cd304..f6bc4539ce 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java @@ -49,9 +49,9 @@ * Representation-neutral JSON cursor and common scalar parsing owner. * *

The base class retains the resolver used by dynamic codecs, the current code-unit position, - * configured and current container depth, a reusable ASCII token view, and a fixed workspace for - * exact floating-point boundary correction. Concrete readers own input storage, string decoding, - * field-name probes, and direct primitive numeric fast paths for their representation. + * configured and current container depth, reusable creator and numeric workspaces, and an ASCII + * token view. Concrete readers own input storage, string decoding, field-name probes, and direct + * primitive numeric fast paths for their representation. * *

Primitive {@code int}, {@code long}, {@code float}, and {@code double} parsing does not * materialize a String or arbitrary-precision number. Precision-sensitive floating input uses the @@ -256,6 +256,7 @@ && matchesScannedString(fieldStart, fieldEnd, info.property())) { public abstract int readSubtypeName(JsonSubtypeScanInfo info); private final AsciiStringView asciiStringView = new AsciiStringView(this); + private final Object[] creatorArguments = new Object[1]; // Primitive floating fallback reuses this exact-boundary workspace. Reader construction is the // cold owner so the first precision-sensitive scalar cannot allocate on the numeric hot path. private final byte[] decimalBoundaryDigits = new byte[DECIMAL_BOUNDARY_DIGITS]; @@ -272,6 +273,19 @@ public final JsonTypeResolver typeResolver() { return typeResolver; } + /** Returns this reader's reusable one-value creator argument array. */ + @Internal + public final Object[] creatorArguments(Object value) { + creatorArguments[0] = value; + return creatorArguments; + } + + /** Releases the value retained by {@link #creatorArguments(Object)}. */ + @Internal + public final void clearCreatorArguments() { + creatorArguments[0] = null; + } + protected abstract int length(); protected abstract char charAt(int index); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java index b41c5d2aa3..06a613b1a7 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java @@ -29,6 +29,7 @@ import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; +import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf16JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; @@ -36,6 +37,7 @@ import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; +import org.apache.fory.platform.JdkVersion; /** Complete String representation selected by one effective {@code JsonValue} member. */ final class JsonStringValueCodec implements JsonValueCodec { @@ -95,7 +97,7 @@ public Object readLatin1(Latin1JsonReader reader) { reader.readNull(); return null; } - return read(reader.readString()); + return read(reader, reader.readString()); } @Override @@ -104,7 +106,7 @@ public Object readUtf16(Utf16JsonReader reader) { reader.readNull(); return null; } - return read(reader.readString()); + return read(reader, reader.readString()); } @Override @@ -113,10 +115,10 @@ public Object readUtf8(Utf8JsonReader reader) { reader.readNull(); return null; } - return read(reader.readString()); + return read(reader, reader.readString()); } - private Object read(String value) { + private Object read(JsonReader reader, String value) { if (raw) { throw new ForyJsonException( "Combined @JsonValue and @JsonRawValue representation is write-only for " @@ -128,7 +130,7 @@ private Object read(String value) { + ownerType.getName() + " requires a one-String-argument @JsonCreator"); } - return creator.create(value); + return creator.create(reader, value); } private abstract static class ValueCreator { @@ -138,7 +140,7 @@ private ValueCreator(Class ownerType) { this.ownerType = ownerType; } - abstract Object create(String value); + abstract Object create(JsonReader reader, String value); final Object requireResult(Object result) { if (result == null || result.getClass() != ownerType) { @@ -160,10 +162,11 @@ static ValueCreator forExecutable( if (generatedCodec != null) { return new GeneratedCreator(ownerType, generatedCodec); } - if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { - return executable instanceof Constructor - ? new ConstructorCreator(ownerType, (Constructor) executable) - : new FactoryCreator(ownerType, (Method) executable); + if (GraalvmSupport.isGraalRuntime() + && JdkVersion.MAJOR_VERSION >= 25 + && executable instanceof Constructor) { + return new ReflectionCreator( + ownerType, JsonCreatorInfo.arrayCreatorHandle(ownerType, executable)); } if (!AndroidSupport.IS_ANDROID) { return new MethodHandleCreator(ownerType, buildInvoker(ownerType, executable)); @@ -188,11 +191,13 @@ private GeneratedCreator(Class ownerType, GeneratedJsonCodec generatedCode } @Override - Object create(String value) { + Object create(JsonReader reader, String value) { try { - return requireResult(generatedCodec.newInstance(new Object[] {value})); + return requireResult(generatedCodec.newInstance(reader.creatorArguments(value))); } catch (Throwable cause) { throw creatorFailure(cause); + } finally { + reader.clearCreatorArguments(); } } } @@ -206,7 +211,7 @@ private MethodHandleCreator(Class ownerType, MethodHandle invoker) { } @Override - Object create(String value) { + Object create(JsonReader reader, String value) { try { Object result = (Object) invoker.invokeExact(value); return requireResult(result); @@ -216,6 +221,27 @@ Object create(String value) { } } + private static final class ReflectionCreator extends ValueCreator { + private final MethodHandle invoker; + + private ReflectionCreator(Class ownerType, MethodHandle invoker) { + super(ownerType); + this.invoker = invoker; + } + + @Override + Object create(JsonReader reader, String value) { + try { + Object result = (Object) invoker.invokeExact(reader.creatorArguments(value)); + return requireResult(result); + } catch (Throwable cause) { + throw creatorFailure(cause instanceof InvocationTargetException ? cause.getCause() : cause); + } finally { + reader.clearCreatorArguments(); + } + } + } + private static final class ConstructorCreator extends ValueCreator { private final Constructor constructor; @@ -225,11 +251,15 @@ private ConstructorCreator(Class ownerType, Constructor constructor) { } @Override - Object create(String value) { + Object create(JsonReader reader, String value) { try { - return requireResult(JsonCreatorInfo.invokeConstructor(constructor, new Object[] {value})); - } catch (Throwable cause) { - throw creatorFailure(cause instanceof InvocationTargetException ? cause.getCause() : cause); + return requireResult(constructor.newInstance(reader.creatorArguments(value))); + } catch (InstantiationException | IllegalAccessException e) { + throw creatorFailure(e); + } catch (InvocationTargetException e) { + throw creatorFailure(e.getCause()); + } finally { + reader.clearCreatorArguments(); } } } @@ -243,11 +273,15 @@ private FactoryCreator(Class ownerType, Method factory) { } @Override - Object create(String value) { + Object create(JsonReader reader, String value) { try { - return requireResult(JsonCreatorInfo.invokeFactory(factory, new Object[] {value})); - } catch (Throwable cause) { - throw creatorFailure(cause instanceof InvocationTargetException ? cause.getCause() : cause); + return requireResult(factory.invoke(null, reader.creatorArguments(value))); + } catch (IllegalAccessException e) { + throw creatorFailure(e); + } catch (InvocationTargetException e) { + throw creatorFailure(e.getCause()); + } finally { + reader.clearCreatorArguments(); } } } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 22f78621a1..2282207a7e 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -61,6 +61,7 @@ import org.apache.fory.json.codec.Base64ByteArrayCodec; import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.ScalarCodecs; +import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry.JsonMixinView; @@ -90,6 +91,7 @@ final class ForyJsonGraalVMFeature implements Feature { private final Set> processedProviders = ConcurrentHashMap.newKeySet(); private final Set> processedCodecs = ConcurrentHashMap.newKeySet(); private final Set> processedContainers = ConcurrentHashMap.newKeySet(); + private final Set processedCreators = new LinkedHashSet<>(); // JsonCodegenKey stays loader-free so runtime configurations can reproduce it. Hosted resolvers // remain loader-specific, while JsonGeneratedClassRegistry merges their generated capabilities. private final Map> hostedConfigurations = @@ -490,12 +492,12 @@ && annotation(annotations, method, JsonValue.class) != null) { } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - registerCreator(constructor); + registerCreator(type, constructor); } } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - registerCreator(method); + registerCreator(type, method); } } return true; @@ -519,6 +521,7 @@ private void registerReflectiveDeclarations(Set declarations) public void afterAnalysis(AfterAnalysisAccess access) { JsonGeneratedClassRegistry.freeze(); JsonFieldAccessor.freezeNativeAccessors(); + JsonCreatorInfo.freezeNativeCreators(); ObjectCodec.freezeNativeAnySetters(); } @@ -587,7 +590,7 @@ boolean record = type.isRecord(); } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - registerCreator(constructor); + registerCreator(type, constructor); registerParameterCodecs(annotations, constructor.getParameters()); registerResolvedParameterTypes(ownerType, constructor.getParameters()); registerUnwrappedParameters( @@ -596,7 +599,7 @@ boolean record = type.isRecord(); } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - registerCreator(method); + registerCreator(type, method); registerParameterCodecs(annotations, method.getParameters()); registerResolvedParameterTypes(ownerType, method.getParameters()); registerUnwrappedParameters(access, ownerType, annotations, method.getParameters()); @@ -611,11 +614,14 @@ private void prepareRecord(Class type) { JsonFieldAccessor.prepareGetter(component.getAccessor()); } Constructor constructor = RecordUtils.getRecordConstructor(type).f0; - registerCreator(constructor); + registerCreator(type, constructor); } - private void registerCreator(Executable executable) { - RuntimeReflection.register(executable); + private void registerCreator(Class ownerType, Executable executable) { + if (processedCreators.add(executable)) { + RuntimeReflection.register(executable); + JsonCreatorInfo.prepareNativeCreator(ownerType, executable); + } } private static void prepareMethodAccessors(JsonMixinView annotations, Method method) { From f69731c6ec6d6e6dc328874b18d42bacc56fb8c5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 11:43:26 +0800 Subject: [PATCH 10/15] ci: build one GraalVM JSON image --- ci/run_ci.sh | 26 +- integration_tests/graalvm_tests/README.md | 12 +- .../apache/fory/graalvm/ForyJsonExample.java | 133 ++++++- .../graalvm/ForyJsonNoProviderExample.java | 375 ------------------ 4 files changed, 136 insertions(+), 410 deletions(-) delete mode 100644 integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java diff --git a/ci/run_ci.sh b/ci/run_ci.sh index 5a9ecf5e72..fe0e9dad9d 100755 --- a/ci/run_ci.sh +++ b/ci/run_ci.sh @@ -79,8 +79,8 @@ install_jdks() { done } -run_graalvm_tests() { - local main_classes=("$@") +run_graalvm_test() { + local main_class="$1" local java_version local java_major java_version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2; exit}') @@ -103,25 +103,21 @@ run_graalvm_tests() { -Dmaven.source.skip=true \ -Dmaven.javadoc.skip=true cd "$ROOT"/integration_tests/graalvm_tests - for main_class in "${main_classes[@]}"; do - echo "Start to build GraalVM JPMS native image for $main_class" - mvn -DmainClass="$main_class" -DskipTests=true -Dassembly.skipAssembly=true \ - --no-transfer-progress -Pnative-module clean package - echo "Built GraalVM JPMS native image" - echo "Start to run GraalVM JPMS native image" - ./target/main-module - echo "Execute GraalVM tests for $main_class succeed!" - done + echo "Start to build GraalVM JPMS native image for $main_class" + mvn -DmainClass="$main_class" -DskipTests=true -Dassembly.skipAssembly=true \ + --no-transfer-progress -Pnative-module clean package + echo "Built GraalVM JPMS native image" + echo "Start to run GraalVM JPMS native image" + ./target/main-module + echo "Execute GraalVM tests for $main_class succeed!" } graalvm_test() { - run_graalvm_tests org.apache.fory.graalvm.Main + run_graalvm_test org.apache.fory.graalvm.Main } graalvm_json_tests() { - run_graalvm_tests \ - org.apache.fory.graalvm.ForyJsonExample \ - org.apache.fory.graalvm.ForyJsonNoProviderExample + run_graalvm_test org.apache.fory.graalvm.ForyJsonExample } jdk25_access_options() { diff --git a/integration_tests/graalvm_tests/README.md b/integration_tests/graalvm_tests/README.md index 5d82f836e6..5462709cd6 100644 --- a/integration_tests/graalvm_tests/README.md +++ b/integration_tests/graalvm_tests/README.md @@ -1,9 +1,10 @@ # GraalVM Native Image Tests -Examples and tests for Fory serialization in GraalVM Native Image. The Fory JSON entry points are -compiled with annotation processing disabled. They cover direct `JsonType` models, exact +Examples and tests for Fory serialization in GraalVM Native Image. The Fory JSON entry point is +compiled with annotation processing disabled. It covers direct `JsonType` models, exact `JsonMixin` target/source mappings, provider-selected hosted codec generation, configuration -fallback to interpreted codecs, and a separate image with no reachable `ForyJsonProvider`. +fallback to interpreted codecs, and hosted access metadata for unprovided configurations in one +native image. ## Test @@ -12,11 +13,6 @@ mvn -DmainClass=org.apache.fory.graalvm.ForyJsonExample clean -DskipTests=true - ./target/main mvn -DmainClass=org.apache.fory.graalvm.ForyJsonExample clean -DskipTests=true -Pnative-module package ./target/main-module - -mvn -DmainClass=org.apache.fory.graalvm.ForyJsonNoProviderExample clean -DskipTests=true -Dexec.skip=true -Pnative package -./target/main -mvn -DmainClass=org.apache.fory.graalvm.ForyJsonNoProviderExample clean -DskipTests=true -Pnative-module package -./target/main-module ``` ## Benchmark diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java index e5557ff620..31055235d8 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -44,7 +44,9 @@ import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; import org.apache.fory.json.annotation.ForyJsonProvider; +import org.apache.fory.json.annotation.JsonAnyGetter; import org.apache.fory.json.annotation.JsonAnyProperty; +import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonBase64; import org.apache.fory.json.annotation.JsonCodec; import org.apache.fory.json.annotation.JsonCreator; @@ -123,16 +125,14 @@ public static void main(String[] args) { } private static void testHostedCodegenConfigurations() { + ForyJson providerJson = newProviderJson(); + ForyJson interpretedJson = newInterpretedJson(); exerciseCodegenConfiguration(ForyJson.builder().build(), false); - exerciseCodegenConfiguration(newProviderJson(), true); - exerciseCodegenConfiguration( - ForyJson.builder() - .withFieldMode(true) - .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) - .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) - .build(), - false); - testEmptyMixinCodegen(); + exerciseCodegenConfiguration(providerJson, true); + exerciseCodegenConfiguration(interpretedJson, false); + testEmptyMixin(providerJson, true); + testEmptyMixin(interpretedJson, false); + testInterpretedMetadata(interpretedJson); testIndependentChildCodegen(); testExternalModuleMixin(); } @@ -147,9 +147,17 @@ private static ForyJson newProviderJson() { .build(); } - private static void testEmptyMixinCodegen() { - CodegenProbeCodec.expect(EmptyMixinTarget.class, true); - ForyJson json = newProviderJson(); + private static ForyJson newInterpretedJson() { + return ForyJson.builder() + .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) + .registerMixin(EmptyMixin.class) + .registerMixin(InterpretedMixin.class) + .build(); + } + + private static void testEmptyMixin(ForyJson json, boolean generated) { + CodegenProbeCodec.expect(EmptyMixinTarget.class, generated); EmptyMixinTarget value = new EmptyMixinTarget(); value.probe = new CodegenProbeValue("empty-mixin"); String encoded = json.toJson(value); @@ -157,6 +165,30 @@ private static void testEmptyMixinCodegen() { json.fromJson(encoded, EmptyMixinTarget.class).probe.value.equals("empty-mixin")); } + private static void testInterpretedMetadata(ForyJson json) { + InterpretedMixinTarget mixinValue = new InterpretedMixinTarget(); + mixinValue.setName("empty-mixin"); + String mixinJson = json.toJson(mixinValue); + Preconditions.checkArgument( + json.fromJson(mixinJson, InterpretedMixinTarget.class).getName().equals("empty-mixin")); + + InterpretedBean bean = new InterpretedBean(); + bean.setName("bean"); + bean.putExtra("dynamic", "extra"); + InterpretedBean decoded = json.fromJson(json.toJson(bean), InterpretedBean.class); + Preconditions.checkArgument(decoded.getName().equals("bean")); + Preconditions.checkArgument(decoded.extra().equals(Map.of("dynamic", "extra"))); + + DirectValueRecord record = new DirectValueRecord("record-value"); + Preconditions.checkArgument(json.toJson(record).equals("\"record-value\"")); + Preconditions.checkArgument( + json.fromJson("\"decoded-record\"", DirectValueRecord.class) + .equals(new DirectValueRecord("decoded-record"))); + Preconditions.checkArgument(json.toJson(DirectValueEnum.READY).equals("\"ready\"")); + Preconditions.checkArgument( + json.fromJson("\"done\"", DirectValueEnum.class) == DirectValueEnum.DONE); + } + private static void exerciseCodegenConfiguration(ForyJson json, boolean generated) { CodegenProbeCodec.expect(CodegenProbeModel.class, generated); CodegenProbeModel value = new CodegenProbeModel(); @@ -437,6 +469,7 @@ private static void testValueAnnotations() { Preconditions.checkArgument( Arrays.equals( json.fromJson("{\"value\":\"AQID\"}", Base64Bytes.class).value, new byte[] {1, 2, 3})); + } private static void testSubtypes() { @@ -547,6 +580,49 @@ public EmptyMixinTarget() {} @JsonMixin(target = EmptyMixinTarget.class) public interface EmptyMixin {} + public static final class InterpretedMixinTarget { + private String name; + + public InterpretedMixinTarget() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @JsonMixin(target = InterpretedMixinTarget.class) + public interface InterpretedMixin {} + + @JsonType + public static final class InterpretedBean { + private String name; + private final transient Map extra = new LinkedHashMap<>(); + + public InterpretedBean() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @JsonAnyGetter + public Map extra() { + return extra; + } + + @JsonAnySetter + public void putExtra(String key, String value) { + extra.put(key, value); + } + } + /** Hosted-only loader which makes the first equivalent provider unable to compile one model. */ public static final class CodegenRejectingClassLoader extends ClassLoader { public CodegenRejectingClassLoader() { @@ -838,6 +914,39 @@ public String value() { } } + @JsonType + public record DirectValueRecord(@JsonValue String value) { + @JsonCreator + public DirectValueRecord {} + } + + @JsonType + public enum DirectValueEnum { + READY("ready"), + DONE("done"); + + private final String value; + + DirectValueEnum(String value) { + this.value = value; + } + + @JsonValue + public String value() { + return value; + } + + @JsonCreator + public static DirectValueEnum fromValue(String value) { + for (DirectValueEnum candidate : values()) { + if (candidate.value.equals(value)) { + return candidate; + } + } + throw new IllegalArgumentException(value); + } + } + @JsonType public static final class RawValue { @JsonRawValue public String body; diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java deleted file mode 100644 index e6fc47119c..0000000000 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonNoProviderExample.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * 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.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; -import org.apache.fory.json.ForyJson; -import org.apache.fory.json.annotation.JsonAnyGetter; -import org.apache.fory.json.annotation.JsonAnySetter; -import org.apache.fory.json.annotation.JsonCodec; -import org.apache.fory.json.annotation.JsonCreator; -import org.apache.fory.json.annotation.JsonMixin; -import org.apache.fory.json.annotation.JsonProperty; -import org.apache.fory.json.annotation.JsonType; -import org.apache.fory.json.annotation.JsonValue; -import org.apache.fory.json.codec.JsonValueCodec; -import org.apache.fory.json.codec.ObjectCodec; -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.platform.GraalvmSupport; -import org.apache.fory.util.Preconditions; - -/** Native-image acceptance coverage when no {@code ForyJsonProvider} is reachable. */ -public final class ForyJsonNoProviderExample { - private static final String NATIVE_INTERPRETER_MESSAGE = - "Fory JSON is using interpreted codecs because the current configuration was not included " - + "in this native image. Return this configuration from a reachable " - + "@ForyJsonProvider to enable generated codecs."; - - private ForyJsonNoProviderExample() {} - - public static void main(String[] args) { - PrintStream originalOut = System.out; - ByteArrayOutputStream captured = new ByteArrayOutputStream(); - try (PrintStream testOut = new PrintStream(captured, true, StandardCharsets.UTF_8)) { - System.setOut(testOut); - try { - exercise(ForyJson.builder().build()); - } finally { - System.setOut(originalOut); - } - } - String output = new String(captured.toByteArray(), StandardCharsets.UTF_8); - if (GraalvmSupport.isGraalRuntime()) { - int occurrences = countOccurrences(output, NATIVE_INTERPRETER_MESSAGE); - Preconditions.checkArgument( - occurrences == 1, - "Expected one Native Image interpreted-codec message, found " - + occurrences - + ": " - + output); - } - originalOut.print(output); - originalOut.println("Fory JSON without provider succeed"); - } - - private static void exercise(ForyJson json) { - Bean bean = new Bean(); - bean.setName("bean"); - bean.putExtra("dynamic", "extra"); - Model value = - new Model( - 7, new Probe("value"), bean, new RecordValue(8, "record"), FactoryValue.create(9)); - String encoded = json.toJson(value); - Preconditions.checkArgument(json.toJsonBytes(value).length != 0); - Preconditions.checkArgument(json.fromJson(encoded, Model.class).equals(value)); - Preconditions.checkArgument( - json.fromJson(encoded.replace("value", "\u4f60"), Model.class) - .probe - .value - .equals("\u4f60")); - Preconditions.checkArgument( - json.fromJson(encoded.getBytes(StandardCharsets.UTF_8), Model.class).equals(value)); - Preconditions.checkArgument( - json.toJson(new DirectValueRecord("record-value")).equals("\"record-value\"")); - Preconditions.checkArgument( - json.fromJson("\"decoded-record\"", DirectValueRecord.class) - .equals(new DirectValueRecord("decoded-record"))); - Preconditions.checkArgument(json.toJson(DirectValueEnum.READY).equals("\"ready\"")); - Preconditions.checkArgument( - json.fromJson("\"done\"", DirectValueEnum.class) == DirectValueEnum.DONE); - - ForyJson emptyMixinJson = ForyJson.builder().registerMixin(EmptyMixin.class).build(); - EmptyMixinTarget emptyMixin = new EmptyMixinTarget(); - emptyMixin.setName("empty-mixin"); - String emptyMixinEncoded = emptyMixinJson.toJson(emptyMixin); - Preconditions.checkArgument( - emptyMixinJson - .fromJson(emptyMixinEncoded, EmptyMixinTarget.class) - .getName() - .equals("empty-mixin")); - } - - private static int countOccurrences(String value, String target) { - int count = 0; - int offset = 0; - while ((offset = value.indexOf(target, offset)) >= 0) { - count++; - offset += target.length(); - } - return count; - } - - public static final class EmptyMixinTarget { - private String name; - - public EmptyMixinTarget() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - @JsonMixin(target = EmptyMixinTarget.class) - public interface EmptyMixin {} - - @JsonType - public static final class Model { - private final int id; - - @JsonCodec(ProbeCodec.class) - private final Probe probe; - - private final Bean bean; - private final RecordValue record; - private final FactoryValue factory; - - @JsonCreator - public Model( - @JsonProperty("id") int id, - @JsonProperty("probe") Probe probe, - @JsonProperty("bean") Bean bean, - @JsonProperty("record") RecordValue record, - @JsonProperty("factory") FactoryValue factory) { - this.id = id; - this.probe = probe; - this.bean = bean; - this.record = record; - this.factory = factory; - } - - public int getId() { - return id; - } - - public Probe getProbe() { - return probe; - } - - public Bean getBean() { - return bean; - } - - public RecordValue getRecord() { - return record; - } - - public FactoryValue getFactory() { - return factory; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof Model)) { - return false; - } - Model that = (Model) other; - return id == that.id - && probe.equals(that.probe) - && bean.equals(that.bean) - && record.equals(that.record) - && factory.equals(that.factory); - } - - @Override - public int hashCode() { - int result = 31 * id + probe.hashCode(); - result = 31 * result + bean.hashCode(); - result = 31 * result + record.hashCode(); - return 31 * result + factory.hashCode(); - } - } - - @JsonType - public static final class Bean { - private String name; - private final transient Map extra = new LinkedHashMap<>(); - - public Bean() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @JsonAnyGetter - public Map extra() { - return extra; - } - - @JsonAnySetter - public void putExtra(String key, String value) { - extra.put(key, value); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Bean)) { - return false; - } - Bean that = (Bean) other; - return name.equals(that.name) && extra.equals(that.extra); - } - - @Override - public int hashCode() { - return 31 * name.hashCode() + extra.hashCode(); - } - } - - @JsonType - public record RecordValue(int rank, String text) {} - - @JsonType - public record DirectValueRecord(@JsonValue String value) { - @JsonCreator - public DirectValueRecord {} - } - - @JsonType - public enum DirectValueEnum { - READY("ready"), - DONE("done"); - - private final String value; - - DirectValueEnum(String value) { - this.value = value; - } - - @JsonValue - public String value() { - return value; - } - - @JsonCreator - public static DirectValueEnum fromValue(String value) { - for (DirectValueEnum candidate : values()) { - if (candidate.value.equals(value)) { - return candidate; - } - } - throw new IllegalArgumentException("Unknown direct enum value " + value); - } - } - - @JsonType - public static final class FactoryValue { - private final int code; - - private FactoryValue(int code) { - this.code = code; - } - - @JsonCreator - public static FactoryValue create(@JsonProperty("code") int code) { - return new FactoryValue(code); - } - - public int getCode() { - return code; - } - - @Override - public boolean equals(Object other) { - return other instanceof FactoryValue && code == ((FactoryValue) other).code; - } - - @Override - public int hashCode() { - return code; - } - } - - public static final class Probe { - private final String value; - - private Probe(String value) { - this.value = value; - } - - @Override - public boolean equals(Object other) { - return other instanceof Probe && value.equals(((Probe) other).value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - } - - public static final class ProbeCodec implements JsonValueCodec { - public ProbeCodec() {} - - @Override - public void writeString(StringJsonWriter writer, Probe value) { - checkInterpreted(writer.typeResolver().getTypeInfo(Model.class, Model.class).stringWriter()); - writer.writeString(value == null ? null : value.value); - } - - @Override - public void writeUtf8(Utf8JsonWriter writer, Probe value) { - checkInterpreted(writer.typeResolver().getTypeInfo(Model.class, Model.class).utf8Writer()); - writer.writeString(value == null ? null : value.value); - } - - @Override - public Probe readLatin1(Latin1JsonReader reader) { - checkInterpreted(reader.typeResolver().getTypeInfo(Model.class, Model.class).latin1Reader()); - return reader.tryReadNullToken() ? null : new Probe(reader.readString()); - } - - @Override - public Probe readUtf16(Utf16JsonReader reader) { - checkInterpreted(reader.typeResolver().getTypeInfo(Model.class, Model.class).utf16Reader()); - return reader.tryReadNullToken() ? null : new Probe(reader.readString()); - } - - @Override - public Probe readUtf8(Utf8JsonReader reader) { - checkInterpreted(reader.typeResolver().getTypeInfo(Model.class, Model.class).utf8Reader()); - return reader.tryReadNullToken() ? null : new Probe(reader.readString()); - } - - private static void checkInterpreted(Object capability) { - if (GraalvmSupport.isGraalRuntime()) { - Preconditions.checkArgument(capability instanceof ObjectCodec); - } - } - } -} From d705ed6a3563b05d3bab0a090401ca5b23b2655e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 11:53:21 +0800 Subject: [PATCH 11/15] refactor(json): merge SQL creator catch --- .../src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java index 80d0f92372..2b471fe262 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/SqlJsonCodecs.java @@ -128,8 +128,6 @@ private T newSqlValue(long millis) { return constructorHandle == null ? constructor.newInstance(millis) : (T) constructorHandle.invoke(millis); - } catch (ReflectiveOperationException e) { - throw new ForyJsonException("Cannot create SQL JSON type " + type, e); } catch (Throwable e) { throw new ForyJsonException("Cannot create SQL JSON type " + type, e); } From bbd41d652f45bd2ca0e017c1c8157ec67b847d0b Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 12:04:07 +0800 Subject: [PATCH 12/15] fix(json): own Native Image initialization metadata --- .../java/org/apache/fory/json/ForyJson.java | 7 +-- .../fory/json/ForyJsonGraalVMFeature.java | 24 +--------- .../fory-json/native-image.properties | 19 +++++++- .../ForyJsonGraalVMFeatureJarVerifier.java | 44 +++++++++++++------ 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index a4b456980c..bc4112bf4c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -36,7 +36,6 @@ import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; -import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.StringSerializer; @@ -92,7 +91,7 @@ public final class ForyJson { } ForyJson(JsonConfig config, JsonSharedRegistry sharedRegistry) { - this.config = GraalvmSupport.isGraalBuildTime() ? config : null; + this.config = config; int poolSize = config.concurrencyLevel(); homeSlotMask = Integer.highestOneBit(poolSize) - 1; // This fixed array is the only JsonState owner. Each state's three readers own their configured @@ -110,10 +109,6 @@ public static ForyJsonBuilder builder() { } JsonConfig config() { - if (config == null) { - throw new IllegalStateException( - "Fory JSON configuration is available only during native-image build"); - } return config; } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 2282207a7e..703608ad7c 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -60,7 +60,6 @@ import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.Base64ByteArrayCodec; import org.apache.fory.json.codec.ObjectCodec; -import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.resolver.JsonSharedRegistry; @@ -73,7 +72,6 @@ import org.apache.fory.reflect.TypeRef; import org.apache.fory.util.record.RecordUtils; import org.graalvm.nativeimage.hosted.Feature; -import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; import org.graalvm.nativeimage.hosted.RuntimeReflection; /** Prepares reachable Fory JSON models and provider-selected codecs for Native Image. */ @@ -104,27 +102,7 @@ public String getDescription() { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - String jsonPackage = ForyJson.class.getPackage().getName(); - // GraalVM 21 requires the Fory JSON implementation used by hosted codegen to have an explicit - // build-time initialization policy. Keep application models and the ScalarCodecs temporal - // formatters out of that policy: the latter capture JDK chronology instances which GraalVM 25 - // initializes at runtime. - RuntimeClassInitialization.initializeAtBuildTime( - ForyJson.class, - ForyJsonBuilder.class, - JsonCodegenKey.class, - JsonConfig.class, - JsonGeneratedClassRegistry.class, - JsonGeneratedClassRegistry.Configuration.class, - PropertyNamingStrategy.class); - RuntimeClassInitialization.initializeAtBuildTime( - jsonPackage + ".codegen", - jsonPackage + ".codec", - jsonPackage + ".meta", - jsonPackage + ".reader", - jsonPackage + ".resolver", - jsonPackage + ".writer"); - RuntimeClassInitialization.initializeAtRunTime(ScalarCodecs.class); + // native-image.properties owns class initialization; this Feature owns reachability metadata. access.registerSubtypeReachabilityHandler(this::processReachableType, Object.class); } 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 index 6f1e1ddc6f..c1ac343343 100644 --- 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 @@ -15,4 +15,21 @@ # specific language governing permissions and limitations # under the License. -Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature +# Hosted codegen initializes only Fory JSON implementation classes. Application providers, models, +# and custom codecs retain their own initialization policy. ScalarCodecs stays runtime initialized +# because its temporal formatters capture JDK chronology state. +Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature \ + --initialize-at-build-time=org.apache.fory.json.ForyJson,\ + org.apache.fory.json.ForyJsonBuilder,\ + org.apache.fory.json.JsonCodegenKey,\ + org.apache.fory.json.JsonConfig,\ + org.apache.fory.json.JsonGeneratedClassRegistry,\ + org.apache.fory.json.JsonGeneratedClassRegistry$Configuration,\ + org.apache.fory.json.PropertyNamingStrategy,\ + org.apache.fory.json.codegen,\ + org.apache.fory.json.codec,\ + org.apache.fory.json.meta,\ + org.apache.fory.json.reader,\ + org.apache.fory.json.resolver,\ + org.apache.fory.json.writer \ + --initialize-at-run-time=org.apache.fory.json.codec.ScalarCodecs diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java index 722bbf7eaf..209e82afba 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java @@ -22,6 +22,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -31,6 +32,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; +import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -51,6 +53,31 @@ public final class ForyJsonGraalVMFeatureJarVerifier { 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 BUILD_TIME_OPTION = "--initialize-at-build-time="; + private static final String RUNTIME_OPTION = "--initialize-at-run-time="; + private static final String BUILD_TIME_TARGETS = + "org.apache.fory.json.ForyJson," + + "org.apache.fory.json.ForyJsonBuilder," + + "org.apache.fory.json.JsonCodegenKey," + + "org.apache.fory.json.JsonConfig," + + "org.apache.fory.json.JsonGeneratedClassRegistry," + + "org.apache.fory.json.JsonGeneratedClassRegistry$Configuration," + + "org.apache.fory.json.PropertyNamingStrategy," + + "org.apache.fory.json.codegen," + + "org.apache.fory.json.codec," + + "org.apache.fory.json.meta," + + "org.apache.fory.json.reader," + + "org.apache.fory.json.resolver," + + "org.apache.fory.json.writer"; + private static final String RUNTIME_TARGETS = "org.apache.fory.json.codec.ScalarCodecs"; + private static final String NATIVE_IMAGE_ARGS = + FEATURE_OPTION + + " " + + BUILD_TIME_OPTION + + BUILD_TIME_TARGETS + + " " + + RUNTIME_OPTION + + RUNTIME_TARGETS; private ForyJsonGraalVMFeatureJarVerifier() {} @@ -82,9 +109,10 @@ private static void verifyBinaryJar(Path jarPath) throws IOException { check( countEntries(jarFile, NATIVE_IMAGE_PROPERTIES) == 1, "Expected exactly one JSON native-image.properties"); - String properties = readEntry(jarFile, NATIVE_IMAGE_PROPERTIES); - check(countOccurrences(properties, "--features=") == 1, "Expected one --features option"); - check(properties.contains(FEATURE_OPTION), "Fory JSON Feature option is missing"); + Properties properties = new Properties(); + properties.load(new StringReader(readEntry(jarFile, NATIVE_IMAGE_PROPERTIES))); + String nativeImageArgs = properties.getProperty("Args"); + check(NATIVE_IMAGE_ARGS.equals(nativeImageArgs), "Unexpected Fory JSON Native Image Args"); } } @@ -155,16 +183,6 @@ private static String readEntry(JarFile jarFile, String entryName) throws IOExce } } - private static int countOccurrences(String value, String target) { - int count = 0; - int offset = 0; - while ((offset = value.indexOf(target, offset)) >= 0) { - count++; - offset += target.length(); - } - return count; - } - private static void check(boolean condition, String message) { if (!condition) { throw new AssertionError(message); From a2e4c326eb9c63b3aaad90be5725a3299066fce8 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 12:11:03 +0800 Subject: [PATCH 13/15] style(json): format GraalVM fixture --- .../src/main/java/org/apache/fory/graalvm/ForyJsonExample.java | 1 - 1 file changed, 1 deletion(-) diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java index 31055235d8..9003fdca2d 100644 --- a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java +++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/ForyJsonExample.java @@ -469,7 +469,6 @@ private static void testValueAnnotations() { Preconditions.checkArgument( Arrays.equals( json.fromJson("{\"value\":\"AQID\"}", Base64Bytes.class).value, new byte[] {1, 2, 3})); - } private static void testSubtypes() { From 945367f37659ecf2f344f2113252aaeba7e14827 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 12:15:25 +0800 Subject: [PATCH 14/15] refactor(json): align GraalVM codegen owners --- .../java/org/apache/fory/json/JsonConfig.java | 1 + .../json/{ => codegen}/JsonCodegenKey.java | 5 +-- .../JsonGeneratedClassRegistry.java | 34 +++++++++---------- .../json/resolver/JsonSharedRegistry.java | 26 ++++++-------- .../fory/json/ForyJsonGraalVMFeature.java | 5 +-- .../fory-json/native-image.properties | 3 -- .../ForyJsonGraalVMFeatureJarVerifier.java | 3 -- 7 files changed, 35 insertions(+), 42 deletions(-) rename java/fory-json/src/main/java/org/apache/fory/json/{ => codegen}/JsonCodegenKey.java (95%) rename java/fory-json/src/main/java/org/apache/fory/json/{ => resolver}/JsonGeneratedClassRegistry.java (86%) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java index 9835d6afe7..c2477fb010 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonConfig.java @@ -30,6 +30,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.fory.annotation.Internal; +import org.apache.fory.json.codegen.JsonCodegenKey; import org.apache.fory.json.resolver.CodecRegistry; /** diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java similarity index 95% rename from java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java rename to java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java index bb0281bb5b..a944949445 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodegenKey.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java @@ -17,10 +17,11 @@ * under the License. */ -package org.apache.fory.json; +package org.apache.fory.json.codegen; import java.util.Objects; import org.apache.fory.annotation.Internal; +import org.apache.fory.json.PropertyNamingStrategy; /** Immutable identity for settings which can change generated Fory JSON source. */ @Internal @@ -31,7 +32,7 @@ public final class JsonCodegenKey { private final String codecRegistryKey; private final String mixinKey; - JsonCodegenKey( + public JsonCodegenKey( boolean writeNullFields, boolean propertyDiscoveryEnabled, PropertyNamingStrategy propertyNamingStrategy, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java similarity index 86% rename from java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java rename to java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java index 228f955fb1..70200ef94b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonGeneratedClassRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.fory.json; +package org.apache.fory.json.resolver; import java.lang.reflect.Type; import java.util.Collections; @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import org.apache.fory.annotation.Internal; +import org.apache.fory.json.codegen.JsonCodegenKey; import org.apache.fory.json.resolver.JsonSharedRegistry.GeneratedClasses; /** Frozen Native Image mapping from JSON configuration semantics to generated classes. */ @@ -37,11 +38,13 @@ public final class JsonGeneratedClassRegistry { private JsonGeneratedClassRegistry() {} - static synchronized Set> register( - JsonCodegenKey key, GeneratedClasses generatedClasses) { + /** Publishes one hosted configuration's generated classes during Native Image analysis. */ + public static synchronized Set> register( + JsonCodegenKey key, JsonSharedRegistry hostedRegistry) { if (frozen) { throw new IllegalStateException("Fory JSON generated class registry is frozen"); } + GeneratedClasses generatedClasses = hostedRegistry.generatedClasses(); MutableConfiguration configuration = pending.get(key); if (configuration == null) { configuration = new MutableConfiguration(); @@ -53,7 +56,8 @@ static synchronized Set> register( return added; } - static synchronized void freeze() { + /** Finalizes generated class lookup after Native Image analysis. */ + public static synchronized void freeze() { if (frozen) { return; } @@ -69,15 +73,11 @@ private static Map snapshot() { return Collections.unmodifiableMap(snapshot); } - /** Returns the immutable generated classes for {@code key}, or {@code null}. */ - @Internal - public static Configuration configuration(JsonCodegenKey key) { + static Configuration configuration(JsonCodegenKey key) { return configurations.get(key); } - /** Immutable generated classes for one configuration. */ - @Internal - public static final class Configuration { + static final class Configuration { private final Map, Class> stringWriters; private final Map, Class> utf8Writers; private final Map, Class> latin1Readers; @@ -96,31 +96,31 @@ private Configuration(MutableConfiguration source) { utf8CollectionReaders = immutable(source.utf8CollectionReaders); } - public Class stringWriter(Class type) { + Class stringWriter(Class type) { return stringWriters.get(type); } - public Class utf8Writer(Class type) { + Class utf8Writer(Class type) { return utf8Writers.get(type); } - public Class latin1Reader(Class type) { + Class latin1Reader(Class type) { return latin1Readers.get(type); } - public Class utf16Reader(Class type) { + Class utf16Reader(Class type) { return utf16Readers.get(type); } - public Class utf8Reader(Class type) { + Class utf8Reader(Class type) { return utf8Readers.get(type); } - public Class utf8CollectionWriter(Type type) { + Class utf8CollectionWriter(Type type) { return utf8CollectionWriters.get(type); } - public Class utf8CollectionReader(Type type) { + Class utf8CollectionReader(Type type) { return utf8CollectionReaders.get(type); } 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 e2e29d9c98..cce8f903ea 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 @@ -98,10 +98,7 @@ import org.apache.fory.codegen.GeneratedClassNames; import org.apache.fory.exception.InsecureException; import org.apache.fory.json.ForyJsonException; -import org.apache.fory.json.JsonCodegenKey; import org.apache.fory.json.JsonConfig; -import org.apache.fory.json.JsonGeneratedClassRegistry; -import org.apache.fory.json.JsonGeneratedClassRegistry.Configuration; import org.apache.fory.json.JsonTypeCheckContext; import org.apache.fory.json.JsonTypeChecker; import org.apache.fory.json.PropertyNamingStrategy; @@ -122,10 +119,12 @@ import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.SqlJsonCodecs; import org.apache.fory.json.codegen.JsonCodegen; +import org.apache.fory.json.codegen.JsonCodegenKey; import org.apache.fory.json.codegen.JsonJITContext; import org.apache.fory.json.meta.JsonAnySetterAccessor; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.resolver.JsonGeneratedClassRegistry.Configuration; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.reflect.ReflectionUtils; @@ -276,8 +275,7 @@ public static JsonSharedRegistry forHostedCodegen(JsonConfig config) { } /** Returns a complete immutable snapshot of every synchronously generated class. */ - @Internal - public GeneratedClasses generatedClasses() { + GeneratedClasses generatedClasses() { if (codegen == null || asyncCompilationEnabled) { throw new IllegalStateException("Generated class snapshots require synchronous codegen"); } @@ -309,9 +307,7 @@ private static Map> completedClasses( return Collections.unmodifiableMap(classes); } - /** Immutable hosted snapshot of generated classes for one configuration. */ - @Internal - public static final class GeneratedClasses { + static final class GeneratedClasses { private final Map, Class> stringWriters; private final Map, Class> utf8Writers; private final Map, Class> latin1Readers; @@ -337,31 +333,31 @@ private GeneratedClasses( this.utf8CollectionReaders = utf8CollectionReaders; } - public Map, Class> stringWriters() { + Map, Class> stringWriters() { return stringWriters; } - public Map, Class> utf8Writers() { + Map, Class> utf8Writers() { return utf8Writers; } - public Map, Class> latin1Readers() { + Map, Class> latin1Readers() { return latin1Readers; } - public Map, Class> utf16Readers() { + Map, Class> utf16Readers() { return utf16Readers; } - public Map, Class> utf8Readers() { + Map, Class> utf8Readers() { return utf8Readers; } - public Map> utf8CollectionWriters() { + Map> utf8CollectionWriters() { return utf8CollectionWriters; } - public Map> utf8CollectionReaders() { + Map> utf8CollectionReaders() { return utf8CollectionReaders; } } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index 703608ad7c..cf37b1cb3b 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -60,8 +60,10 @@ import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.Base64ByteArrayCodec; import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codegen.JsonCodegenKey; import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; +import org.apache.fory.json.resolver.JsonGeneratedClassRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonSharedRegistry.JsonMixinView; import org.apache.fory.json.resolver.JsonTypeResolver; @@ -282,8 +284,7 @@ private boolean generateConfigurations(DuringAnalysisAccess access) { "Cannot generate Fory JSON codecs for " + model.getName(), e); } Set> generatedClasses = - JsonGeneratedClassRegistry.register( - entry.getKey(), configuration.registry.generatedClasses()); + JsonGeneratedClassRegistry.register(entry.getKey(), configuration.registry); for (Class generatedClass : generatedClasses) { registerGeneratedClass(generatedClass); } 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 index c1ac343343..7feb4e41fb 100644 --- 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 @@ -21,10 +21,7 @@ Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature \ --initialize-at-build-time=org.apache.fory.json.ForyJson,\ org.apache.fory.json.ForyJsonBuilder,\ - org.apache.fory.json.JsonCodegenKey,\ org.apache.fory.json.JsonConfig,\ - org.apache.fory.json.JsonGeneratedClassRegistry,\ - org.apache.fory.json.JsonGeneratedClassRegistry$Configuration,\ org.apache.fory.json.PropertyNamingStrategy,\ org.apache.fory.json.codegen,\ org.apache.fory.json.codec,\ diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java index 209e82afba..2fee6272da 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java @@ -58,10 +58,7 @@ public final class ForyJsonGraalVMFeatureJarVerifier { private static final String BUILD_TIME_TARGETS = "org.apache.fory.json.ForyJson," + "org.apache.fory.json.ForyJsonBuilder," - + "org.apache.fory.json.JsonCodegenKey," + "org.apache.fory.json.JsonConfig," - + "org.apache.fory.json.JsonGeneratedClassRegistry," - + "org.apache.fory.json.JsonGeneratedClassRegistry$Configuration," + "org.apache.fory.json.PropertyNamingStrategy," + "org.apache.fory.json.codegen," + "org.apache.fory.json.codec," From 97058e51b51f886dd78286cd253df9efb5dfe578 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Mon, 3 Aug 2026 12:37:18 +0800 Subject: [PATCH 15/15] refactor(json): unify native access caches --- .../apache/fory/json/codec/ObjectCodec.java | 62 ++---- .../fory/json/meta/JsonCreatorInfo.java | 184 +++++------------- .../fory/json/meta/JsonFieldAccessor.java | 131 +++---------- .../json/resolver/JsonStringValueCodec.java | 35 +--- .../json/resolver/JsonValueDeclaration.java | 5 +- .../fory/json/ForyJsonGraalVMFeature.java | 70 +++---- 6 files changed, 131 insertions(+), 356 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java index 1dc9ac5318..88f2b607dd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ObjectCodec.java @@ -25,10 +25,11 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.apache.fory.annotation.Internal; +import org.apache.fory.collection.ClassValueCache; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.PropertyNamingStrategy; import org.apache.fory.json.annotation.JsonCodec; @@ -159,18 +160,6 @@ static ObjectCodec createCodec( instantiator); } - /** Prepares one {@code JsonAnySetter} handle for Native Image runtime metadata construction. */ - @Internal - public static void prepareNativeAnySetter(Method method) { - AnyInfo.prepareNativeSetter(method); - } - - /** Freezes all Native Image {@code JsonAnySetter} handles after hosted analysis. */ - @Internal - public static void freezeNativeAnySetters() { - AnyInfo.freezeNativeSetters(); - } - /** Returns whether hosted metadata must retain this method for object discovery. */ @Internal public static boolean usesJsonMetadata(Method method, boolean record) { @@ -1425,8 +1414,10 @@ private void writeAnyMembers(Utf8JsonWriter writer, T value, int written) { @Internal public static final class AnyInfo { - private static Map nativeSetterHandles = new HashMap<>(); - private static boolean nativeSetterHandlesFrozen; + // Hosted discovery and runtime codec construction both resolve Any setters through + // anySetterHandle, so the image heap owns the single cache of prepared handles. + private static final ClassValueCache> + NATIVE_SETTER_HANDLES = ClassValueCache.newClassKeyCache(32); private final Field writeField; private final Method writeGetter; @@ -1475,7 +1466,7 @@ public static final class AnyInfo { setterHandle = readSetter == null || generatedSetter != null || AndroidSupport.IS_ANDROID ? null - : methodHandle(readSetter); + : anySetterHandle(readSetter); if (readSetter != null && generatedSetter == null && AndroidSupport.IS_ANDROID) { readSetter.setAccessible(true); } @@ -1601,39 +1592,24 @@ private void put(Object target, String name, Object value) { } } - private static MethodHandle methodHandle(Method method) { - if (GraalvmSupport.isGraalRuntime()) { - MethodHandle handle = nativeSetterHandles.get(method); - if (handle == null) { - throw new ForyJsonException( - "Missing Native Image Fory JSON Any setter metadata for " + method); - } - return handle; + /** Returns the invocation handle for one {@code JsonAnySetter} method. */ + @Internal + public static MethodHandle anySetterHandle(Method method) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + ConcurrentMap handles = + NATIVE_SETTER_HANDLES.get(method.getDeclaringClass(), ConcurrentHashMap::new); + return handles.computeIfAbsent(method, AnyInfo::newAnySetterHandle); } + return newAnySetterHandle(method); + } + + private static MethodHandle newAnySetterHandle(Method method) { try { return _JDKAccess._trustedLookup(method.getDeclaringClass()).unreflect(method); } catch (IllegalAccessException e) { throw new ForyJsonException("Cannot access @JsonAnySetter " + method, e); } } - - private static synchronized void prepareNativeSetter(Method method) { - if (!GraalvmSupport.isGraalBuildTime() || nativeSetterHandlesFrozen) { - throw new IllegalStateException("Fory JSON native Any setter cache is not writable"); - } - nativeSetterHandles.putIfAbsent(method, methodHandle(method)); - } - - private static synchronized void freezeNativeSetters() { - if (nativeSetterHandlesFrozen) { - return; - } - nativeSetterHandles = - nativeSetterHandles.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(nativeSetterHandles)); - nativeSetterHandlesFrozen = true; - } } /** Owns one parameterized POJO binding whose child types differ from the raw-class binding. */ diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java index f68c3dd442..5a7e564c33 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorInfo.java @@ -20,23 +20,21 @@ package org.apache.fory.json.meta; import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.apache.fory.annotation.Internal; +import org.apache.fory.collection.ClassValueCache; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.internal._JDKAccess; /** @@ -50,11 +48,10 @@ */ @Internal public final class JsonCreatorInfo { - private static final MethodHandle CONSTRUCTOR_REFLECTION_INVOKER = - prepareConstructorReflectionInvoker(); - private static Map nativeInvokers = new HashMap<>(); - private static Map nativeStringInvokers = new HashMap<>(); - private static boolean nativeCreatorsFrozen; + // The Feature resolves each discovered executable through creatorHandle during analysis, and + // runtime configurations retrieve the retained handles through the same cache. + private static final ClassValueCache> NATIVE_CREATORS = + ClassValueCache.newClassKeyCache(32); private final Class ownerType; private final Executable executable; @@ -75,10 +72,7 @@ public JsonCreatorInfo( this.fields = fields; this.defaults = defaults; this.generatedCodec = generatedCodec; - invoker = - generatedCodec == null - ? buildInvoker(ownerType, executable, executable.getParameterCount()) - : null; + invoker = generatedCodec == null ? buildInvoker(executable) : null; hashes = new long[fields.length]; for (int i = 0; i < fields.length; i++) { hashes[i] = fields[i].nameHash(); @@ -150,7 +144,6 @@ private Object invoke(Object[] arguments) { try { value = (Object) invoker.invokeExact(arguments); } catch (Throwable cause) { - cause = creatorCause(cause); if (cause instanceof Error) { throw (Error) cause; } @@ -159,15 +152,6 @@ private Object invoke(Object[] arguments) { return requireResult(value); } - private Throwable creatorCause(Throwable cause) { - return GraalvmSupport.isGraalRuntime() - && JdkVersion.MAJOR_VERSION >= 25 - && executable instanceof Constructor - && cause instanceof InvocationTargetException - ? cause.getCause() - : cause; - } - private Object requireResult(Object value) { if (value == null || value.getClass() != ownerType) { throw new ForyJsonException( @@ -176,22 +160,26 @@ private Object requireResult(Object value) { return value; } - private static MethodHandle buildInvoker( - Class ownerType, Executable executable, int parameterCount) { + private static MethodHandle buildInvoker(Executable executable) { if (AndroidSupport.IS_ANDROID) { // Android has no supported trusted MethodHandle lookup. Creator shape validation guarantees // a public executable; accessibility is needed only when its declaring class is non-public. executable.setAccessible(true); return null; } - if (GraalvmSupport.isGraalRuntime()) { - MethodHandle invoker = nativeInvokers.get(executable); - if (invoker == null) { - throw missingNativeCreator(executable); - } - return invoker; + return creatorHandle(executable); + } + + /** Returns the array-argument invocation handle for one JSON creator. */ + @Internal + public static MethodHandle creatorHandle(Executable executable) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return nativeCreatorHandles(executable).arrayInvoker; } - MethodHandle target = creatorTarget(ownerType, executable); + return arrayInvoker(creatorTarget(executable), executable.getParameterCount()); + } + + private static MethodHandle arrayInvoker(MethodHandle target, int parameterCount) { // The interpreted reader already owns one trusted fixed-size argument array. Spread that // exact array into the creator without a second carrier or per-call reflective access check. return target @@ -201,115 +189,49 @@ private static MethodHandle buildInvoker( /** Returns the one-String-argument creator used by a JsonValue representation. */ @Internal - public static MethodHandle stringCreatorHandle(Class ownerType, Executable executable) { - if (GraalvmSupport.isGraalRuntime()) { - MethodHandle invoker = nativeStringInvokers.get(executable); - if (invoker == null) { - throw missingNativeCreator(executable); - } - return invoker; - } - return creatorTarget(ownerType, executable) - .asType(MethodType.methodType(Object.class, String.class)); - } - - /** Prepares the exact Native Image runtime handle for one registered creator. */ - @Internal - public static synchronized void prepareNativeCreator(Class ownerType, Executable executable) { - if (!GraalvmSupport.isGraalBuildTime() || nativeCreatorsFrozen) { - throw new IllegalStateException("Fory JSON native creator cache is not writable"); - } - MethodHandle target; - MethodHandle invoker; - if (executable instanceof Constructor && JdkVersion.MAJOR_VERSION >= 25) { - target = constructorReflectionTarget(ownerType, (Constructor) executable); - invoker = target; - } else { - target = creatorTarget(ownerType, executable); - invoker = - target - .asSpreader(Object[].class, executable.getParameterCount()) - .asType(MethodType.methodType(Object.class, Object[].class)); - } - nativeInvokers.putIfAbsent(executable, invoker); - if (executable.getParameterCount() == 1 && executable.getParameterTypes()[0] == String.class) { - if (!(executable instanceof Constructor) || JdkVersion.MAJOR_VERSION < 25) { - nativeStringInvokers.putIfAbsent( - executable, target.asType(MethodType.methodType(Object.class, String.class))); - } - } - } - - /** Freezes all Native Image creator handles after hosted analysis. */ - @Internal - public static synchronized void freezeNativeCreators() { - if (nativeCreatorsFrozen) { - return; + public static MethodHandle stringCreatorHandle(Executable executable) { + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return nativeCreatorHandles(executable).stringInvoker; } - nativeInvokers = immutable(nativeInvokers); - nativeStringInvokers = immutable(nativeStringInvokers); - nativeCreatorsFrozen = true; - } - - private static Map immutable(Map invokers) { - return invokers.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(invokers)); + return creatorTarget(executable).asType(MethodType.methodType(Object.class, String.class)); } - private static ForyJsonException missingNativeCreator(Executable executable) { - return new ForyJsonException( - "Missing Native Image Fory JSON creator metadata for " + executable); + private static CreatorHandles nativeCreatorHandles(Executable executable) { + ConcurrentMap creators = + NATIVE_CREATORS.get(executable.getDeclaringClass(), ConcurrentHashMap::new); + return creators.computeIfAbsent(executable, JsonCreatorInfo::newCreatorHandles); } - /** Returns the prepared array-argument creator used by GraalVM 25 constructors. */ - @Internal - public static MethodHandle arrayCreatorHandle(Class ownerType, Executable executable) { - return buildInvoker(ownerType, executable, executable.getParameterCount()); - } - - private static MethodHandle constructorReflectionTarget( - Class ownerType, Constructor constructor) { - return MethodHandles.insertArguments( - CONSTRUCTOR_REFLECTION_INVOKER.bindTo(constructor), 1, true, ownerType); + private static CreatorHandles newCreatorHandles(Executable executable) { + MethodHandle target = creatorTarget(executable); + MethodHandle stringInvoker = + executable.getParameterCount() == 1 && executable.getParameterTypes()[0] == String.class + ? target.asType(MethodType.methodType(Object.class, String.class)) + : null; + return new CreatorHandles(arrayInvoker(target, executable.getParameterCount()), stringInvoker); } - private static MethodHandle prepareConstructorReflectionInvoker() { - if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE || JdkVersion.MAJOR_VERSION < 25) { - return null; - } + private static MethodHandle creatorTarget(Executable executable) { + Class declaringClass = executable.getDeclaringClass(); try { - // GraalVM 25 implements direct constructor MethodHandles through a reflection bridge whose - // synthetic caller cannot access concealed model packages. Preserve the executable's normal - // public access contract by supplying its declaring class as the caller. The resulting - // per-executable handle is prepared at image build time and performs no runtime lookup. - return _JDKAccess._trustedLookup(Constructor.class) - .findVirtual( - Constructor.class, - "newInstanceWithCaller", - MethodType.methodType(Object.class, Object[].class, boolean.class, Class.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new ForyJsonException("Cannot prepare GraalVM 25 JSON constructor invocation", e); + // A target-class trusted lookup has full member access without requiring the application + // module to export or open its model package. + return executable instanceof Constructor + ? _JDKAccess._trustedLookup(declaringClass) + .unreflectConstructor((Constructor) executable) + : _JDKAccess._trustedLookup(declaringClass).unreflect((Method) executable); + } catch (IllegalAccessException e) { + throw new ForyJsonException("Cannot access JSON creator " + executable, e); } } - private static MethodHandle creatorTarget(Class ownerType, Executable executable) { - try { - // A target-class trusted lookup has full member access without requiring the application - // module to export or open its model package. - if (executable instanceof Constructor) { - return _JDKAccess._trustedLookup(ownerType) - .findConstructor( - ownerType, MethodType.methodType(void.class, executable.getParameterTypes())); - } - Method factory = (Method) executable; - return _JDKAccess._trustedLookup(factory.getDeclaringClass()) - .findStatic( - factory.getDeclaringClass(), - factory.getName(), - MethodType.methodType(factory.getReturnType(), factory.getParameterTypes())); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new ForyJsonException("Cannot access JSON creator for " + ownerType.getName(), e); + private static final class CreatorHandles { + private final MethodHandle arrayInvoker; + private final MethodHandle stringInvoker; + + private CreatorHandles(MethodHandle arrayInvoker, MethodHandle stringInvoker) { + this.arrayInvoker = arrayInvoker; + this.stringInvoker = stringInvoker; } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index 0821f3018d..b4f3ad7fe1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -22,17 +22,16 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import org.apache.fory.annotation.Internal; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.apache.fory.collection.ClassValueCache; import org.apache.fory.json.ForyJsonException; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.FieldAccessor; -import org.apache.fory.util.record.RecordUtils; /** * Uniform interpreted object-member access for fields, getters, and setters. @@ -43,10 +42,10 @@ * generated codecs consume the original field or method metadata and emit direct expressions. */ public abstract class JsonFieldAccessor { - private static Map nativeFields = new HashMap<>(); - private static Map nativeGetters = new HashMap<>(); - private static Map nativeSetters = new HashMap<>(); - private static boolean nativeCachesFrozen; + // The Feature calls the ordinary factories during analysis, so Native Image retains the same + // accessor instances later returned while runtime configurations build interpreted codecs. + private static final ClassValueCache> NATIVE_ACCESSORS = + ClassValueCache.newClassKeyCache(32); public Object getObject(Object target) { throw new UnsupportedOperationException(); @@ -137,92 +136,40 @@ public void putChar(Object target, char value) { } public static JsonFieldAccessor forField(Field field) { - if (GraalvmSupport.isGraalRuntime()) { - return requireNativeAccessor(nativeFields.get(field), field); + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return nativeAccessors(field).computeIfAbsent(field, JsonFieldAccessor::newFieldAccessor); } - return new FieldJsonAccessor(FieldAccessor.createAccessor(field)); + return newFieldAccessor(field); } public static JsonFieldAccessor forGetter(Method getter) { - if (GraalvmSupport.isGraalRuntime()) { - return requireNativeAccessor(nativeGetters.get(getter), getter); + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return nativeAccessors(getter).computeIfAbsent(getter, JsonFieldAccessor::newGetterAccessor); } - return new GetterJsonAccessor(getter); + return newGetterAccessor(getter); } public static JsonFieldAccessor forSetter(Method setter) { - if (GraalvmSupport.isGraalRuntime()) { - return requireNativeAccessor(nativeSetters.get(setter), setter); + if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) { + return nativeAccessors(setter).computeIfAbsent(setter, JsonFieldAccessor::newSetterAccessor); } - return new SetterJsonAccessor(setter); - } - - /** Prepares one field accessor for Native Image runtime metadata construction. */ - @Internal - public static synchronized void prepareField(Field field) { - requireNativeBuildTime(); - // Core field access intentionally falls back to Method.invoke for record backing fields in a - // native image. JSON retains the semantic field identity but must cache the component accessor - // MethodHandle here so interpreted codecs never perform runtime reflection. - JsonFieldAccessor accessor = - RecordUtils.isRecord(field.getDeclaringClass()) - ? new RecordFieldJsonAccessor(field) - : new FieldJsonAccessor(FieldAccessor.createAccessor(field)); - putPrepared(nativeFields, field, accessor); - } - - /** Prepares one getter accessor for Native Image runtime metadata construction. */ - @Internal - public static synchronized void prepareGetter(Method getter) { - requireNativeBuildTime(); - putPrepared(nativeGetters, getter, new GetterJsonAccessor(getter)); - } - - /** Prepares one setter accessor for Native Image runtime metadata construction. */ - @Internal - public static synchronized void prepareSetter(Method setter) { - requireNativeBuildTime(); - putPrepared(nativeSetters, setter, new SetterJsonAccessor(setter)); - } - - /** Freezes all Native Image accessors after hosted analysis. */ - @Internal - public static synchronized void freezeNativeAccessors() { - if (nativeCachesFrozen) { - return; - } - nativeFields = immutable(nativeFields); - nativeGetters = immutable(nativeGetters); - nativeSetters = immutable(nativeSetters); - nativeCachesFrozen = true; + return newSetterAccessor(setter); } - private static void requireNativeBuildTime() { - if (!GraalvmSupport.isGraalBuildTime() || nativeCachesFrozen) { - throw new IllegalStateException("Fory JSON native accessor cache is not writable"); - } + private static ConcurrentMap nativeAccessors(Member member) { + return NATIVE_ACCESSORS.get(member.getDeclaringClass(), ConcurrentHashMap::new); } - private static void putPrepared( - Map accessors, M member, JsonFieldAccessor accessor) { - JsonFieldAccessor previous = accessors.putIfAbsent(member, accessor); - if (previous != null && previous.getClass() != accessor.getClass()) { - throw new IllegalStateException("Conflicting Fory JSON accessor for " + member); - } + private static JsonFieldAccessor newFieldAccessor(Member member) { + return new FieldJsonAccessor(FieldAccessor.createAccessor((Field) member)); } - private static Map immutable(Map accessors) { - return accessors.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(accessors)); + private static JsonFieldAccessor newGetterAccessor(Member member) { + return new GetterJsonAccessor((Method) member); } - private static JsonFieldAccessor requireNativeAccessor( - JsonFieldAccessor accessor, Object member) { - if (accessor == null) { - throw new ForyJsonException("Missing Native Image Fory JSON accessor metadata for " + member); - } - return accessor; + private static JsonFieldAccessor newSetterAccessor(Member member) { + return new SetterJsonAccessor((Method) member); } private static final class FieldJsonAccessor extends JsonFieldAccessor { @@ -333,34 +280,6 @@ public void putChar(Object target, char value) { } } - private static final class RecordFieldJsonAccessor extends JsonFieldAccessor { - private final Field field; - private final MethodHandle getterHandle; - - private RecordFieldJsonAccessor(Field field) { - this.field = field; - try { - getterHandle = methodHandle(field.getDeclaringClass().getDeclaredMethod(field.getName())); - } catch (NoSuchMethodException e) { - throw new ForyJsonException("Cannot find JSON record accessor for " + field, e); - } - } - - @Override - public Field field() { - return field; - } - - @Override - public Object getObject(Object target) { - try { - return getterHandle.invoke(target); - } catch (Throwable e) { - throw new ForyJsonException("Cannot access JSON record field " + field, e); - } - } - } - private static final class GetterJsonAccessor extends JsonFieldAccessor { private final Method getter; private final MethodHandle getterHandle; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java index 06a613b1a7..f7b4425a7a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonStringValueCodec.java @@ -36,8 +36,6 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.platform.AndroidSupport; -import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.JdkVersion; /** Complete String representation selected by one effective {@code JsonValue} member. */ final class JsonStringValueCodec implements JsonValueCodec { @@ -162,14 +160,8 @@ static ValueCreator forExecutable( if (generatedCodec != null) { return new GeneratedCreator(ownerType, generatedCodec); } - if (GraalvmSupport.isGraalRuntime() - && JdkVersion.MAJOR_VERSION >= 25 - && executable instanceof Constructor) { - return new ReflectionCreator( - ownerType, JsonCreatorInfo.arrayCreatorHandle(ownerType, executable)); - } if (!AndroidSupport.IS_ANDROID) { - return new MethodHandleCreator(ownerType, buildInvoker(ownerType, executable)); + return new MethodHandleCreator(ownerType, buildInvoker(executable)); } executable.setAccessible(true); return executable instanceof Constructor @@ -177,8 +169,8 @@ static ValueCreator forExecutable( : new FactoryCreator(ownerType, (Method) executable); } - private static MethodHandle buildInvoker(Class ownerType, Executable executable) { - return JsonCreatorInfo.stringCreatorHandle(ownerType, executable); + private static MethodHandle buildInvoker(Executable executable) { + return JsonCreatorInfo.stringCreatorHandle(executable); } } @@ -221,27 +213,6 @@ Object create(JsonReader reader, String value) { } } - private static final class ReflectionCreator extends ValueCreator { - private final MethodHandle invoker; - - private ReflectionCreator(Class ownerType, MethodHandle invoker) { - super(ownerType); - this.invoker = invoker; - } - - @Override - Object create(JsonReader reader, String value) { - try { - Object result = (Object) invoker.invokeExact(reader.creatorArguments(value)); - return requireResult(result); - } catch (Throwable cause) { - throw creatorFailure(cause instanceof InvocationTargetException ? cause.getCause() : cause); - } finally { - reader.clearCreatorArguments(); - } - } - } - private static final class ConstructorCreator extends ValueCreator { private final Constructor constructor; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java index b4e3fb6983..6193ac5b84 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonValueDeclaration.java @@ -174,10 +174,7 @@ boolean record = && (registry.annotation(type, field, JsonRawValue.class) != null) == (registry.annotation(type, method, JsonRawValue.class) != null)) { members.clear(); - members.add( - generatedCodec != null && generatedCodec.validatedAccessor(method) != null - ? method - : field); + members.add(method); } } diff --git a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java index cf37b1cb3b..5078331175 100644 --- a/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java +++ b/java/fory-json/src/main/java17/org/apache/fory/json/ForyJsonGraalVMFeature.java @@ -155,8 +155,7 @@ private boolean registerProvider(Class providerClass) { || Modifier.isAbstract(modifiers) || providerClass.isInterface() || providerClass.isEnum()) { - throw providerFailure( - providerClass, "must be a public concrete class", null); + throw providerFailure(providerClass, "must be a public concrete class", null); } Constructor constructor; try { @@ -323,8 +322,7 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { registerContainer(type); registerDeclarations(type); JsonCodec directTypeCodec = type.getDeclaredAnnotation(JsonCodec.class); - boolean hasTypeCodec = - directTypeCodec != null || hasInheritedTypeCodec(type, null); + boolean hasTypeCodec = directTypeCodec != null || hasInheritedTypeCodec(type, null); boolean hasJsonValue = (!hasTypeCodec || isCompleteTypeCodec(directTypeCodec)) && registerJsonValueDeclarations(access, type, null); @@ -336,7 +334,7 @@ private boolean registerModel(DuringAnalysisAccess access, Class type) { if (!intrinsicType && !hasTypeCodec && !hasJsonValue && subTypes == null) { registerModelHierarchy(access, type); if (type.isRecord()) { - prepareRecord(type); + registerRecord(type); } else if (!Modifier.isAbstract(type.getModifiers())) { ObjectInstantiators.getObjectInstantiator(type); if (GraalvmSupport.needReflectionRegisterForCreation(type)) { @@ -383,7 +381,7 @@ private boolean registerMixin( if (!intrinsicTarget && !hasTypeCodec && !hasJsonValue && subTypes == null) { registerModelHierarchy(access, targetType, annotations); if (targetType.isRecord()) { - prepareRecord(targetType); + registerRecord(targetType); } else if (!Modifier.isAbstract(targetType.getModifiers())) { ObjectInstantiators.getObjectInstantiator(targetType); if (GraalvmSupport.needReflectionRegisterForCreation(targetType)) { @@ -432,10 +430,10 @@ private boolean registerJsonValueDeclarations( if (annotation(annotations, field, JsonValue.class) != null) { hasValue = true; RuntimeReflection.register(field); - // JsonValueDeclaration deliberately coalesces a Record component's propagated field and - // accessor annotations to the backing field on the interpreted path. - JsonFieldAccessor.prepareField(field); - if (Runtime.version().feature() <= 24) { + if (!field.getDeclaringClass().isRecord()) { + JsonFieldAccessor.forField(field); + } + if (!field.getDeclaringClass().isRecord() && Runtime.version().feature() <= 24) { access.registerAsUnsafeAccessed(field); } registerOccurrenceCodecs(annotations, field); @@ -446,7 +444,7 @@ private boolean registerJsonValueDeclarations( if (annotation(annotations, method, JsonValue.class) != null) { hasValue = true; RuntimeReflection.register(method); - JsonFieldAccessor.prepareGetter(method); + JsonFieldAccessor.forGetter(method); registerOccurrenceCodecs(annotations, method); } } @@ -458,7 +456,7 @@ private boolean registerJsonValueDeclarations( && annotation(annotations, method, JsonValue.class) != null) { hasValue = true; RuntimeReflection.register(method); - JsonFieldAccessor.prepareGetter(method); + JsonFieldAccessor.forGetter(method); registerOccurrenceCodecs(annotations, method); } } @@ -467,16 +465,16 @@ && annotation(annotations, method, JsonValue.class) != null) { return false; } if (type.isRecord()) { - prepareRecord(type); + registerRecord(type); } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - registerCreator(type, constructor); + registerCreator(constructor); } } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - registerCreator(type, method); + registerCreator(method); } } return true; @@ -499,9 +497,6 @@ private void registerReflectiveDeclarations(Set declarations) @Override public void afterAnalysis(AfterAnalysisAccess access) { JsonGeneratedClassRegistry.freeze(); - JsonFieldAccessor.freezeNativeAccessors(); - JsonCreatorInfo.freezeNativeCreators(); - ObjectCodec.freezeNativeAnySetters(); } private void registerModelHierarchy(DuringAnalysisAccess access, Class type) { @@ -524,7 +519,7 @@ boolean record = type.isRecord(); for (Field field : current.getDeclaredFields()) { if (isJsonField(field)) { if (!record) { - JsonFieldAccessor.prepareField(field); + JsonFieldAccessor.forField(field); } if (!current.isRecord() && Runtime.version().feature() <= 24) { access.registerAsUnsafeAccessed(field); @@ -539,14 +534,14 @@ boolean record = type.isRecord(); } for (Method method : current.getDeclaredMethods()) { if (annotation(annotations, method, JsonValue.class) != null) { - prepareMethodAccessors(annotations, method); + resolveMethodAccessors(annotations, method); } } } for (Method method : type.getMethods()) { boolean mixinSelector = hasMixinSelector(annotations, method); if (ObjectCodec.usesJsonMetadata(method, record) || mixinSelector) { - prepareMethodAccessors(annotations, method); + resolveMethodAccessors(annotations, method); if (method.getDeclaringClass().isInterface()) { RuntimeReflection.register(method); } @@ -569,16 +564,15 @@ boolean record = type.isRecord(); } for (Constructor constructor : type.getDeclaredConstructors()) { if (annotation(annotations, constructor, JsonCreator.class) != null) { - registerCreator(type, constructor); + registerCreator(constructor); registerParameterCodecs(annotations, constructor.getParameters()); registerResolvedParameterTypes(ownerType, constructor.getParameters()); - registerUnwrappedParameters( - access, ownerType, annotations, constructor.getParameters()); + registerUnwrappedParameters(access, ownerType, annotations, constructor.getParameters()); } } for (Method method : type.getDeclaredMethods()) { if (annotation(annotations, method, JsonCreator.class) != null) { - registerCreator(type, method); + registerCreator(method); registerParameterCodecs(annotations, method.getParameters()); registerResolvedParameterTypes(ownerType, method.getParameters()); registerUnwrappedParameters(access, ownerType, annotations, method.getParameters()); @@ -586,35 +580,34 @@ boolean record = type.isRecord(); } } - private void prepareRecord(Class type) { + private void registerRecord(Class type) { RuntimeReflection.registerAllRecordComponents(type); - RecordUtils.prepareRecordComponentGetters(type); for (RecordComponent component : type.getRecordComponents()) { - JsonFieldAccessor.prepareGetter(component.getAccessor()); + JsonFieldAccessor.forGetter(component.getAccessor()); } Constructor constructor = RecordUtils.getRecordConstructor(type).f0; - registerCreator(type, constructor); + registerCreator(constructor); } - private void registerCreator(Class ownerType, Executable executable) { + private void registerCreator(Executable executable) { if (processedCreators.add(executable)) { RuntimeReflection.register(executable); - JsonCreatorInfo.prepareNativeCreator(ownerType, executable); + JsonCreatorInfo.creatorHandle(executable); } } - private static void prepareMethodAccessors(JsonMixinView annotations, Method method) { + private static void resolveMethodAccessors(JsonMixinView annotations, Method method) { int modifiers = method.getModifiers(); if (Modifier.isStatic(modifiers) || method.isSynthetic() || method.isBridge()) { return; } if (method.getParameterCount() == 0 && method.getReturnType() != void.class) { - JsonFieldAccessor.prepareGetter(method); + JsonFieldAccessor.forGetter(method); } else if (method.getParameterCount() == 1 && method.getReturnType() == void.class) { - JsonFieldAccessor.prepareSetter(method); + JsonFieldAccessor.forSetter(method); } if (annotation(annotations, method, JsonAnySetter.class) != null) { - ObjectCodec.prepareNativeAnySetter(method); + ObjectCodec.AnyInfo.anySetterHandle(method); } } @@ -656,15 +649,13 @@ private boolean registerDeclarations(Class type) { return changed; } - private void registerParameterCodecs( - JsonMixinView annotations, Parameter[] parameters) { + private void registerParameterCodecs(JsonMixinView annotations, Parameter[] parameters) { for (Parameter parameter : parameters) { registerCodecs(annotation(annotations, parameter, JsonCodec.class)); } } - private void registerOccurrenceCodecs( - JsonMixinView annotations, AnnotatedElement element) { + private void registerOccurrenceCodecs(JsonMixinView annotations, AnnotatedElement element) { registerCodecs(annotation(annotations, element, JsonCodec.class)); if (annotation(annotations, element, JsonBase64.class) != null) { registerCodec(Base64ByteArrayCodec.class); @@ -909,5 +900,4 @@ public int hashCode() { return 31 * name.hashCode() + Arrays.hashCode(parameterTypes); } } - }