From a30f3676cff3144d5ee4b0a334d9a234aeca7268 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Fri, 21 Aug 2026 23:51:28 +0800 Subject: [PATCH 01/11] update java.md --- .agents/languages/java.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 28c6ef0ebd..be75624877 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -77,7 +77,27 @@ Load this file when changing anything under `java/` or when Java drives a cross- - For GraalVM, use `fory codegen` to generate serializers when building native images. Do not add reflection configuration except for JDK `proxy`. - In Java native mode (`xlang=false`), only `Types.BOOL` through `Types.STRING` share type IDs with xlang mode. Other native-mode type IDs differ. - Choose one serializer ownership location per logical Java type family. Add native/xlang serializer variants only when the wire format or constructor contract truly differs. -- Do not add normal-JVM process-global caches keyed by user classes, generated classes, serializer classes, classloaders, or class-bound method handles. Prefer per-runtime state, immutable shared metadata, or build-time-only template data. +- Fory JSON must keep one immutable static set of exact Java types whose default representation is + implemented by type-specific `JsonReader` or `JsonWriter` methods. The set contains all primitive + scalar types and their boxed classes, `String`, `CharSequence`, `Number`, `BigInteger`, + `BigDecimal`, `UUID`, `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `Duration`, `ZoneOffset`, + `ZonedDateTime`, `Year`, `YearMonth`, `MonthDay`, `Period`, `OffsetTime`, `OffsetDateTime`, + `byte[]`, `String[]`, and `long[]`. Exact codec registration, exact codec-factory registration, + and exact factory handled-runtime-class claims for these types must fail before registry mutation + on every Java runtime, not only in GraalVM. Do not expand this set to every default codec: enums, + other arrays, collections, maps, atomics, optionals, `File`, `URI`, `Path`, `ByteBuffer`, calendar + and locale types, `Float16`, `BFloat16`, and user-defined types remain registerable. Field/type + `@JsonCodec`, `@JsonFormat`, and semantic metadata remain separate from exact registry mutation; + generated-class keys may omit a protected type's codec class only when the resolved role still + uses its canonical built-in path. +- Do not add normal-JVM process-global caches keyed by user classes, generated classes, serializer + classes, classloaders, or class-bound method handles. Prefer per-runtime state, immutable shared + metadata, or build-time-only template data. The only exception is Fory JSON's generated-role + class cache in `JsonCodegen`, backed by `ClassValueCache.newClassKeySoftCache`: ordinary-JVM + values may contain only generated-class keys, binary names, and completed generated classes, not + codec instances, resolvers, configured classloaders, or `CodeGenerator`. Its GraalVM branch may + be strong only during hosted analysis and must be reset after the frozen Native registry is + published. Do not extend this exception to another cache or retained value. - Concrete serializers may opt into sharing only after auditing retained fields. Treat serializers retaining `TypeResolver`, `RefResolver`, mutable scratch buffers, runtime state, or classloader-sensitive state as non-shareable unless that state is externalized. - Resolver and serializer hot paths should keep the fast-path/null-slow-path shape obvious. Hoist repeated buffer or cache-state access into locals for multi-step operations and keep rebuild/restoration logic cold. - Remote metadata and class-token paths that materialize Java classes must keep From ea4fe14b7702d9b2bf1a293d7ca881c7220bbbca Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 01:03:38 +0800 Subject: [PATCH 02/11] feat(json): share generated codec classes by exact key --- docs/json/annotations.md | 5 +- docs/json/custom-codecs.md | 18 +- docs/json/graalvm.md | 34 +- docs/json/kotlin.md | 7 +- docs/json/modules.md | 11 +- docs/json/object-mapping.md | 10 +- .../apache/fory/graalvm/ForyJsonExample.java | 90 ++- .../graalvm/closed/ClosedJsonConfigs.java | 2 - .../org/apache/fory/json/ForyJsonBuilder.java | 19 +- .../apache/fory/json/JsonCodecFactory.java | 7 +- .../java/org/apache/fory/json/JsonConfig.java | 61 +- .../org/apache/fory/json/ModuleContext.java | 10 +- .../org/apache/fory/json/ModuleInstaller.java | 6 - .../json/annotation/ForyJsonProvider.java | 9 +- .../fory/json/codegen/GeneratedCodecKey.java | 336 ++++++++++ .../apache/fory/json/codegen/JsonCodegen.java | 426 +++++++------ .../fory/json/codegen/JsonCodegenKey.java | 86 --- .../fory/json/codegen/JsonReaderCodegen.java | 14 +- .../fory/json/codegen/JsonWriterCodegen.java | 2 +- .../json/codegen/StringWriterCodegen.java | 2 +- .../fory/json/codegen/Utf8WriterCodegen.java | 2 +- .../fory/json/resolver/CodecRegistry.java | 103 ++- .../resolver/JsonGeneratedClassRegistry.java | 219 +++---- .../json/resolver/JsonSharedRegistry.java | 256 ++------ .../fory/json/resolver/JsonTypeResolver.java | 592 +++++++++++++++--- .../fory/json/ForyJsonGraalVMFeature.java | 121 ++-- .../fory/json/JsonAsyncCompilationTest.java | 1 - .../fory/json/JsonCodecRegistrationTest.java | 156 +++++ .../org/apache/fory/json/JsonCreatorTest.java | 5 +- .../fory/json/JsonFieldNameCacheTest.java | 7 +- .../json/JsonGeneratedCapabilityKeyTest.java | 265 +++++++- .../fory/json/JsonGeneratedCodecTest.java | 46 +- .../fory/json/JsonGraphMemoryBudgetTest.java | 34 +- .../org/apache/fory/json/JsonMixinTest.java | 16 +- .../org/apache/fory/json/JsonModuleTest.java | 9 +- .../fory/json/JsonRawValueAnnotationTest.java | 44 -- .../org/apache/fory/json/JsonRecordTest.java | 5 +- .../org/apache/fory/json/JsonScalarTest.java | 192 +----- .../org/apache/fory/json/JsonTestSupport.java | 15 - .../apache/fory/json/JsonTypeCheckerTest.java | 54 +- .../JsonGeneratedClassRegistryTest.java | 58 +- 41 files changed, 1968 insertions(+), 1387 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java delete mode 100644 java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java diff --git a/docs/json/annotations.md b/docs/json/annotations.md index 68f7e00dd5..bf6ad68494 100644 --- a/docs/json/annotations.md +++ b/docs/json/annotations.md @@ -41,8 +41,9 @@ construction operations. In Android builds that use R8 or ProGuard, Kotlin KSP e retention rules for Kotlin `@JsonType` models. It also processes an exact Mixin declared in application source when either the Mixin or its exact target is Kotlin. KSP does not generate codecs or construction operations. GraalVM Native Image discovers reachable Java and Kotlin `JsonType` -declarations directly, and provider-selected configurations generate codecs while the image is -built. See the [GraalVM guide](graalvm.md) and [Android guide](android.md) for the platform workflows. +declarations directly. It generates the default configuration baseline plus additions from +reachable providers, while exact misses use interpreted codecs. See the +[GraalVM guide](graalvm.md) and [Android guide](android.md) for the platform workflows. ## Kotlin use-site targets diff --git a/docs/json/custom-codecs.md b/docs/json/custom-codecs.md index bd4ed85025..85eb1c11c1 100644 --- a/docs/json/custom-codecs.md +++ b/docs/json/custom-codecs.md @@ -72,6 +72,20 @@ ForyJson json = .build(); ``` +Exact `registerCodec` and exact-class factory registration are not supported for types with +dedicated reader/writer operations: + +- `boolean`, `byte`, `short`, `int`, `long`, `float`, `double`, and `char`, including their boxed + classes +- `String`, `CharSequence`, `Number`, `BigInteger`, `BigDecimal`, and `UUID` +- `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `Duration`, `ZoneOffset`, `ZonedDateTime`, + `Year`, `YearMonth`, `MonthDay`, `Period`, `OffsetTime`, and `OffsetDateTime` +- `byte[]`, `String[]`, and `long[]` + +The restriction is exact; it does not include application subclasses. It also does not disable +occurrence-level `JsonCodec`, `JsonFormat`, or other semantic mappings. Use those mechanisms when a +field or parameter of a protected type needs a different representation. + Use `JsonCodecFactory` when one factory owns a family of declared or parameterized types: ```java @@ -310,8 +324,8 @@ decoded keys must match the declared key type. An annotation codec class must be public, concrete, top-level or static nested, and have a public no-argument constructor. One instance is shared by all annotated sites and concurrent operations of -the built `ForyJson`, so it must be thread-safe. Use `registerCodec(Target.class, instance)` when a -complete-value codec needs configuration. +the built `ForyJson`, so it must be thread-safe. For an eligible type, use +`registerCodec(Target.class, instance)` when a complete-value codec needs configuration. 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 diff --git a/docs/json/graalvm.md b/docs/json/graalvm.md index b86403e505..8742f36a4a 100644 --- a/docs/json/graalvm.md +++ b/docs/json/graalvm.md @@ -53,9 +53,11 @@ 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. At -runtime, `ForyJson.builder().build()` can therefore use interpreted codecs without application -reflection configuration, package exports or opens, or build-time initialization. +model metadata and prepares its field, property, creator, record, and `JsonAnySetter` access. It +also generates codecs for reachable models under the default configuration. At runtime, +`ForyJson.builder().build()` uses those exact generated codecs and falls back to interpreted codecs +when no exact generated entry matches, without application reflection configuration, package +exports or opens, or build-time initialization. An application class configured for build-time initialization may retain a static `ForyJson` in the image heap. Set `withConcurrencyLevel` explicitly when the runtime may have a different processor @@ -69,8 +71,8 @@ it does not create the runtime instance. ## Generated Codecs -To include generated codecs for a configuration, return that completed configuration from a -reachable `@ForyJsonProvider`: +The default configuration is generated automatically. To add generated codecs for a custom +configuration, return that completed configuration from a reachable `@ForyJsonProvider`: ```java import org.apache.fory.json.ForyJson; @@ -97,19 +99,19 @@ public final class JsonConfigs { 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 may return multiple configurations, and multiple providers may be reachable. When the +default and provider configurations produce the same exact model-role key, they reuse one +generated class. 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, 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 `ForyJson` configuration was not included, ordinary -Java models and complete value codecs use their prepared interpreted codecs, and Fory JSON logs one -process-wide warning recommending a reachable `@ForyJsonProvider`. Language-module object models -that require hosted capabilities fail before reading or writing a value. `withCodegen(false)` +Generated entries are additive: the default baseline is never removed when a provider is present, +and every reachable provider contributes its own exact entries. A codegen-enabled runtime first +looks up the exact model role for its current configuration; a miss uses the interpreted codec. +Reflection metadata is prepared independently of generated-codec matches. `withCodegen(false)` explicitly selects interpreted codecs and does not request generated-codec lookup. Asynchronous compilation is disabled in a native executable. @@ -131,10 +133,10 @@ class JsonConfigs { Annotate each reachable concrete Kotlin model with `@JsonType`, or register an exact reachable Mixin for a third-party target. Fory reads and validates Kotlin metadata while building the image, -then generates the provider-selected codecs. A provider configuration with disabled code -generation or an unsupported metadata ABI fails image construction. A Kotlin-enabled runtime -configuration that was not returned by a provider fails before it reads or writes a Kotlin object; -it never falls back to reflective construction. +then generates codecs for each reachable Kotlin-enabled provider configuration. A provider +configuration with disabled code generation or an unsupported metadata ABI fails image +construction. A Kotlin-enabled runtime configuration with no exact generated match uses its +prepared interpreted codec. An exact generic Kotlin root is available only when its complete binding is reached through a property, constructor argument, container/map child, or closed subtype of a provider-selected diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md index 96e75d3b7e..df8aae4020 100644 --- a/docs/json/kotlin.md +++ b/docs/json/kotlin.md @@ -341,9 +341,10 @@ See [Security](security.md) before decoding untrusted input. On GraalVM Native Image, use the existing `@ForyJsonProvider` workflow, install `ForyJsonKotlin`, and enable code generation in the returned configuration. Annotate each reachable concrete Kotlin model with `@JsonType`, or register an exact reachable Mixin for a third-party -target. Fory reads the Kotlin metadata and prepares generated codecs while building the image. -Only exact generic bindings reachable through provider-selected concrete roots are available. Do -not add reflection configuration or package-wide opens. +target. Fory reads the Kotlin metadata and adds generated codecs for each reachable Kotlin-enabled +provider configuration while building the image. A runtime exact miss uses the prepared +interpreted codec. Only exact generic bindings reached through concrete roots are available. Do not +add reflection configuration or package-wide opens. On Android, use API 26 or later. The runtime reads Kotlin metadata in both debug and release builds, and runtime JSON code generation remains disabled. Follow the [installation](#installation) above diff --git a/docs/json/modules.md b/docs/json/modules.md index 9eb4dd4898..0587651a8e 100644 --- a/docs/json/modules.md +++ b/docs/json/modules.md @@ -69,6 +69,8 @@ concurrent operations and must be thread-safe. Codec implementations and `JsonCodecFactory` behavior are documented in [Custom Codecs](custom-codecs.md). Modules only package those registrations for installation. +Exact module registrations reject the same dedicated scalar and array types listed there; use an +occurrence annotation or semantic mapping for those representations. Application registrations made directly on `ForyJsonBuilder` take precedence over module exact registrations. Conflicting module registrations fail during `build()` instead of depending on @@ -91,12 +93,11 @@ precedence. See [Kotlin](kotlin.md) for type tokens and optional Android minific ## Module Identity -`moduleKey()` identifies the module configuration for generated-code reuse and conflict checking. -The default key is the module class name and is sufficient for a configuration-free module. +`moduleKey()` identifies the module configuration for installation conflict checking. The default +key is the module class name and is sufficient for a configuration-free module. -A configurable module must return a deterministic key that includes every option affecting codec -selection or generated code. Do not include secrets, mutable process state, or values unrelated to -the installed JSON behavior. +A configurable module must return a deterministic key that includes every option affecting its +installed JSON behavior. Do not include secrets, mutable process state, or unrelated values. ```java public final class ConfiguredJsonModule implements ForyJsonModule { diff --git a/docs/json/object-mapping.md b/docs/json/object-mapping.md index 4e32d2032e..7f8eeb1eb5 100644 --- a/docs/json/object-mapping.md +++ b/docs/json/object-mapping.md @@ -215,7 +215,7 @@ original key type. Null map keys are rejected. | `withMaxCachedFieldNames(int)` | `DEFAULT_MAX_CACHED_FIELD_NAMES` (`8192`) | Field-name cache entries per reader; zero disables caching | | `withConcurrencyLevel(int)` | `max(1, 2 * processors)` | Maximum concurrent root operations | | `withBufferSizeLimitBytes(int)` | 2 MiB | Maximum reusable capacity retained by each pooled writer | -| `registerCodec(type, codec)` | None | Replace the exact class's complete JSON codec | +| `registerCodec(type, codec)` | None | Replace an eligible exact class's complete JSON codec | | `registerMixin(mixinType)` | None | Apply one annotation Mixin to its exact declared target | Concurrency-level and buffer-retention limits must be positive. The cached-field-name limit @@ -229,7 +229,7 @@ see [Fory JSON Security](security.md). Builder mutation after `build()` does not modify an existing `ForyJson` instance. 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 metadata. Every other builder option keeps the -behavior described above. +image, runtime compilation is unavailable. Reachable models receive a default generated baseline, +and reachable `ForyJsonProvider` configurations add exact generated entries. A runtime exact miss +uses an interpreted codec with build-time-prepared access metadata. Every other builder option +keeps the behavior described above. 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 12a7ba544a..6276badc93 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 @@ -79,10 +79,6 @@ /** 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 " - + "in this native image. Return this configuration from a reachable " - + "@ForyJsonProvider to enable generated codecs."; // Portable lower bound: the 8-byte object base plus one 4-byte int field. private static final long GRAPH_BUDGET_VALUE_BYTES = 12; private static final int REF_BYTES = GraphMemoryEstimates.REFERENCE_BYTES; @@ -127,15 +123,6 @@ public static void main(String[] args) { } } 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"); } @@ -143,11 +130,11 @@ public static void main(String[] args) { private static void testHostedCodegenConfigurations() { ForyJson providerJson = newProviderJson(); ForyJson interpretedJson = newInterpretedJson(); - exerciseCodegenConfiguration(DEFAULT_JSON, false); - exerciseCodegenConfiguration(providerJson, true); - exerciseCodegenConfiguration(interpretedJson, false); - testEmptyMixin(providerJson, true); - testEmptyMixin(interpretedJson, false); + exerciseCodegenConfiguration(DEFAULT_JSON, true, true); + exerciseCodegenConfiguration(providerJson, true, true); + exerciseCodegenConfiguration(interpretedJson, false, true); + testEmptyMixin(providerJson, true, true); + testEmptyMixin(interpretedJson, false, true); testInterpretedMetadata(interpretedJson); testPrimitiveProperties(interpretedJson); testIndependentChildCodegen(); @@ -173,8 +160,9 @@ private static ForyJson newInterpretedJson() { .build(); } - private static void testEmptyMixin(ForyJson json, boolean generated) { - CodegenProbeCodec.expect(EmptyMixinTarget.class, generated); + private static void testEmptyMixin( + ForyJson json, boolean writerGenerated, boolean readerGenerated) { + CodegenProbeCodec.expect(EmptyMixinTarget.class, writerGenerated, readerGenerated); EmptyMixinTarget value = new EmptyMixinTarget(); value.probe = new CodegenProbeValue("empty-mixin"); String encoded = json.toJson(value); @@ -231,8 +219,9 @@ private static void testPrimitiveProperties(ForyJson json) { Preconditions.checkArgument(decoded.getCharValue() == '\u4f60'); } - private static void exerciseCodegenConfiguration(ForyJson json, boolean generated) { - CodegenProbeCodec.expect(CodegenProbeModel.class, generated); + private static void exerciseCodegenConfiguration( + ForyJson json, boolean writerGenerated, boolean readerGenerated) { + CodegenProbeCodec.expect(CodegenProbeModel.class, writerGenerated, readerGenerated); CodegenProbeModel value = new CodegenProbeModel(); value.id = 41; value.probe = new CodegenProbeValue("probe"); @@ -255,16 +244,6 @@ private static void exerciseCodegenConfiguration(ForyJson json, boolean generate .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() { ClosedJsonRecord value = new ClosedJsonRecord(17, "closed"); ForyJson interpreted = ForyJson.builder().build(); @@ -861,21 +840,6 @@ public void setCharValue(char charValue) { } } - /** 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; @@ -886,46 +850,62 @@ private CodegenProbeValue(String value) { public static final class CodegenProbeCodec implements JsonValueCodec { private static Class expectedType; - private static boolean expectGenerated; + private static boolean expectGeneratedWriter; + private static boolean expectGeneratedReader; public CodegenProbeCodec() {} private static void expect(Class type, boolean generated) { + expect(type, generated, generated); + } + + private static void expect(Class type, boolean writerGenerated, boolean readerGenerated) { expectedType = type; - expectGenerated = generated; + expectGeneratedWriter = writerGenerated; + expectGeneratedReader = readerGenerated; } @Override public void writeString(StringJsonWriter writer, CodegenProbeValue value) { - checkCapability(writer.typeResolver().getTypeInfo(expectedType, expectedType).stringWriter()); + checkCapability( + writer.typeResolver().getTypeInfo(expectedType, expectedType).stringWriter(), + expectGeneratedWriter); writer.writeString(value == null ? null : value.value); } @Override public void writeUtf8(Utf8JsonWriter writer, CodegenProbeValue value) { - checkCapability(writer.typeResolver().getTypeInfo(expectedType, expectedType).utf8Writer()); + checkCapability( + writer.typeResolver().getTypeInfo(expectedType, expectedType).utf8Writer(), + expectGeneratedWriter); writer.writeString(value == null ? null : value.value); } @Override public CodegenProbeValue readLatin1(Latin1JsonReader reader) { - checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).latin1Reader()); + checkCapability( + reader.typeResolver().getTypeInfo(expectedType, expectedType).latin1Reader(), + expectGeneratedReader); return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); } @Override public CodegenProbeValue readUtf16(Utf16JsonReader reader) { - checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).utf16Reader()); + checkCapability( + reader.typeResolver().getTypeInfo(expectedType, expectedType).utf16Reader(), + expectGeneratedReader); return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); } @Override public CodegenProbeValue readUtf8(Utf8JsonReader reader) { - checkCapability(reader.typeResolver().getTypeInfo(expectedType, expectedType).utf8Reader()); + checkCapability( + reader.typeResolver().getTypeInfo(expectedType, expectedType).utf8Reader(), + expectGeneratedReader); return reader.tryReadNullToken() ? null : new CodegenProbeValue(reader.readString()); } - private static void checkCapability(Object capability) { + private static void checkCapability(Object capability, boolean expectGenerated) { boolean generated = !(capability instanceof ObjectCodec); Preconditions.checkArgument(generated == expectGenerated); } 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 ec0f4bd5ec..d49898f505 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,6 @@ 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; @@ -43,7 +42,6 @@ public ForyJson aRestrictedConfiguration() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) - .withClassLoader(new CodegenRejectingClassLoader()) .withTypeChecker((className, context) -> false) .build(); } 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 1a5a98ecbf..e20b5445a3 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 @@ -81,9 +81,9 @@ public ForyJsonBuilder writeNullFields(boolean writeNullFields) { /** * Enables generated object codecs for supported classes. Enabled by default and automatically - * 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. + * disabled on Android. A GraalVM Native Image includes generated codecs for the default + * configuration and for each reachable {@link org.apache.fory.json.annotation.ForyJsonProvider} + * configuration. An exact generated-codec miss uses the interpreted codec. */ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { this.codegenEnabled = codegenEnabled; @@ -210,13 +210,23 @@ public ForyJsonBuilder withBufferSizeLimitBytes(int bufferSizeLimitBytes) { *

The same codec instance may be called concurrently by pooled JSON states and must therefore * be thread-safe. Building snapshots the registration map, although the registered codec objects * themselves are intentionally shared. + * + *

Exact registration is rejected for primitive and boxed scalar types, {@link String}, {@link + * CharSequence}, {@link Number}, standard big-number, UUID, and {@code java.time} scalar types, + * plus {@code byte[]}, {@code String[]}, and {@code long[]}. Those types are owned by dedicated + * reader/writer operations. Occurrence annotations such as {@code JsonCodec} remain supported. */ public ForyJsonBuilder registerCodec(Class type, JsonValueCodec codec) { codecRegistry.register(type, codec); return this; } - /** Registers a resolver-owned complete codec factory for one exact class. */ + /** + * Registers a resolver-owned complete codec factory for one exact class. + * + *

The exact type restrictions documented by {@link #registerCodec(Class, JsonValueCodec)} + * apply to this registration as well. + */ public ForyJsonBuilder registerCodec(Class type, JsonCodecFactory factory) { codecRegistry.registerFactory(type, factory); return this; @@ -289,7 +299,6 @@ public ForyJson build() { installed.codecs, installed.mixins, installed.factories, - installed.moduleIdentities, installed.factoryIdentities, typeChecker)); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java index f4af72022d..c74e03c651 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java @@ -42,7 +42,12 @@ default String factoryKey() { return getClass().getName(); } - /** Returns runtime classes represented by an exact closed root codec. */ + /** + * Returns runtime classes represented by an exact closed root codec. + * + *

The list must not contain a dedicated reader/writer scalar type or {@code byte[]}, {@code + * String[]}, or {@code long[]}. + */ @Internal default List> handledRuntimeClasses() { return Collections.emptyList(); 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 ca50102598..a398f6b6f5 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 @@ -19,25 +19,19 @@ package org.apache.fory.json; -import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.apache.fory.annotation.Internal; -import org.apache.fory.json.codegen.JsonCodegenKey; import org.apache.fory.json.resolver.CodecRegistry; /** * Build configuration used to create all pooled states of one {@link ForyJson} instance. * *

Scalar settings and the codec registry are snapshotted at construction; the JSON runtime never - * observes later builder mutation. {@link JsonCodegenKey} identifies only settings that can change - * generated source; runtime-only settings such as depth, graph memory, 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. + * observes later builder mutation. */ public final class JsonConfig { private static final int MAX_CACHED_FIELD_NAMES = 1 << 29; @@ -59,7 +53,6 @@ public final class JsonConfig { private final String[] codecFactoryIdentities; private final JsonTypeChecker typeChecker; private final JsonTypeCheckContext typeCheckContext; - private final JsonCodegenKey codegenKey; JsonConfig( boolean writeNullFields, @@ -76,7 +69,6 @@ public final class JsonConfig { CodecRegistry codecRegistry, Map, Class> mixins, JsonCodecFactory[] codecFactories, - List moduleIdentities, List factoryIdentities, JsonTypeChecker typeChecker) { this.writeNullFields = writeNullFields; @@ -99,17 +91,6 @@ public final class JsonConfig { this.codecFactoryIdentities = factoryIdentities.toArray(new String[0]); this.typeChecker = typeChecker; typeCheckContext = new JsonTypeCheckContext(); - String codecRegistryKey = - this.codecRegistry.codegenKey() - + identityKey("module", moduleIdentities) - + identityKey("factory", factoryIdentities); - codegenKey = - new JsonCodegenKey( - writeNullFields, - propertyDiscoveryEnabled, - propertyNamingStrategy, - codecRegistryKey, - mixinKey(this.mixins)); } public boolean writeNullFields() { @@ -208,44 +189,4 @@ private static Map, Class> immutableMixins(Map, Class> r } return Collections.unmodifiableMap(new IdentityHashMap<>(registrations)); } - - private static String mixinKey(Map, Class> mixins) { - if (mixins.isEmpty()) { - return ""; - } - List, Class>> entries = new ArrayList<>(mixins.entrySet()); - entries.sort( - Comparator.comparing((Map.Entry, Class> entry) -> entry.getKey().getName()) - .thenComparing(entry -> entry.getValue().getName())); - StringBuilder builder = new StringBuilder(entries.size() * 64); - for (Map.Entry, Class> entry : entries) { - appendIdentity(builder, entry.getKey().getName()); - appendIdentity(builder, entry.getValue().getName()); - } - return builder.toString(); - } - - private static String identityKey(String kind, List identities) { - if (identities.isEmpty()) { - return ""; - } - ArrayList sorted = new ArrayList<>(identities); - sorted.sort(String::compareTo); - StringBuilder builder = new StringBuilder(sorted.size() * 48); - for (String identity : sorted) { - appendIdentity(builder, kind); - appendIdentity(builder, identity); - } - return builder.toString(); - } - - private static void appendIdentity(StringBuilder builder, String value) { - builder.append(value.length()).append(':').append(value); - } - - /** 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/ModuleContext.java b/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java index cbfd5cb709..4c9c63fccb 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java @@ -23,10 +23,16 @@ /** Build-time registration surface exposed to a {@link ForyJsonModule}. */ public interface ModuleContext { - /** Registers a complete codec for one exact class. */ + /** + * Registers a complete codec for one eligible exact class. Dedicated reader/writer scalar types + * and {@code byte[]}, {@code String[]}, and {@code long[]} cannot be registered exactly. + */ void registerCodec(Class type, JsonValueCodec codec); - /** Registers a resolver-owned codec factory for one exact class. */ + /** + * Registers a resolver-owned codec factory for one eligible exact class. The same protected + * built-in types as {@link #registerCodec(Class, JsonValueCodec)} are rejected. + */ void registerCodec(Class type, JsonCodecFactory factory); /** Registers the target Mixin declared by {@code mixinType}. */ diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ModuleInstaller.java b/java/fory-json/src/main/java/org/apache/fory/json/ModuleInstaller.java index 829c0bcc08..d36a071c34 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ModuleInstaller.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ModuleInstaller.java @@ -65,13 +65,10 @@ static InstalledModules install( mergedCodecs.putDefaults(installer.codecs); Map, Class> mergedMixins = new IdentityHashMap<>(installer.mixins); mergedMixins.putAll(applicationMixins); - ArrayList identities = new ArrayList<>(installer.moduleIdentities); - identities.sort(String::compareTo); return new InstalledModules( mergedCodecs, mergedMixins, installer.factories.toArray(new JsonCodecFactory[0]), - Collections.unmodifiableList(identities), Collections.unmodifiableList(new ArrayList<>(installer.factoryIdentities))); } finally { installer.frozen = true; @@ -158,19 +155,16 @@ static final class InstalledModules { final CodecRegistry codecs; final Map, Class> mixins; final JsonCodecFactory[] factories; - final List moduleIdentities; final List factoryIdentities; private InstalledModules( CodecRegistry codecs, Map, Class> mixins, JsonCodecFactory[] factories, - List moduleIdentities, List factoryIdentities) { this.codecs = codecs; this.mixins = mixins; this.factories = factories; - this.moduleIdentities = moduleIdentities; this.factoryIdentities = factoryIdentities; } } 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 9d6536da48..c5d48d7477 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 @@ -32,11 +32,10 @@ *

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 for ordinary Java models and complete value codecs; language-module object - * models require a returned generated configuration. The provider package does not need to be - * exported or opened to Fory. + * methods and public interface default methods. Fory JSON always generates reachable models for the + * default configuration, then adds generated codecs for every returned configuration. Equal exact + * model-role keys reuse one generated class. At runtime, an exact miss uses the interpreted codec. + * The provider package does not need to be exported or opened to Fory. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java new file mode 100644 index 0000000000..1ef94b2587 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java @@ -0,0 +1,336 @@ +/* + * 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.codegen; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Objects; +import org.apache.fory.annotation.Internal; + +/** Exact, source-independent identity of one generated JSON capability class. */ +@Internal +public final class GeneratedCodecKey { + private static final int CLASS_VERSION = 1; + + /** Generated capability roles whose classes have independent source shapes. */ + public enum Role { + STRING_WRITER("StringWriter"), + UTF8_WRITER("Utf8Writer"), + LATIN1_READER("Latin1Reader"), + UTF16_READER("Utf16Reader"), + UTF8_READER("Utf8Reader"), + UTF8_COLLECTION_WRITER("Utf8CollectionWriter"), + UTF8_COLLECTION_READER("Utf8CollectionReader"); + + private final String classSuffix; + + Role(String classSuffix) { + this.classSuffix = classSuffix; + } + + public String classSuffix() { + return classSuffix; + } + } + + /** + * Stable JVM identity for one reflected member without retaining a reflection-object identity. + */ + public static final class MemberDescriptor { + private final Class declaringClass; + private final byte kind; + private final String name; + private final String descriptor; + private final int hash; + + private MemberDescriptor(Class declaringClass, byte kind, String name, String descriptor) { + this.declaringClass = declaringClass; + this.kind = kind; + this.name = name; + this.descriptor = descriptor; + hash = + (((System.identityHashCode(declaringClass) * 31 + kind) * 31 + name.hashCode()) * 31) + + descriptor.hashCode(); + } + + public static MemberDescriptor of(Member member) { + if (member == null) { + return null; + } + if (member instanceof Field) { + Field field = (Field) member; + return new MemberDescriptor( + field.getDeclaringClass(), (byte) 1, field.getName(), descriptor(field.getType())); + } + Executable executable = (Executable) member; + StringBuilder descriptor = new StringBuilder("("); + for (Class parameter : executable.getParameterTypes()) { + descriptor.append(descriptor(parameter)); + } + descriptor.append(')'); + byte kind; + String name; + if (executable instanceof Constructor) { + kind = 2; + name = ""; + descriptor.append('V'); + } else { + kind = 3; + name = executable.getName(); + descriptor.append(descriptor(((Method) executable).getReturnType())); + } + return new MemberDescriptor( + executable.getDeclaringClass(), kind, name, descriptor.toString()); + } + + public Class declaringClass() { + return declaringClass; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MemberDescriptor)) { + return false; + } + MemberDescriptor that = (MemberDescriptor) other; + return declaringClass == that.declaringClass + && kind == that.kind + && name.equals(that.name) + && descriptor.equals(that.descriptor); + } + + @Override + public int hashCode() { + return hash; + } + + private static String descriptor(Class type) { + if (type.isPrimitive()) { + 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"; + } + return "D"; + } + if (type.isArray()) { + return type.getName().replace('.', '/'); + } + return "L" + type.getName().replace('.', '/') + ";"; + } + } + + private final Class targetClass; + private final Role role; + private final Object[] projection; + private final Class[] referencedClasses; + private final Class anchorClass; + private final int hash; + + private GeneratedCodecKey( + Class targetClass, + Role role, + Object[] projection, + Class[] referencedClasses, + Class preferredAnchor) { + this.targetClass = Objects.requireNonNull(targetClass); + this.role = Objects.requireNonNull(role); + this.projection = projection.clone(); + this.referencedClasses = uniqueClasses(targetClass, referencedClasses, projection); + anchorClass = anchor(preferredAnchor, this.referencedClasses); + hash = + ((System.identityHashCode(targetClass) * 31 + role.hashCode()) * 31 + CLASS_VERSION) * 31 + + valuesHash(this.projection); + } + + public static GeneratedCodecKey object( + Class targetClass, Role role, Object[] projection, Class[] referencedClasses) { + if (role == Role.UTF8_COLLECTION_WRITER || role == Role.UTF8_COLLECTION_READER) { + throw new IllegalArgumentException("Collection role requires a collection key"); + } + return new GeneratedCodecKey(targetClass, role, projection, referencedClasses, targetClass); + } + + public static GeneratedCodecKey collection( + Class collectionClass, Class elementClass, Role role, boolean stringElements) { + if (role != Role.UTF8_COLLECTION_WRITER && role != Role.UTF8_COLLECTION_READER) { + throw new IllegalArgumentException("Object role requires an object key"); + } + return new GeneratedCodecKey( + collectionClass, + role, + new Object[] {collectionClass, elementClass, stringElements}, + new Class[] {elementClass, collectionClass}, + elementClass); + } + + public Class targetClass() { + return targetClass; + } + + public Role role() { + return role; + } + + /** Returns the first application-owned class whose lifecycle may retain this key. */ + public Class anchorClass() { + return anchorClass; + } + + /** Returns the identity-deduplicated classes required by compilation in canonical order. */ + public Class[] referencedClasses() { + return referencedClasses.clone(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof GeneratedCodecKey)) { + return false; + } + GeneratedCodecKey that = (GeneratedCodecKey) other; + return targetClass == that.targetClass + && role == that.role + && valuesEqual(projection, that.projection); + } + + @Override + public int hashCode() { + return hash; + } + + private static Class[] uniqueClasses( + Class target, Class[] explicit, Object[] projection) { + ArrayList> classes = new ArrayList<>(); + IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); + addClass(target, classes, seen); + for (Class type : explicit) { + addClass(type, classes, seen); + } + collectClasses(projection, classes, seen); + return classes.toArray(new Class[0]); + } + + private static void collectClasses( + Object value, ArrayList> classes, IdentityHashMap, Boolean> seen) { + if (value instanceof Class) { + addClass((Class) value, classes, seen); + } else if (value instanceof MemberDescriptor) { + addClass(((MemberDescriptor) value).declaringClass, classes, seen); + } else if (value instanceof Object[]) { + for (Object item : (Object[]) value) { + collectClasses(item, classes, seen); + } + } + } + + private static void addClass( + Class type, ArrayList> classes, IdentityHashMap, Boolean> seen) { + if (type != null && seen.put(type, Boolean.TRUE) == null) { + classes.add(type); + } + } + + private static Class anchor(Class preferred, Class[] classes) { + if (preferred.getClassLoader() != null) { + return preferred; + } + for (Class type : classes) { + if (type.getClassLoader() != null) { + return type; + } + } + return preferred; + } + + private static boolean valuesEqual(Object[] left, Object[] right) { + if (left.length != right.length) { + return false; + } + for (int i = 0; i < left.length; i++) { + Object a = left[i]; + Object b = right[i]; + if (a instanceof Class || b instanceof Class) { + if (a != b) { + return false; + } + } else if (a instanceof Object[] && b instanceof Object[]) { + if (!valuesEqual((Object[]) a, (Object[]) b)) { + return false; + } + } else if (a instanceof byte[] && b instanceof byte[]) { + if (!Arrays.equals((byte[]) a, (byte[]) b)) { + return false; + } + } else if (!Objects.equals(a, b)) { + return false; + } + } + return true; + } + + private static int valuesHash(Object[] values) { + int hash = 1; + for (Object value : values) { + int valueHash; + if (value instanceof Class) { + valueHash = System.identityHashCode(value); + } else if (value instanceof Object[]) { + valueHash = valuesHash((Object[]) value); + } else if (value instanceof byte[]) { + valueHash = Arrays.hashCode((byte[]) value); + } else { + valueHash = Objects.hashCode(value); + } + hash = hash * 31 + valueHash; + } + return hash; + } +} 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 e85ab2f16b..41ce70eb7e 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 @@ -23,21 +23,22 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Collection; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import org.apache.fory.annotation.Internal; +import org.apache.fory.builder.Generated; import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.CompileUnit; import org.apache.fory.codegen.JaninoUtils; import org.apache.fory.codegen.JaninoUtils.DirectInvocation; +import org.apache.fory.collection.ClassValueCache; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; @@ -56,20 +57,21 @@ import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.meta.JsonFieldInfo; -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; import org.apache.fory.reflect.TypeRef; +import org.apache.fory.util.ClassLoaderUtils; /** * Generates concrete object and exact-collection capability classes. * - *

One instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. The - * registry owns every generated-class future and single-flight decision. A resolver is passed only - * to the active source-generation call for short canonical metadata lookups; neither owner retains - * it. + *

One frontend instance belongs to one {@link org.apache.fory.json.resolver.JsonSharedRegistry}. + * Generated classes are shared by exact {@link GeneratedCodecKey} through a class-lifecycle cache. + * On an ordinary JVM, {@link CodeGenerator} owns compilation and definition single-flight. Hosted + * compilation defines completed classes beside their source owners before Native Image freezes its + * exact-key registry. * *

This class owns class generation only. Resolver-local generated instances, final direct-child * capture, canonical cycle slots, and {@link JsonTypeInfo} slot installation belong to {@link @@ -85,10 +87,14 @@ public final class JsonCodegen { // the already-independent callee body back to this planner's budget. private static final int HOT_INLINE_LIMIT = 325; private static final int GENERATED_NAME_PREFIX_CODE_POINTS = 32; - private final String codegenIdentity; + private static final AtomicLong GENERATED_CLASS_SUFFIX = new AtomicLong(); + private static volatile ClassValueCache generatedClasses = + newGeneratedClassCache(); + private final CodeGenerator codeGenerator; private final ClassLoader jsonLoader; private final boolean hostedCodegen; + private final String generatedClassName; static String generatedCodecType(CodegenContext ctx, Class codecType) { // Janino-generated serializers use erased types, matching Fory core code generation. Runtime @@ -101,11 +107,19 @@ static String generatedCodecArrayType(CodegenContext ctx, Class arrayType) { return ctx.type(arrayType); } - public JsonCodegen(JsonCodegenKey codegenKey, ClassLoader jsonLoader, boolean hostedCodegen) { - codegenIdentity = codegenKey.identity(); + public JsonCodegen(boolean hostedCodegen) { + this(null, null, hostedCodegen, null); + } + + private JsonCodegen( + CodeGenerator codeGenerator, + ClassLoader jsonLoader, + boolean hostedCodegen, + String generatedClassName) { this.jsonLoader = jsonLoader; this.hostedCodegen = hostedCodegen; - codeGenerator = new CodeGenerator(jsonLoader); + this.codeGenerator = codeGenerator; + this.generatedClassName = generatedClassName; } /** @@ -122,79 +136,197 @@ public JsonCodegen(JsonCodegenKey codegenKey, ClassLoader jsonLoader, boolean ho */ @Internal public Class compileStringWriter( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { - if (!canCompileWriter(codec)) { - return null; - } - return buildStringWriter(declaredType, codec, resolver); + GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { + return compileObject(key, codec, compiler -> compiler.buildStringWriter(codec, resolver)); } @Internal public Class compileUtf8Writer( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { - if (!canCompileWriter(codec)) { - return null; - } - return buildUtf8Writer(declaredType, codec, resolver); + GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { + return compileObject(key, codec, compiler -> compiler.buildUtf8Writer(codec, resolver)); } @Internal public Class compileLatin1Reader( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { - if (!canCompileReader(codec)) { - return null; - } - return buildLatin1Reader(declaredType, codec, resolver); + GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { + return compileObject(key, codec, compiler -> compiler.buildLatin1Reader(codec, resolver)); } @Internal public Class compileUtf16Reader( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { - if (!canCompileReader(codec)) { - return null; - } - return buildUtf16Reader(declaredType, codec, resolver); + GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { + return compileObject(key, codec, compiler -> compiler.buildUtf16Reader(codec, resolver)); } @Internal public Class compileUtf8Reader( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { - if (!canCompileReader(codec)) { - return null; - } - return buildUtf8Reader(declaredType, codec, resolver); + GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { + return compileObject(key, codec, compiler -> compiler.buildUtf8Reader(codec, resolver)); } @Internal - public Class compileUtf8CollectionWriter(TypeRef declaredType, CollectionCodec owner) { + public Class compileUtf8CollectionWriter( + GeneratedCodecKey key, TypeRef declaredType, CollectionCodec owner) { + Type type = declaredType.getType(); + Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + return compile( + key, + CodeGenerator.getPackage(elementType), + compiler -> compiler.buildUtf8CollectionWriter(declaredType, owner)); + } + + private Class buildUtf8CollectionWriter(TypeRef declaredType, CollectionCodec owner) { Type type = declaredType.getType(); - Class rawType = CodecUtils.rawType(type, Collection.class); Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); String generatedPackage = CodeGenerator.getPackage(elementType); boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec; - String className = - className(declaredType, simpleClassName(rawType) + "Utf8CollectionWriter", stringElements); + String className = className(); String code = new Utf8CollectionWriterCodegen().genCode(generatedPackage, className, stringElements); - return compileCodecClass(generatedPackage, className, code); + return compileCollectionCodecClass(elementType, generatedPackage, className, code); } @Internal - public Class compileUtf8CollectionReader(TypeRef declaredType, CollectionCodec owner) { + public Class compileUtf8CollectionReader( + GeneratedCodecKey key, TypeRef declaredType, CollectionCodec owner) { + Type type = declaredType.getType(); + Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + return compile( + key, + CodeGenerator.getPackage(elementType), + compiler -> compiler.buildUtf8CollectionReader(declaredType, owner)); + } + + private Class buildUtf8CollectionReader(TypeRef declaredType, CollectionCodec owner) { if (!owner.createsArrayList()) { throw new IllegalArgumentException( "Generated UTF-8 collection requires an ArrayList binding"); } Type type = declaredType.getType(); - Class rawType = CodecUtils.rawType(type, Collection.class); Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); String generatedPackage = CodeGenerator.getPackage(elementType); boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec; - String className = - className(declaredType, simpleClassName(rawType) + "Utf8CollectionReader", stringElements); + String className = className(); String code = new Utf8CollectionReaderCodegen().genCode(generatedPackage, className, stringElements); - return compileCodecClass(generatedPackage, className, code); + return compileCollectionCodecClass(elementType, generatedPackage, className, code); + } + + private Class compileObject( + GeneratedCodecKey key, ObjectCodec owner, CompilerOperation operation) { + return compile( + key, + CodeGenerator.getPackage(owner.type()), + compiler -> { + boolean writer = + key.role() == GeneratedCodecKey.Role.STRING_WRITER + || key.role() == GeneratedCodecKey.Role.UTF8_WRITER; + if (writer ? !compiler.canCompileWriter(owner) : !compiler.canCompileReader(owner)) { + return null; + } + return operation.compile(compiler); + }); + } + + private Class compile( + GeneratedCodecKey key, String generatedPackage, CompilerOperation operation) { + PerClassGeneratedCodecCache perClass = + generatedClasses.get(key.anchorClass(), PerClassGeneratedCodecCache::new); + CacheEntry entry = + perClass.entries.computeIfAbsent( + key, + ignored -> { + String className = + generatedNamePrefix(key.targetClass()) + + key.role().classSuffix() + + "ForyJsonCodec_" + + GENERATED_CLASS_SUFFIX.incrementAndGet(); + return new CacheEntry(qualifiedClassName(generatedPackage, className), className); + }); + Class completed = entry.generatedClass; + if (completed != null) { + return completed; + } + JsonCodegen compiler = compiler(key, entry.className); + Class generatedClass = operation.compile(compiler); + if (generatedClass != null) { + entry.publish(generatedClass); + } + return generatedClass; + } + + private JsonCodegen compiler(GeneratedCodecKey key, String className) { + ClassLoader[] loaders = canonicalLoaders(key); + if (hostedCodegen) { + // Use the canonical loader tuple only for source compilation and visibility decisions. The + // generated class is defined beside its source owner below, so this hosted-only composed + // loader cannot become reachable from the frozen Native Image registry. + ClassLoader loader = + loaders.length == 1 ? loaders[0] : new ClassLoaderUtils.ComposedClassLoader(loaders); + return new JsonCodegen(null, loader, true, className); + } + CodeGenerator generator = + loaders.length == 1 + ? CodeGenerator.getSharedCodeGenerator(loaders[0]) + : CodeGenerator.getSharedCodeGenerator(loaders); + return new JsonCodegen(generator, generator.getClassLoader(), false, className); + } + + private ClassLoader[] canonicalLoaders(GeneratedCodecKey key) { + ArrayList loaders = new ArrayList<>(); + IdentityHashMap seen = new IdentityHashMap<>(); + for (Class type : key.referencedClasses()) { + ClassLoader loader = type.getClassLoader(); + if (loader != null && seen.put(loader, Boolean.TRUE) == null) { + loaders.add(loader); + } + } + ClassLoader foryLoader = JsonCodegen.class.getClassLoader(); + if (foryLoader != null && seen.put(foryLoader, Boolean.TRUE) == null) { + loaders.add(foryLoader); + } + if (loaders.isEmpty()) { + loaders.add(CodeGenerator.class.getClassLoader()); + } + return loaders.toArray(new ClassLoader[0]); + } + + /** Releases the hosted strong cache after Native Image analysis freezes the runtime registry. */ + @Internal + public static void resetGeneratedClassCache() { + generatedClasses = newGeneratedClassCache(); + } + + private static ClassValueCache newGeneratedClassCache() { + return ClassValueCache.newClassKeySoftCache(32); + } + + private interface CompilerOperation { + Class compile(JsonCodegen compiler); + } + + private static final class PerClassGeneratedCodecCache { + private final ConcurrentHashMap entries = + new ConcurrentHashMap<>(); + } + + private static final class CacheEntry { + private final String binaryName; + private final String className; + private volatile Class generatedClass; + + private CacheEntry(String binaryName, String className) { + this.binaryName = binaryName; + this.className = className; + } + + private void publish(Class generatedClass) { + Class completed = this.generatedClass; + if (completed != null && completed != generatedClass) { + throw new IllegalStateException("Conflicting generated JSON class " + binaryName); + } + this.generatedClass = generatedClass; + } } private DirectInvocation[] writerInvocations(ObjectCodec codec) { @@ -349,11 +481,10 @@ private static void addInvocation( } } - private Class buildStringWriter( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { + private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); - String className = className(declaredType, "StringWriter"); + String className = className(); DirectInvocation[] invocations = writerInvocations(codec); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -392,11 +523,10 @@ private Class buildStringWriter( invocations); } - private Class buildUtf8Writer( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { + private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); - String className = className(declaredType, "Utf8Writer"); + String className = className(); DirectInvocation[] invocations = writerInvocations(codec); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -462,11 +592,10 @@ private Class buildUtf8Writer( invocations); } - private Class buildLatin1Reader( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { + private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); - String className = className(declaredType, "Latin1Reader"); + String className = className(); DirectInvocation[] invocations = readerInvocations(codec); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -499,11 +628,10 @@ private Class buildLatin1Reader( invocations); } - private Class buildUtf16Reader( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { + private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); - String className = className(declaredType, "Utf16Reader"); + String className = className(); DirectInvocation[] invocations = readerInvocations(codec); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -536,11 +664,10 @@ private Class buildUtf16Reader( invocations); } - private Class buildUtf8Reader( - TypeRef declaredType, ObjectCodec codec, JsonTypeResolver resolver) { + private Class buildUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver) { Class type = codec.type(); String generatedPackage = CodeGenerator.getPackage(type); - String className = className(declaredType, "Utf8Reader"); + String className = className(); DirectInvocation[] invocations = readerInvocations(codec); JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -837,13 +964,13 @@ private Class compileObjectCodecClass( String className, String code, DirectInvocation[] invocations) { - if (!hostedCodegen || _JDKAccess.isExported(ownerType)) { + if (!hostedCodegen) { return compileCodecClass(generatedPackage, className, code, invocations); } 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. + // Hosted classes live beside their source owner. Concealed models need that placement for + // member access; exported models use it as well so the transient canonical compilation + // loader cannot become reachable through the generated Class mirror in the image heap. CompileUnit unit = new CompileUnit(generatedPackage, className, code); return compileHostedClass(ownerType, unit, invocations); } catch (Throwable e) { @@ -866,6 +993,26 @@ private Class compileCodecClass(String generatedPackage, String className, St return compileCodecClass(generatedPackage, className, code, new DirectInvocation[0]); } + private Class compileCollectionCodecClass( + Class elementType, String generatedPackage, String className, String code) { + if (!hostedCodegen) { + return compileCodecClass(generatedPackage, className, code); + } + Class definitionOwner = elementType; + while (definitionOwner.isArray()) { + definitionOwner = definitionOwner.getComponentType(); + } + if (definitionOwner.isPrimitive() + || definitionOwner.getClassLoader() == null + || !CodeGenerator.getPackage(definitionOwner).equals(generatedPackage)) { + definitionOwner = Generated.class; + } + return compileHostedClass( + definitionOwner, + new CompileUnit(generatedPackage, className, code), + new DirectInvocation[0]); + } + private Class compileHostedClass( Class ownerType, CompileUnit unit, DirectInvocation[] invocations) { Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); @@ -919,6 +1066,12 @@ public boolean canCompileWriter(ObjectCodec codec) { return any == null || canCompileAnyWrite(any); } + /** Checks source visibility through the same canonical loader tuple used by compilation. */ + @Internal + public boolean canCompileWriter(GeneratedCodecKey key, ObjectCodec codec) { + return compiler(key, "ForyJsonCodecProbe").canCompileWriter(codec); + } + private boolean canCompileUnwrappedWrite( ObjectCodec owner, JsonUnwrappedInfo.WriteEntry[] entries) { for (JsonUnwrappedInfo.WriteEntry entry : entries) { @@ -969,6 +1122,12 @@ public boolean canCompileReader(ObjectCodec codec) { return any == null || canCompileAnyRead(any, codec.creatorInfo() != null); } + /** Checks source visibility through the same canonical loader tuple used by compilation. */ + @Internal + public boolean canCompileReader(GeneratedCodecKey key, ObjectCodec codec) { + return compiler(key, "ForyJsonCodecProbe").canCompileReader(codec); + } + private boolean canCompileUnwrappedRead(ObjectCodec owner, JsonUnwrappedInfo unwrapped) { for (JsonFieldInfo field : owner.readFields()) { if (!canCompileRead(field)) { @@ -1125,92 +1284,9 @@ Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { return Utf8ReaderCodec.class; } - @Internal - public static Class readNestedType(JsonFieldInfo property, JsonTypeResolver resolver) { - if (!property.readsUnboxedValue() - && property.readKind() == JsonFieldKind.OBJECT - && property.readRawType() != Object.class - && resolver.canonicalObjectCodec(property.readTypeInfo()) != null) { - return property.readRawType(); - } - return null; - } - - @Internal - public static boolean usesWriteCodec(JsonFieldInfo property) { - if (property.writesUnboxedValue() && property.writeKind() == JsonFieldKind.ENUM) { - return true; - } - switch (property.writeKind()) { - case ARRAY: - case MAP: - case OBJECT: - return true; - case COLLECTION: - return !writesStringCollectionDirectly(property); - default: - return false; - } - } - - @Internal - public static boolean usesUtf8WriteCodec(JsonFieldInfo property, JsonTypeResolver resolver) { - return usesWriteCodec(property) - || property.writeKind() == JsonFieldKind.COLLECTION - && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) != null; - } - - static boolean writesStringCollectionDirectly(JsonFieldInfo property) { - return property.writeElementRawType() == String.class - && property.writeTypeInfo().stringWriter().getClass() - == CollectionCodec.StringCollectionCodec.class; - } - - @Internal - public static boolean usesReadCodec(JsonFieldInfo property, JsonTypeResolver resolver) { - if (property.readsUnboxedValue()) { - if (property.readDirectUnboxedValueCodec() != null) { - return false; - } - Class rawType = property.readTypeInfo().rawType(); - JsonFieldKind kind = property.readKind(); - if (rawType == String.class && kind == JsonFieldKind.STRING) { - return false; - } - if (rawType.isPrimitive()) { - return !((rawType == boolean.class && kind == JsonFieldKind.BOOLEAN) - || (rawType == byte.class && kind == JsonFieldKind.BYTE) - || (rawType == short.class && kind == JsonFieldKind.SHORT) - || (rawType == int.class && kind == JsonFieldKind.INT) - || (rawType == long.class && kind == JsonFieldKind.LONG) - || (rawType == float.class && kind == JsonFieldKind.FLOAT) - || (rawType == double.class && kind == JsonFieldKind.DOUBLE) - || (rawType == char.class && kind == JsonFieldKind.CHAR)); - } - return true; - } - switch (property.readKind()) { - case ENUM: - case ARRAY: - case COLLECTION: - case MAP: - return true; - case OBJECT: - return !usesReadObjectCodec(property, resolver); - default: - return false; - } - } - - static boolean usesReadObjectCodec(JsonFieldInfo property, JsonTypeResolver resolver) { - return property.readKind() == JsonFieldKind.OBJECT - && property.readRawType() != Object.class - && resolver.canonicalObjectCodec(property.readTypeInfo()) != null; - } - static boolean storesReadObjectCodec( Class type, JsonFieldInfo property, JsonTypeResolver resolver) { - Class nestedType = readNestedType(property, resolver); + Class nestedType = resolver.readNestedType(property); return nestedType != null && nestedType != type; } @@ -1227,7 +1303,7 @@ public static boolean storesSelfReader(ObjectCodec owner, JsonTypeResolver re JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); if (unwrapped != null) { for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { - if (route.field() != null && readNestedType(route.field(), resolver) == owner.type()) { + if (route.field() != null && resolver.readNestedType(route.field()) == owner.type()) { return true; } } @@ -1248,7 +1324,7 @@ static boolean storesSelfReader( return false; } for (JsonFieldInfo property : properties) { - if (readNestedType(property, resolver) == type) { + if (resolver.readNestedType(property) == type) { return true; } } @@ -1344,47 +1420,11 @@ private static boolean isPublicSourceType(Class type) { return true; } - private String className(TypeRef type, String role) { - String identity = generatedIdentity(type, role); - return generatedNamePrefix(type.getRawType()) + role + "ForyJsonCodec_" + digest(identity); - } - - private String className(TypeRef type, String role, boolean stringElements) { - StringBuilder identity = new StringBuilder(generatedIdentity(type, role)); - appendIdentity(identity, stringElements ? "1" : "0"); - return generatedNamePrefix(type.getRawType()) - + role - + "ForyJsonCodec_" - + digest(identity.toString()); - } - - private String generatedIdentity(TypeRef type, String role) { - StringBuilder identity = new StringBuilder(codegenIdentity.length() + role.length() + 96); - appendIdentity(identity, codegenIdentity); - appendIdentity(identity, role); - appendIdentity(identity, type.getTypeKey()); - return identity.toString(); - } - - private static void appendIdentity(StringBuilder builder, String value) { - builder.append(value.length()).append(':').append(value); - } - - private static String digest(String value) { - try { - byte[] bytes = - MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); - char[] hex = new char[bytes.length * 2]; - char[] digits = "0123456789abcdef".toCharArray(); - for (int i = 0; i < bytes.length; i++) { - int current = bytes[i] & 0xff; - hex[i * 2] = digits[current >>> 4]; - hex[i * 2 + 1] = digits[current & 0x0f]; - } - return new String(hex); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("SHA-256 is unavailable", e); + private String className() { + if (generatedClassName == null) { + throw new IllegalStateException("Generated JSON class name has not been assigned"); } + return generatedClassName; } private static String simpleClassName(Class type) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java deleted file mode 100644 index c5971c044e..0000000000 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegenKey.java +++ /dev/null @@ -1,86 +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.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 -public final class JsonCodegenKey { - private final boolean writeNullFields; - private final boolean propertyDiscoveryEnabled; - private final String propertyNamingStrategy; - private final String codecRegistryKey; - private final String mixinKey; - - public JsonCodegenKey( - boolean writeNullFields, - boolean propertyDiscoveryEnabled, - PropertyNamingStrategy propertyNamingStrategy, - String codecRegistryKey, - String mixinKey) { - this.writeNullFields = writeNullFields; - this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; - this.propertyNamingStrategy = propertyNamingStrategy.name(); - 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.equals(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(); - } - - /** Returns the deterministic generated-source identity used in generated class names. */ - public String identity() { - StringBuilder builder = new StringBuilder(); - builder.append(writeNullFields ? '1' : '0'); - builder.append(propertyDiscoveryEnabled ? '1' : '0'); - append(builder, propertyNamingStrategy); - append(builder, codecRegistryKey); - append(builder, mixinKey); - return builder.toString(); - } - - private static void append(StringBuilder builder, String value) { - builder.append(value.length()).append(':').append(value); - } -} diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 1f150c290a..980c3c9c4e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -141,7 +141,7 @@ abstract Expression readEnumField( abstract Reference readerRef(); final Class readNestedType(JsonFieldInfo property) { - return JsonCodegen.readNestedType(property, resolver); + return resolver.readNestedType(property); } String genReaderCode( @@ -173,7 +173,7 @@ String genReaderCode( if (usesReadInfo(properties[i])) { ctx.addField(JsonFieldInfo.class, "rp" + i); } - if (JsonCodegen.usesReadCodec(properties[i], resolver)) { + if (resolver.usesReadCodec(properties[i])) { addValueReaderField(ctx, properties[i], "r" + i); } if (storesReadObjectCodec(type, properties[i])) { @@ -373,7 +373,7 @@ String genUnwrappedReaderCode( if (usesReadInfo(field)) { ctx.addField(JsonFieldInfo.class, "rp" + id); } - if (JsonCodegen.usesReadCodec(field, resolver)) { + if (resolver.usesReadCodec(field)) { addValueReaderField(ctx, field, "r" + id); } if (storesReadObjectCodec(type, field)) { @@ -525,7 +525,7 @@ private void addReaderFields(CodegenContext ctx, Class type, JsonFieldInfo[] if (usesReadInfo(properties[i])) { ctx.addField(JsonFieldInfo.class, "rp" + i); } - if (JsonCodegen.usesReadCodec(properties[i], resolver)) { + if (resolver.usesReadCodec(properties[i])) { addValueReaderField(ctx, properties[i], "r" + i); } if (storesReadObjectCodec(type, properties[i])) { @@ -2041,7 +2041,7 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr hashes, new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)).inline(), id)); - if (JsonCodegen.usesReadCodec(properties[i], resolver)) { + if (resolver.usesReadCodec(properties[i])) { if (usesReaderSlot(properties[i].readTypeInfo())) { expressions.add( new Expression.Assign( @@ -2177,7 +2177,7 @@ private void addUnwrappedReaderAssignment( new Expression.Assign( new Reference("this.rp" + id, TypeRef.of(JsonFieldInfo.class)), property)); } - if (JsonCodegen.usesReadCodec(field, resolver)) { + if (resolver.usesReadCodec(field)) { if (usesReaderSlot(field.readTypeInfo())) { expressions.add( new Expression.Assign( @@ -4501,7 +4501,7 @@ final Expression not(Expression expression) { } final boolean usesReadCodec(JsonFieldInfo property) { - return JsonCodegen.usesReadCodec(property, resolver); + return resolver.usesReadCodec(property); } final boolean usesReadInfo(JsonFieldInfo property) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 92a10d76b0..fe1bf4802d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -161,7 +161,7 @@ abstract Expression utf16EnumFieldValue( private boolean usesWriteCodec(JsonFieldInfo property) { return property.writeKind() == JsonFieldKind.COLLECTION ? !writesStringCollectionDirectly(property) - : JsonCodegen.usesWriteCodec(property); + : resolver.usesWriteCodec(property); } static Reference fieldRef(String name, Class type) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java index b898fc8473..0713a37178 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java @@ -84,7 +84,7 @@ int splitMemberThreshold() { @Override boolean writesStringCollectionDirectly(JsonFieldInfo property) { - return JsonCodegen.writesStringCollectionDirectly(property); + return JsonTypeResolver.writesStringCollectionDirectly(property); } @Override diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java index 7b0b8ba1a4..c590f9ec41 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java @@ -101,7 +101,7 @@ int splitMemberThreshold() { @Override boolean writesStringCollectionDirectly(JsonFieldInfo property) { - return JsonCodegen.writesStringCollectionDirectly(property) + return JsonTypeResolver.writesStringCollectionDirectly(property) && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) == null; } 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 a0b6436559..562b2451fc 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 @@ -19,6 +19,21 @@ package org.apache.fory.json.resolver; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -26,6 +41,8 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.fory.annotation.Internal; @@ -38,11 +55,11 @@ * *

Registration is keyed by class identity and replaces any previous codec for the exact class. A * {@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. + * from an existing {@code ForyJson}. The runtime registry reads that owned snapshot directly. */ public final class CodecRegistry { + private static final Set> PROTECTED_BUILTIN_TYPES = protectedBuiltinTypes(); + private final ConcurrentMap, JsonValueCodec> codecs; private final ConcurrentMap, FactoryBinding> factories; @@ -61,6 +78,7 @@ private CodecRegistry( public void register(Class type, JsonValueCodec codec) { Preconditions.checkNotNull(type); Preconditions.checkNotNull(codec); + checkRegistrationType(type); codecs.put(type, codec); factories.remove(type); } @@ -68,10 +86,17 @@ public void register(Class type, JsonValueCodec codec) { public void registerFactory(Class type, JsonCodecFactory factory) { Preconditions.checkNotNull(type); Preconditions.checkNotNull(factory); + checkRegistrationType(type); factories.put(type, FactoryBinding.create(type, factory)); codecs.remove(type); } + /** Returns whether the exact type is implemented by a protected built-in reader/writer path. */ + @Internal + public static boolean isProtectedBuiltinType(Class type) { + return PROTECTED_BUILTIN_TYPES.contains(type); + } + public JsonValueCodec get(Class type) { return codecs.get(type); } @@ -117,31 +142,56 @@ public CodecRegistry copy() { return new CodecRegistry(copied, copiedFactories); } - public String codegenKey() { - List, JsonValueCodec>> entries = new ArrayList<>(codecs.entrySet()); - entries.sort(Comparator.comparing(entry -> entry.getKey().getName())); - StringBuilder builder = new StringBuilder(entries.size() * 48); - for (Map.Entry, JsonValueCodec> entry : entries) { - appendIdentity(builder, entry.getKey().getName()); - appendIdentity(builder, entry.getValue().getClass().getName()); - } - List, FactoryBinding>> factoryEntries = - new ArrayList<>(factories.entrySet()); - factoryEntries.sort(Comparator.comparing(entry -> entry.getKey().getName())); - for (Map.Entry, FactoryBinding> entry : factoryEntries) { - FactoryBinding binding = entry.getValue(); - appendIdentity(builder, entry.getKey().getName()); - appendIdentity(builder, binding.factory.getClass().getName()); - appendIdentity(builder, binding.key); - for (Class runtimeType : binding.handledRuntimeClasses) { - appendIdentity(builder, runtimeType.getName()); - } - } - return builder.toString(); + private static Set> protectedBuiltinTypes() { + Set> types = Collections.newSetFromMap(new IdentityHashMap<>()); + Collections.addAll( + types, + boolean.class, + Boolean.class, + byte.class, + Byte.class, + short.class, + Short.class, + int.class, + Integer.class, + long.class, + Long.class, + float.class, + Float.class, + double.class, + Double.class, + char.class, + Character.class, + String.class, + CharSequence.class, + Number.class, + BigInteger.class, + BigDecimal.class, + UUID.class, + LocalDate.class, + LocalTime.class, + LocalDateTime.class, + Instant.class, + Duration.class, + ZoneOffset.class, + ZonedDateTime.class, + Year.class, + YearMonth.class, + MonthDay.class, + Period.class, + OffsetTime.class, + OffsetDateTime.class, + byte[].class, + String[].class, + long[].class); + return Collections.unmodifiableSet(types); } - private static void appendIdentity(StringBuilder builder, String value) { - builder.append(value.length()).append(':').append(value); + private static void checkRegistrationType(Class type) { + if (isProtectedBuiltinType(type)) { + throw new IllegalArgumentException( + "JSON codec registration is not supported for built-in type " + type.getTypeName()); + } } /** Immutable build-time snapshot of one exact factory registration. */ @@ -169,6 +219,7 @@ private static FactoryBinding create(Class target, JsonCodecFactory factory) HashSet names = new HashSet<>(); for (Class runtimeType : declared) { Preconditions.checkNotNull(runtimeType); + checkRegistrationType(runtimeType); if (!target.isAssignableFrom(runtimeType)) { throw new IllegalArgumentException( runtimeType.getName() + " is not a subtype of " + target.getName()); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java index 9ec488616d..a5b6b16a0e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java @@ -19,182 +19,171 @@ package org.apache.fory.json.resolver; -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.codec.GeneratedJsonCodec; -import org.apache.fory.json.codegen.JsonCodegenKey; +import org.apache.fory.json.codegen.GeneratedCodecKey; 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. */ +/** Frozen exact-key registry of generated JSON classes retained in a Native Image. */ @Internal public final class JsonGeneratedClassRegistry { - private static Map pending = new HashMap<>(); - private static Map configurations = Collections.emptyMap(); + private static Map> pendingClasses = new HashMap<>(); + private static Map> pendingCompanions = new HashMap<>(); + private static GeneratedEntry[] generatedEntries = new GeneratedEntry[0]; + private static CompanionEntry[] companionEntries = new CompanionEntry[0]; private static boolean frozen; private JsonGeneratedClassRegistry() {} /** Publishes one hosted configuration's generated classes during Native Image analysis. */ - public static synchronized Set> register( - JsonCodegenKey key, JsonSharedRegistry hostedRegistry) { + public static synchronized Set> register(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(); - pending.put(key, configuration); - } + GeneratedClasses generated = hostedRegistry.generatedClasses(); LinkedHashSet> added = new LinkedHashSet<>(); - configuration.merge(generatedClasses, added); - configurations = snapshot(); + mergeClasses(generated.classes(), added); + mergeCompanions(generated.sourceCodecs(), added); + snapshot(); return added; } - /** Finalizes generated class lookup after Native Image analysis. */ + /** Finalizes Native runtime lookup and releases hosted mutable state. */ public static synchronized void freeze() { if (frozen) { return; } - pending = null; + snapshot(); + pendingClasses = null; + pendingCompanions = 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()); + static Class generatedClass(GeneratedCodecKey key) { + Map> pending = pendingClasses; + if (pending != null) { + return pending.get(key); + } + for (GeneratedEntry entry : generatedEntries) { + if (entry.key.equals(key)) { + return entry.generatedClass; + } } - return Collections.unmodifiableMap(snapshot); + return null; } - static Configuration configuration(JsonCodegenKey key) { - return configurations.get(key); + static GeneratedJsonCodec sourceCodec(CompanionKey key) { + Map> pending = pendingCompanions; + if (pending != null) { + return pending.get(key); + } + for (CompanionEntry entry : companionEntries) { + if (entry.key.equals(key)) { + return entry.codec; + } + } + return null; + } + + private static void mergeClasses(Map> source, Set> added) { + for (Map.Entry> entry : source.entrySet()) { + GeneratedCodecKey key = entry.getKey(); + Class generatedClass = entry.getValue(); + Class previous = pendingClasses.putIfAbsent(key, generatedClass); + if (previous == null) { + added.add(generatedClass); + } else if (previous != generatedClass) { + throw new IllegalStateException( + "Conflicting generated Fory JSON classes for " + key.targetClass().getName()); + } + } + } + + private static void mergeCompanions( + Map> source, Set> added) { + mergeSourceCodecs(source, pendingCompanions, added); } static void mergeSourceCodecs( - Map, GeneratedJsonCodec> source, - Map, GeneratedJsonCodec> target, + Map> source, + Map> target, Set> added) { - for (Map.Entry, GeneratedJsonCodec> entry : source.entrySet()) { - TypeRef type = entry.getKey(); + for (Map.Entry> entry : source.entrySet()) { GeneratedJsonCodec codec = entry.getValue(); - GeneratedJsonCodec previous = target.putIfAbsent(type, codec); + GeneratedJsonCodec previous = target.putIfAbsent(entry.getKey(), codec); if (previous == null) { added.add(codec.getClass()); } else if (previous.getClass() != codec.getClass()) { throw new IllegalStateException( - "Conflicting source-generated Fory JSON companions for " + type); + "Conflicting source-generated Fory JSON companions for " + entry.getKey().type); } } } - 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, Class> utf8CollectionWriters; - private final Map, Class> utf8CollectionReaders; - private final Map, GeneratedJsonCodec> sourceCodecs; - - private Configuration(MutableConfiguration source) { - stringWriters = immutableValues(source.stringWriters); - utf8Writers = immutableValues(source.utf8Writers); - latin1Readers = immutableValues(source.latin1Readers); - utf16Readers = immutableValues(source.utf16Readers); - utf8Readers = immutableValues(source.utf8Readers); - utf8CollectionWriters = immutableValues(source.utf8CollectionWriters); - utf8CollectionReaders = immutableValues(source.utf8CollectionReaders); - sourceCodecs = immutableValues(source.sourceCodecs); - } - - Class stringWriter(TypeRef type) { - return stringWriters.get(type); + private static void snapshot() { + generatedEntries = new GeneratedEntry[pendingClasses.size()]; + int index = 0; + for (Map.Entry> entry : pendingClasses.entrySet()) { + generatedEntries[index++] = new GeneratedEntry(entry.getKey(), entry.getValue()); } - - Class utf8Writer(TypeRef type) { - return utf8Writers.get(type); - } - - Class latin1Reader(TypeRef type) { - return latin1Readers.get(type); - } - - Class utf16Reader(TypeRef type) { - return utf16Readers.get(type); - } - - Class utf8Reader(TypeRef type) { - return utf8Readers.get(type); + companionEntries = new CompanionEntry[pendingCompanions.size()]; + index = 0; + for (Map.Entry> entry : pendingCompanions.entrySet()) { + companionEntries[index++] = new CompanionEntry(entry.getKey(), entry.getValue()); } + } - Class utf8CollectionWriter(TypeRef type) { - return utf8CollectionWriters.get(type); - } + static final class CompanionKey { + private final TypeRef type; + private final Class mixinType; + private final int hash; - Class utf8CollectionReader(TypeRef type) { - return utf8CollectionReaders.get(type); + CompanionKey(TypeRef type, Class mixinType) { + this.type = type; + this.mixinType = mixinType; + hash = type.hashCode() * 31 + System.identityHashCode(mixinType); } - GeneratedJsonCodec sourceCodec(TypeRef type) { - return sourceCodecs.get(type); + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof CompanionKey)) { + return false; + } + CompanionKey that = (CompanionKey) other; + return type.equals(that.type) && mixinType == that.mixinType; } - private static Map immutableValues(Map values) { - return values.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(values)); + @Override + public int hashCode() { + return hash; } } - 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, Class> utf8CollectionWriters = new HashMap<>(); - private final Map, Class> utf8CollectionReaders = new HashMap<>(); - private final Map, GeneratedJsonCodec> sourceCodecs = 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); - merge(source.utf8CollectionWriters(), utf8CollectionWriters, added); - merge(source.utf8CollectionReaders(), utf8CollectionReaders, added); - JsonGeneratedClassRegistry.mergeSourceCodecs(source.sourceCodecs(), sourceCodecs, added); - } + private static final class GeneratedEntry { + private final GeneratedCodecKey key; + private final Class generatedClass; - private static void merge( - Map> source, Map> target, Set> added) { - for (Map.Entry> entry : source.entrySet()) { - merge(entry.getKey(), entry.getValue(), target, added); - } + private GeneratedEntry(GeneratedCodecKey key, Class generatedClass) { + this.key = key; + this.generatedClass = generatedClass; } + } - 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 final class CompanionEntry { + private final CompanionKey key; + private final GeneratedJsonCodec codec; - private Configuration freeze() { - return new Configuration(this); + private CompanionEntry(CompanionKey key, GeneratedJsonCodec codec) { + this.key = key; + this.codec = codec; } } } 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 97bd727b66..57f6d7a99e 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 @@ -118,15 +118,14 @@ import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.SqlJsonCodecs; +import org.apache.fory.json.codegen.GeneratedCodecKey; 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.CodecRegistry.FactoryBinding; -import org.apache.fory.json.resolver.JsonGeneratedClassRegistry.Configuration; -import org.apache.fory.meta.TypeExtMeta; +import org.apache.fory.json.resolver.JsonGeneratedClassRegistry.CompanionKey; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; import org.apache.fory.reflect.ReflectionUtils; @@ -179,7 +178,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 nativeCodegenEnabled; private final boolean hostedCodegen; private final boolean asyncCompilationEnabled; private final ExecutorService compilationService; @@ -198,16 +197,9 @@ public int compare(DeclarationCandidate left, DeclarationCandidate right) { private final ConcurrentHashMap, MapKeyCodec> mapKeyCodecs; private final ConcurrentHashMap, GeneratedJsonCodec> generatedCodecs; private final Set> typesWithoutGeneratedCodec; - private final ConcurrentHashMap, GeneratedJsonCodec> generatedCodecCapabilities; - private final ConcurrentHashMap, CompletableFuture>> stringWriterClasses; - private final ConcurrentHashMap, CompletableFuture>> utf8WriterClasses; - private final ConcurrentHashMap, CompletableFuture>> latin1ReaderClasses; - private final ConcurrentHashMap, CompletableFuture>> utf16ReaderClasses; - private final ConcurrentHashMap, CompletableFuture>> utf8ReaderClasses; - private final ConcurrentHashMap, CompletableFuture>> - utf8CollectionWriterClasses; - private final ConcurrentHashMap, CompletableFuture>> - utf8CollectionReaderClasses; + private final ConcurrentHashMap> generatedCodecCapabilities; + private final ConcurrentHashMap>> + generatedClassFutures; // Only ForyJson's fixed-pool reader-local caches publish production entries here, and each reader // owns its configured entry limit. This reference-reuse table does not own a second capacity // policy. @@ -264,22 +256,15 @@ private JsonSharedRegistry( generatedCodecs = new ConcurrentHashMap<>(); typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet(); generatedCodecCapabilities = new ConcurrentHashMap<>(); - stringWriterClasses = new ConcurrentHashMap<>(); - utf8WriterClasses = new ConcurrentHashMap<>(); - latin1ReaderClasses = new ConcurrentHashMap<>(); - utf16ReaderClasses = new ConcurrentHashMap<>(); - utf8ReaderClasses = new ConcurrentHashMap<>(); - utf8CollectionWriterClasses = new ConcurrentHashMap<>(); - utf8CollectionReaderClasses = new ConcurrentHashMap<>(); + generatedClassFutures = 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.codegenKey(), classLoader, hostedCodegen) : null; - nativeCodegenKey = - codegenEnabled && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE ? config.codegenKey() : null; + codegen = createCompiler ? new JsonCodegen(hostedCodegen) : null; + nativeCodegenEnabled = + codegenEnabled && GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && !hostedCodegen; asyncCompilationEnabled = createCompiler && !hostedCodegen && config.asyncCompilationEnabled(); this.compilationService = compilationService; registerExactCodecs(); @@ -302,24 +287,10 @@ GeneratedClasses generatedClasses() { if (codegen == null || asyncCompilationEnabled) { throw new IllegalStateException("Generated class snapshots require synchronous codegen"); } - Map, Class> stringWriters = completedClasses(stringWriterClasses); - Map, Class> utf8Writers = completedClasses(utf8WriterClasses); - Map, Class> latin1Readers = completedClasses(latin1ReaderClasses); - Map, Class> utf16Readers = completedClasses(utf16ReaderClasses); - Map, Class> utf8Readers = completedClasses(utf8ReaderClasses); - Map, Class> utf8CollectionWriters = completedClasses(utf8CollectionWriterClasses); - Map, Class> utf8CollectionReaders = completedClasses(utf8CollectionReaderClasses); - Map, GeneratedJsonCodec> sourceCodecs = + Map> classes = completedClasses(generatedClassFutures); + Map> sourceCodecs = immutableSnapshot(generatedCodecCapabilities); - return new GeneratedClasses( - stringWriters, - utf8Writers, - latin1Readers, - utf16Readers, - utf8Readers, - utf8CollectionWriters, - utf8CollectionReaders, - sourceCodecs); + return new GeneratedClasses(classes, sourceCodecs); } private static Map immutableSnapshot(Map values) { @@ -347,208 +318,101 @@ private static Map> completedClasses( } 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, Class> utf8CollectionWriters; - private final Map, Class> utf8CollectionReaders; - private final Map, GeneratedJsonCodec> sourceCodecs; + private final Map> classes; + private final Map> sourceCodecs; private GeneratedClasses( - Map, Class> stringWriters, - Map, Class> utf8Writers, - Map, Class> latin1Readers, - Map, Class> utf16Readers, - Map, Class> utf8Readers, - Map, Class> utf8CollectionWriters, - Map, Class> utf8CollectionReaders, - Map, GeneratedJsonCodec> sourceCodecs) { - this.stringWriters = stringWriters; - this.utf8Writers = utf8Writers; - this.latin1Readers = latin1Readers; - this.utf16Readers = utf16Readers; - this.utf8Readers = utf8Readers; - this.utf8CollectionWriters = utf8CollectionWriters; - this.utf8CollectionReaders = utf8CollectionReaders; + Map> classes, + Map> sourceCodecs) { + this.classes = classes; this.sourceCodecs = sourceCodecs; } - Map, Class> stringWriters() { - return stringWriters; + Map> classes() { + return classes; } - Map, Class> utf8Writers() { - return utf8Writers; - } - - Map, Class> latin1Readers() { - return latin1Readers; - } - - Map, Class> utf16Readers() { - return utf16Readers; - } - - Map, Class> utf8Readers() { - return utf8Readers; - } - - Map, Class> utf8CollectionWriters() { - return utf8CollectionWriters; - } - - Map, Class> utf8CollectionReaders() { - return utf8CollectionReaders; - } - - Map, GeneratedJsonCodec> sourceCodecs() { + Map> sourceCodecs() { return sourceCodecs; } } CompletableFuture> stringWriterClass( JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - TypeRef generatedType = generatedCapabilityType(typeInfo.typeRef()); + GeneratedCodecKey key = + resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.STRING_WRITER); return generatedClassFuture( - stringWriterClasses, - generatedType, - () -> codegen.compileStringWriter(generatedType, owner, resolver)); + generatedClassFutures, key, () -> codegen.compileStringWriter(key, owner, resolver)); } CompletableFuture> utf8WriterClass( JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - TypeRef generatedType = generatedCapabilityType(typeInfo.typeRef()); + GeneratedCodecKey key = + resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_WRITER); return generatedClassFuture( - utf8WriterClasses, - generatedType, - () -> codegen.compileUtf8Writer(generatedType, owner, resolver)); + generatedClassFutures, key, () -> codegen.compileUtf8Writer(key, owner, resolver)); } CompletableFuture> latin1ReaderClass( JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - TypeRef generatedType = generatedCapabilityType(typeInfo.typeRef()); + GeneratedCodecKey key = + resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.LATIN1_READER); return generatedClassFuture( - latin1ReaderClasses, - generatedType, - () -> codegen.compileLatin1Reader(generatedType, owner, resolver)); + generatedClassFutures, key, () -> codegen.compileLatin1Reader(key, owner, resolver)); } CompletableFuture> utf16ReaderClass( JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - TypeRef generatedType = generatedCapabilityType(typeInfo.typeRef()); + GeneratedCodecKey key = + resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF16_READER); return generatedClassFuture( - utf16ReaderClasses, - generatedType, - () -> codegen.compileUtf16Reader(generatedType, owner, resolver)); + generatedClassFutures, key, () -> codegen.compileUtf16Reader(key, owner, resolver)); } CompletableFuture> utf8ReaderClass( JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - TypeRef generatedType = generatedCapabilityType(typeInfo.typeRef()); + GeneratedCodecKey key = + resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_READER); return generatedClassFuture( - utf8ReaderClasses, - generatedType, - () -> codegen.compileUtf8Reader(generatedType, owner, resolver)); + generatedClassFutures, key, () -> codegen.compileUtf8Reader(key, owner, resolver)); } CompletableFuture> utf8CollectionWriterClass( - TypeRef declaredType, CollectionCodec owner) { - TypeRef generatedType = generatedCapabilityType(declaredType); + JsonTypeInfo typeInfo, CollectionCodec owner, JsonTypeResolver resolver) { + GeneratedCodecKey key = + resolver.generatedCollectionKey( + typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_WRITER); return generatedClassFuture( - utf8CollectionWriterClasses, - generatedType, - () -> codegen.compileUtf8CollectionWriter(generatedType, owner)); + generatedClassFutures, + key, + () -> codegen.compileUtf8CollectionWriter(key, typeInfo.typeRef(), owner)); } CompletableFuture> utf8CollectionReaderClass( - TypeRef declaredType, CollectionCodec owner) { - TypeRef generatedType = generatedCapabilityType(declaredType); + JsonTypeInfo typeInfo, CollectionCodec owner, JsonTypeResolver resolver) { + GeneratedCodecKey key = + resolver.generatedCollectionKey( + typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_READER); return generatedClassFuture( - utf8CollectionReaderClasses, - generatedType, - () -> codegen.compileUtf8CollectionReader(generatedType, owner)); + generatedClassFutures, + key, + () -> codegen.compileUtf8CollectionReader(key, typeInfo.typeRef(), owner)); } boolean generatedCapabilitiesEnabled() { - return codegen != null || nativeConfiguration() != null; + return codegen != null || nativeCodegenEnabled; } boolean hostedCodegen() { return hostedCodegen; } - boolean missingNativeConfiguration() { - return nativeCodegenKey != null && nativeConfiguration() == null; - } - boolean nativeGeneratedClasses() { - return nativeCodegenKey != null && codegen == null && nativeConfiguration() != null; - } - - Class nativeStringWriterClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null ? null : configuration.stringWriter(generatedCapabilityType(type)); + return nativeCodegenEnabled && codegen == null; } - Class nativeUtf8WriterClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null ? null : configuration.utf8Writer(generatedCapabilityType(type)); - } - - Class nativeLatin1ReaderClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null ? null : configuration.latin1Reader(generatedCapabilityType(type)); - } - - Class nativeUtf16ReaderClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null ? null : configuration.utf16Reader(generatedCapabilityType(type)); - } - - Class nativeUtf8ReaderClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null ? null : configuration.utf8Reader(generatedCapabilityType(type)); - } - - Class nativeUtf8CollectionWriterClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null - ? null - : configuration.utf8CollectionWriter(generatedCapabilityType(type)); - } - - Class nativeUtf8CollectionReaderClass(TypeRef type) { - Configuration configuration = nativeConfiguration(); - return configuration == null - ? null - : configuration.utf8CollectionReader(generatedCapabilityType(type)); - } - - static TypeRef generatedCapabilityType(TypeRef type) { - TypeExtMeta metadata = type.getTypeExtMeta(); - if (metadata == null - || metadata.typeId() != Types.UNKNOWN - || metadata.trackingRef() - || metadata.nullableWrapper() - || metadata.covariant()) { - return type; - } - // Generated codecs own the value body after the outer occurrence null gate. Ordinary outer - // nullability therefore cannot change generated source, while every nested occurrence and any - // non-default outer semantic fact must remain part of the structural capability identity. - return TypeRef.ofSemanticTypeArguments( - type.getType(), - null, - type.hasExplicitTypeArguments() ? type.getTypeArguments() : null, - type.isArray() ? type.getComponentType() : null); - } - - private Configuration nativeConfiguration() { - return nativeCodegenKey == null - ? null - : JsonGeneratedClassRegistry.configuration(nativeCodegenKey); + Class nativeGeneratedClass(GeneratedCodecKey key) { + return nativeCodegenEnabled ? JsonGeneratedClassRegistry.generatedClass(key) : null; } private CompletableFuture> generatedClassFuture( @@ -633,15 +497,16 @@ GeneratedJsonCodec generatedCodec(TypeRef type) { private GeneratedJsonCodec generatedCodec(TypeRef type, boolean requireCompanion) { if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && !hostedCodegen) { - Configuration configuration = nativeConfiguration(); // Native hosted analysis owns reflection reachability and generated capabilities. A Java // annotation-processor companion is an optional faster operation source, not a prerequisite. - return configuration == null ? null : configuration.sourceCodec(type); + return JsonGeneratedClassRegistry.sourceCodec( + new CompanionKey(type, mixinType(type.getRawType()))); } GeneratedJsonCodec codec = generatedCodec(type.getRawType(), requireCompanion && !hostedCodegen); if (codec != null && hostedCodegen) { - GeneratedJsonCodec previous = generatedCodecCapabilities.putIfAbsent(type, codec); + CompanionKey key = new CompanionKey(type, mixinType(type.getRawType())); + GeneratedJsonCodec previous = generatedCodecCapabilities.putIfAbsent(key, codec); if (previous != null && previous != codec) { throw new IllegalStateException("Conflicting generated JSON companions for " + type); } @@ -1351,6 +1216,11 @@ Class mixinType(Class targetType) { return overlay == null ? null : overlay.mixinType(); } + boolean canonicalProtectedBuiltin(JsonTypeInfo typeInfo, Object capability) { + return CodecRegistry.isProtectedBuiltinType(typeInfo.rawType()) + && exactCodecs.get(typeInfo.rawType()) == capability; + } + /** Adds exact pair context to a cold effective-schema validation failure. */ @Internal public ForyJsonException mixinSchemaFailure(Class targetType, ForyJsonException failure) { 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 087a0b9004..01370209b9 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 @@ -52,6 +52,7 @@ import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.CompositeJsonCodec; +import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonObjectModel; import org.apache.fory.json.codec.JsonSubTypesInfo; @@ -64,10 +65,14 @@ import org.apache.fory.json.codec.ObjectCodec.AnyInfo; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.StringWriterCodec; +import org.apache.fory.json.codec.TransparentUnboxedValueCodec; import org.apache.fory.json.codec.UnboxedValueCodec; import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; import org.apache.fory.json.codec.Utf8WriterCodec; +import org.apache.fory.json.codegen.GeneratedCodecKey; +import org.apache.fory.json.codegen.GeneratedCodecKey.MemberDescriptor; +import org.apache.fory.json.codegen.GeneratedCodecKey.Role; import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.codegen.JsonJITContext; import org.apache.fory.json.meta.JsonCreatorDeclaration; @@ -76,8 +81,6 @@ 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.meta.TypeExtMeta; import org.apache.fory.reflect.TypeRef; import org.apache.fory.type.Types; @@ -99,13 +102,8 @@ * identity so parameterized and language-semantic bindings own distinct generated capabilities. */ public final class JsonTypeResolver { - private static final Logger LOG = LoggerFactory.getLogger(JsonTypeResolver.class); private static final TypeExtMeta NON_NULL_SUBTYPE_TYPE = TypeExtMeta.of(Types.UNKNOWN, false, false, false, false); - 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; @@ -814,23 +812,11 @@ private void completeResolution(ResolutionSnapshot snapshot) { 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) { if (snapshot == null) { return; @@ -1051,11 +1037,6 @@ public ObjectCodec createObjectCodec(TypeRef ownerType, JsonObjectModel ob Class type = ownerType.getRawType(); sharedRegistry.checkSecure(type); validateObjectModel(ownerType, objectModel); - if (!sharedRegistry.hostedCodegen() && sharedRegistry.missingNativeConfiguration()) { - throw new ForyJsonException( - "Missing provider-selected Fory JSON Native configuration for language object model " - + ownerType); - } // The language module already owns the exact construction/accessor model. A Java generated // companion may still supply faster operations, but its absence must not override that model. GeneratedJsonCodec generatedCodec = sharedRegistry.generatedCodecIfPresent(ownerType); @@ -1306,7 +1287,7 @@ private StringWriterCodec newStringWriter( (StringWriterCodec[]) new StringWriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field)) { + if (usesWriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.STRING_WRITER); } @@ -1338,7 +1319,7 @@ private Utf8WriterCodec newUtf8Writer( (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesUtf8WriteCodec(field, this)) { + if (usesUtf8WriteCodec(field)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_WRITER); } @@ -1367,7 +1348,7 @@ private StringWriterCodec newUnwrappedStringWriter( (StringWriterCodec[]) new StringWriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesWriteCodec(field)) { + if (usesWriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.STRING_WRITER); } @@ -1396,7 +1377,7 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (JsonCodegen.usesUtf8WriteCodec(field, this)) { + if (usesUtf8WriteCodec(field)) { JsonTypeInfo child = field.writeTypeInfo(); codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_WRITER); } @@ -1467,10 +1448,9 @@ private Latin1ReaderCodec newLatin1Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field, this)) { + if (usesReadCodec(field)) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); - } else if (JsonCodegen.readNestedType(field, this) != null - && field.readRawType() != owner.type()) { + } else if (readNestedType(field) != null && field.readRawType() != owner.type()) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); } } @@ -1537,10 +1517,9 @@ private Utf16ReaderCodec newUtf16Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field, this)) { + if (usesReadCodec(field)) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); - } else if (JsonCodegen.readNestedType(field, this) != null - && field.readRawType() != owner.type()) { + } else if (readNestedType(field) != null && field.readRawType() != owner.type()) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); } } @@ -1607,10 +1586,9 @@ private Utf8ReaderCodec newUtf8Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (JsonCodegen.usesReadCodec(field, this)) { + if (usesReadCodec(field)) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); - } else if (JsonCodegen.readNestedType(field, this) != null - && field.readRawType() != owner.type()) { + } else if (readNestedType(field) != null && field.readRawType() != owner.type()) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); } } @@ -1759,7 +1737,7 @@ private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); } - private enum CapabilityKind { + enum CapabilityKind { STRING_WRITER, UTF8_WRITER, LATIN1_READER, @@ -1767,6 +1745,475 @@ private enum CapabilityKind { UTF8_READER } + GeneratedCodecKey generatedObjectKey( + JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { + ArrayList projection = new ArrayList<>(); + ArrayList> classes = new ArrayList<>(); + if (!readerKind(kind)) { + projection.add(sharedRegistry.writeNullFields()); + } + projection.add(sharedRegistry.propertyDiscoveryEnabled()); + projection.add(sharedRegistry.propertyNamingStrategy()); + addMixinProjection(owner.type(), projection, classes); + if (readerKind(kind)) { + projection.add(owner.graphMemoryBytes()); + projection.add(owner.hasValidators()); + } + JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); + if (unwrapped != null) { + for (JsonUnwrappedInfo.Group group : unwrapped.groups()) { + ObjectCodec child = group.childCodec(); + Class childType = child.type(); + projection.add(childType); + classes.add(childType); + addMixinProjection(childType, projection, classes); + projection.add(MemberDescriptor.of(accessorMember(group.declaration().writeAccessor()))); + projection.add(MemberDescriptor.of(accessorMember(group.declaration().readAccessor()))); + projection.add(group.readIndex()); + projection.add(group.parent() == null ? -1 : group.parent().readIndex()); + projection.add(group.declaration().constructionIndex()); + Class parentType = group.parentCodec().type(); + projection.add(parentType); + classes.add(parentType); + projection.add(group.writeEnabled()); + projection.add(group.readEnabled()); + if (readerKind(kind)) { + projection.add(child.graphMemoryBytes()); + projection.add(child.hasValidators()); + addCreatorProjection(child.creatorInfo(), projection, classes, kind); + } + } + } + if (readerKind(kind)) { + addCreatorProjection(owner.creatorInfo(), projection, classes, kind); + if (unwrapped == null) { + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + addReadFields(owner, owner.readFields(), projection, classes, kind); + } else { + addCreatorFields(owner, creator.fields(), projection, classes, kind); + } + } else { + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + addReadFields(owner, owner.readFields(), projection, classes, kind); + } else { + addCreatorFields(owner, creator.fields(), projection, classes, kind); + } + for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { + projection.add("route"); + projection.add(route.group().readIndex()); + if (route.field() != null) { + addReadField(owner, route.field(), projection, classes, kind); + } else { + addCreatorField(owner, route.creatorField(), projection, classes, kind); + } + } + } + } else { + if (unwrapped != null) { + addUnwrappedWriteOrder(unwrapped.writeEntries(), projection, classes); + } + JsonFieldInfo[] fields = unwrapped == null ? owner.writeFields() : unwrapped.writeFields(); + for (int i = 0; i < fields.length; i++) { + addWriteField(owner, fields[i], projection, classes, kind); + } + } + addAnyProjection(owner, projection, classes, kind); + return GeneratedCodecKey.object( + typeInfo.rawType(), role(kind), projection.toArray(), classes.toArray(new Class[0])); + } + + private void addUnwrappedWriteOrder( + JsonUnwrappedInfo.WriteEntry[] entries, + ArrayList projection, + ArrayList> classes) { + projection.add(entries.length); + for (JsonUnwrappedInfo.WriteEntry entry : entries) { + projection.add(entry.kind()); + if (entry.kind() == JsonUnwrappedInfo.DIRECT) { + JsonFieldInfo field = entry.field(); + projection.add(field.name()); + addMember(field.writeField(), projection, classes); + addMember(field.writeGetter(), projection, classes); + } else if (entry.kind() == JsonUnwrappedInfo.GROUP) { + projection.add(entry.group().readIndex()); + addUnwrappedWriteOrder(entry.group().writeEntries(), projection, classes); + } + } + } + + GeneratedCodecKey generatedCollectionKey( + JsonTypeInfo typeInfo, CollectionCodec owner, CapabilityKind kind) { + Type type = typeInfo.type(); + Class rawType = CodecUtils.rawType(type, Collection.class); + Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + return GeneratedCodecKey.collection( + rawType, + elementType, + kind == CapabilityKind.UTF8_WRITER + ? Role.UTF8_COLLECTION_WRITER + : Role.UTF8_COLLECTION_READER, + owner instanceof CollectionCodec.StringCollectionCodec); + } + + private void addMixinProjection( + Class target, ArrayList projection, ArrayList> classes) { + Class mixin = sharedRegistry.mixinType(target); + projection.add(mixin); + if (mixin != null) { + classes.add(mixin); + } + } + + private void addCreatorProjection( + JsonCreatorInfo creator, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + if (creator == null) { + projection.add(null); + return; + } + projection.add("creator"); + addMember(creator.executable(), projection, classes); + addMember(creator.invocationExecutable(), projection, classes); + addMember(creator.defaultConstructor(), projection, classes); + projection.add(creator.argumentCount()); + projection.add(creator.defaultMaskCount()); + projection.add(creator.tracksArgumentPresence()); + for (int i = 0; i < creator.argumentCount(); i++) { + projection.add(creator.defaultMaskBit(i)); + projection.add(creator.hasDefault(i)); + addMember(creator.defaultMethod(i), projection, classes); + } + JsonFieldInfo[] deferred = creator.deferredFields(); + projection.add(deferred.length); + for (int i = 0; i < deferred.length; i++) { + projection.add(creator.deferredRequired(i)); + addReadField(null, deferred[i], projection, classes, kind); + } + } + + private void addWriteField( + ObjectCodec owner, + JsonFieldInfo field, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + JsonTypeInfo child = field.writeTypeInfo(); + projection.add("write"); + projection.add(field.name()); + projection.add(field.writeRawType()); + projection.add(child.rawType()); + projection.add(field.writeKind()); + projection.add(field.writeNull()); + projection.add(field.requiresNonNullWrite()); + projection.add(field.writesRawString()); + projection.add(field.writesUnboxedValue()); + projection.add(usesWriterSlot(owner, child)); + addMember(field.writeField(), projection, classes); + addMember(field.writeGetter(), projection, classes); + addCapabilityProjection(child, kind, projection, classes); + addUnboxedProjection(field.writeUnboxedValueCodec(), false, projection, classes); + addClass(field.writeRawType(), classes); + addClass(child.rawType(), classes); + } + + private void addReadFields( + ObjectCodec owner, + JsonFieldInfo[] fields, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + for (JsonFieldInfo field : fields) { + addReadField(owner, field, projection, classes, kind); + } + } + + private void addReadField( + ObjectCodec owner, + JsonFieldInfo field, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + JsonTypeInfo child = field.readTypeInfo(); + projection.add("read"); + projection.add(field.name()); + projection.add(field.readRawType()); + projection.add(child.rawType()); + projection.add(field.readKind()); + projection.add(field.readIndex()); + projection.add(field.hasOccurrenceNullability()); + projection.add(field.occurrenceNullable()); + projection.add(field.occurrenceWrapsNull()); + projection.add(field.readsUnboxedValue()); + projection.add(owner != null && usesReaderSlot(owner, child)); + addMember(field.readField(), projection, classes); + addMember(field.readSetter(), projection, classes); + addCapabilityProjection(child, kind, projection, classes); + addUnboxedProjection(field.readUnboxedValueCodec(), true, projection, classes); + addClass(field.readRawType(), classes); + addClass(child.rawType(), classes); + } + + private void addCreatorFields( + ObjectCodec owner, + JsonCreatorFieldInfo[] fields, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + for (JsonCreatorFieldInfo field : fields) { + addCreatorField(owner, field, projection, classes, kind); + } + } + + private void addCreatorField( + ObjectCodec owner, + JsonCreatorFieldInfo field, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + JsonTypeInfo child = field.typeInfo(); + projection.add("argument"); + projection.add(field.name()); + projection.add(field.argumentIndex()); + projection.add(field.rawType()); + projection.add(child.rawType()); + projection.add(child.kind()); + projection.add(child.nullable()); + projection.add(child.rejectsNull()); + projection.add(field.materializesNullCarrier()); + projection.add(owner != null && usesReaderSlot(owner, child)); + addCapabilityProjection(child, kind, projection, classes); + addUnboxedProjection(field.unboxedValueCodec(), true, projection, classes); + addClass(field.rawType(), classes); + addClass(child.rawType(), classes); + } + + private void addAnyProjection( + ObjectCodec owner, + ArrayList projection, + ArrayList> classes, + CapabilityKind kind) { + AnyInfo any = owner.anyInfo(); + if (any == null) { + projection.add(null); + return; + } + projection.add("any"); + projection.add(any.valueRawType()); + projection.add(any.writeIndex()); + projection.add(any.constructionIndex()); + boolean storesCodec = storesAnyCodec(owner, any); + projection.add(storesCodec); + projection.add( + storesCodec + && (readerKind(kind) + ? usesReaderSlot(owner, any.valueTypeInfo()) + : usesWriterSlot(owner, any.valueTypeInfo()))); + if (readerKind(kind)) { + addMember(any.readField(), projection, classes); + addMember(any.readSetter(), projection, classes); + } else { + addMember(any.writeField(), projection, classes); + addMember(any.writeGetter(), projection, classes); + } + addCapabilityProjection(any.valueTypeInfo(), kind, projection, classes); + addClass(any.valueRawType(), classes); + } + + private void addCapabilityProjection( + JsonTypeInfo typeInfo, + CapabilityKind kind, + ArrayList projection, + ArrayList> classes) { + Object capability = currentCapability(typeInfo, kind); + Class capabilityClass = + sharedRegistry.canonicalProtectedBuiltin(typeInfo, capability) + ? null + : logicalCapabilityClass(typeInfo, capability); + projection.add(capabilityClass); + projection.add(typeInfo.kind()); + projection.add(typeInfo.nullable()); + projection.add(typeInfo.rejectsNull()); + projection.add(typeInfo.transparentNull()); + addClass(capabilityClass, classes); + } + + private Class logicalCapabilityClass(JsonTypeInfo typeInfo, Object capability) { + if (canonicalObjectOwner(typeInfo) != null) { + return ObjectCodec.class; + } + CollectionCodec collection = collectionCodecs.get(typeInfo); + if (collection != null) { + return collection.getClass(); + } + return capability instanceof ClosedSubtypeCodec + ? ClosedSubtypeCodec.class + : capability.getClass(); + } + + private static void addUnboxedProjection( + UnboxedValueCodec codec, + boolean reader, + ArrayList projection, + ArrayList> classes) { + if (codec == null) { + projection.add(null); + return; + } + projection.add(codec.getClass()); + addClass(codec.getClass(), classes); + if (codec instanceof DirectUnboxedValueCodec) { + DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) codec; + addMember( + reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), projection, classes); + return; + } + TransparentUnboxedValueCodec transparent = (TransparentUnboxedValueCodec) codec; + JsonTypeInfo terminal = transparent.valueTypeInfo(); + projection.add(terminal.rawType()); + projection.add(terminal.kind()); + addClass(terminal.rawType(), classes); + Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); + projection.add(methods.length); + for (Method method : methods) { + addMember(method, projection, classes); + } + if (reader) { + int[] boxes = transparent.constructBoxBytes(); + projection.add(boxes.length); + for (int box : boxes) { + projection.add(box); + } + } + } + + private static java.lang.reflect.Member accessorMember( + org.apache.fory.json.meta.JsonFieldAccessor accessor) { + if (accessor == null) { + return null; + } + return accessor.getter() != null ? accessor.getter() : accessor.field(); + } + + private static void addMember( + java.lang.reflect.Member member, ArrayList projection, ArrayList> classes) { + MemberDescriptor descriptor = MemberDescriptor.of(member); + projection.add(descriptor); + if (member != null) { + addClass(member.getDeclaringClass(), classes); + } + } + + private static void addClass(Class type, ArrayList> classes) { + if (type != null) { + classes.add(type); + } + } + + private static Role role(CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + return Role.STRING_WRITER; + case UTF8_WRITER: + return Role.UTF8_WRITER; + case LATIN1_READER: + return Role.LATIN1_READER; + case UTF16_READER: + return Role.UTF16_READER; + case UTF8_READER: + return Role.UTF8_READER; + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } + } + + /** Returns the nested object type inlined by generated readers, or {@code null}. */ + @Internal + public Class readNestedType(JsonFieldInfo field) { + if (!field.readsUnboxedValue() + && field.readKind() == JsonFieldKind.OBJECT + && field.readRawType() != Object.class + && canonicalObjectCodec(field.readTypeInfo()) != null) { + return field.readRawType(); + } + return null; + } + + /** Returns whether a generated string writer stores a field codec. */ + @Internal + public boolean usesWriteCodec(JsonFieldInfo field) { + if (field.writesUnboxedValue() && field.writeKind() == JsonFieldKind.ENUM) { + return true; + } + switch (field.writeKind()) { + case ARRAY: + case MAP: + case OBJECT: + return true; + case COLLECTION: + return !writesStringCollectionDirectly(field); + default: + return false; + } + } + + /** Returns whether a generated UTF-8 writer stores a field codec. */ + @Internal + public boolean usesUtf8WriteCodec(JsonFieldInfo field) { + return usesWriteCodec(field) + || field.writeKind() == JsonFieldKind.COLLECTION + && exactUtf8WriterCollection(field.writeTypeInfo()) != null; + } + + /** Returns whether a generated reader stores a field codec. */ + @Internal + public boolean usesReadCodec(JsonFieldInfo field) { + if (field.readsUnboxedValue()) { + if (field.readDirectUnboxedValueCodec() != null) { + return false; + } + Class rawType = field.readTypeInfo().rawType(); + JsonFieldKind kind = field.readKind(); + if (rawType == String.class && kind == JsonFieldKind.STRING) { + return false; + } + if (rawType.isPrimitive()) { + return !((rawType == boolean.class && kind == JsonFieldKind.BOOLEAN) + || (rawType == byte.class && kind == JsonFieldKind.BYTE) + || (rawType == short.class && kind == JsonFieldKind.SHORT) + || (rawType == int.class && kind == JsonFieldKind.INT) + || (rawType == long.class && kind == JsonFieldKind.LONG) + || (rawType == float.class && kind == JsonFieldKind.FLOAT) + || (rawType == double.class && kind == JsonFieldKind.DOUBLE) + || (rawType == char.class && kind == JsonFieldKind.CHAR)); + } + return true; + } + switch (field.readKind()) { + case ENUM: + case ARRAY: + case COLLECTION: + case MAP: + return true; + case OBJECT: + return !(field.readRawType() != Object.class + && canonicalObjectCodec(field.readTypeInfo()) != null); + default: + return false; + } + } + + /** Returns whether the standard string collection writer is fully inlined. */ + @Internal + public static boolean writesStringCollectionDirectly(JsonFieldInfo field) { + return field.writeElementRawType() == String.class + && field.writeTypeInfo().stringWriter().getClass() + == CollectionCodec.StringCollectionCodec.class; + } + private ArrayList capabilityChildren(ObjectCodec owner, CapabilityKind kind) { ArrayList children = new ArrayList<>(); AnyInfo any = owner.anyInfo(); @@ -1777,9 +2224,7 @@ private ArrayList capabilityChildren(ObjectCodec owner, Capabil for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; boolean usesCodec = - kind == CapabilityKind.UTF8_WRITER - ? JsonCodegen.usesUtf8WriteCodec(field, this) - : JsonCodegen.usesWriteCodec(field); + kind == CapabilityKind.UTF8_WRITER ? usesUtf8WriteCodec(field) : usesWriteCodec(field); if (usesCodec && (field.writeRawType() != owner.type() || canonicalObjectOwner(field.writeTypeInfo()) == null)) { @@ -1836,8 +2281,8 @@ && storesAnyCodec(owner, any)) { private void addReadDependency( ArrayList children, ObjectCodec owner, JsonFieldInfo field) { - if (JsonCodegen.usesReadCodec(field, this) - || JsonCodegen.readNestedType(field, this) != null && field.readRawType() != owner.type()) { + if (usesReadCodec(field) + || readNestedType(field) != null && field.readRawType() != owner.type()) { children.add(field.readTypeInfo()); } } @@ -1953,50 +2398,34 @@ private boolean canCompile(JsonTypeInfo typeInfo, ObjectCodec owner, Capabili if (owner.fixedInstance()) { return false; } - if (nativeObjectClass(typeInfo.typeRef(), kind) != null) { + GeneratedCodecKey key = generatedObjectKey(typeInfo, owner, kind); + if (sharedRegistry.nativeGeneratedClass(key) != null) { return true; } - if (sharedRegistry.nativeGeneratedClasses() && typeInfo.type() instanceof ParameterizedType) { - // A selected Native configuration contains only the exact generic bindings reached during - // hosted analysis. A missing parameterized object is not the same schema as its raw class. - return true; + if (sharedRegistry.nativeGeneratedClasses()) { + return false; } return codegen != null && (kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER - ? codegen.canCompileWriter(owner) - : codegen.canCompileReader(owner)); + ? codegen.canCompileWriter(key, owner) + : codegen.canCompileReader(key, owner)); } private boolean canCompileCollection(JsonTypeInfo typeInfo, CapabilityKind kind) { - TypeRef type = typeInfo.typeRef(); - boolean generated = - kind == CapabilityKind.UTF8_WRITER - ? sharedRegistry.nativeUtf8CollectionWriterClass(type) != null - : sharedRegistry.nativeUtf8CollectionReaderClass(type) != null; - if (generated) { + CollectionCodec owner = exactUtf8CollectionOwner(typeInfo); + GeneratedCodecKey key = generatedCollectionKey(typeInfo, owner, kind); + if (sharedRegistry.nativeGeneratedClass(key) != null) { return true; } - if (sharedRegistry.nativeGeneratedClasses() && type.getType() instanceof ParameterizedType) { - return true; + if (sharedRegistry.nativeGeneratedClasses()) { + return false; } return codegen != null; } - private Class nativeObjectClass(TypeRef 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 Class nativeObjectClass( + JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { + return sharedRegistry.nativeGeneratedClass(generatedObjectKey(typeInfo, owner, kind)); } private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { @@ -2022,12 +2451,10 @@ private CompletableFuture> generatedClass(CapabilityNode node, Capabili } if (node.collectionOwner != null) { if (kind == CapabilityKind.UTF8_WRITER) { - return sharedRegistry.utf8CollectionWriterClass( - node.typeInfo.typeRef(), node.collectionOwner); + return sharedRegistry.utf8CollectionWriterClass(node.typeInfo, node.collectionOwner, this); } if (kind == CapabilityKind.UTF8_READER) { - return sharedRegistry.utf8CollectionReaderClass( - node.typeInfo.typeRef(), node.collectionOwner); + return sharedRegistry.utf8CollectionReaderClass(node.typeInfo, node.collectionOwner, this); } throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); } @@ -2052,15 +2479,10 @@ private Class nativeGeneratedClass(CapabilityNode node, CapabilityKind kind) throw new IllegalStateException("Inline subtype readers reuse child generated classes"); } if (node.collectionOwner != null) { - if (kind == CapabilityKind.UTF8_WRITER) { - return sharedRegistry.nativeUtf8CollectionWriterClass(node.typeInfo.typeRef()); - } - if (kind == CapabilityKind.UTF8_READER) { - return sharedRegistry.nativeUtf8CollectionReaderClass(node.typeInfo.typeRef()); - } - throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); + return sharedRegistry.nativeGeneratedClass( + generatedCollectionKey(node.typeInfo, node.collectionOwner, kind)); } - return nativeObjectClass(node.typeInfo.typeRef(), kind); + return nativeObjectClass(node.typeInfo, node.objectOwner, kind); } private Object newCapability( 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 55728b6f19..6607bf3cdd 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 @@ -64,7 +64,7 @@ import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.ObjectCodec.AnyInfo; -import org.apache.fory.json.codegen.JsonCodegenKey; +import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.meta.JsonFieldInfo; @@ -83,7 +83,7 @@ import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeReflection; -/** Prepares reachable Fory JSON models and provider-selected codecs for Native Image. */ +/** Prepares reachable Fory JSON models and exact generated codecs for Native Image. */ final class ForyJsonGraalVMFeature implements Feature { private static final String SCALA_DERIVED_CODEC_METHOD = "derived$ScalaJsonCodec"; private static final String SCALA_JSON_CODEC_CLASS = "org.apache.fory.json.scala.ScalaJsonCodec"; @@ -114,10 +114,8 @@ final class ForyJsonGraalVMFeature implements Feature { private final Set processedCreators = new LinkedHashSet<>(); private final Set> processedObjectModels = Collections.newSetFromMap(new IdentityHashMap<>()); - // 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 ArrayList hostedConfigurations = new ArrayList<>(); + private boolean defaultConfigurationAdded; @Override public String getDescription() { @@ -159,6 +157,7 @@ public void duringAnalysis(DuringAnalysisAccess access) { } if (type == ForyJson.class) { registerBuiltInTypes(access); + addDefaultConfiguration(); changed = true; } } @@ -244,25 +243,9 @@ private boolean registerProvider(DuringAnalysisAccess access, Class providerC throw providerFailure( providerClass, "provider method returned a codegen-disabled ForyJson: " + method, null); } - JsonCodegenKey key = config.codegenKey(); - ArrayList configurations = hostedConfigurations.get(key); - if (configurations == null) { - configurations = new ArrayList<>(); - hostedConfigurations.put(key, configurations); - } - ClassLoader classLoader = config.classLoader(); - HostedConfiguration configuration = null; - for (HostedConfiguration candidate : configurations) { - if (candidate.classLoader == classLoader) { - configuration = candidate; - break; - } - } - if (configuration == null) { - configuration = new HostedConfiguration(config); - configurations.add(configuration); - changed = true; - } + HostedConfiguration configuration = new HostedConfiguration(config); + hostedConfigurations.add(configuration); + changed = true; ArrayList, FactoryBinding>> bindings = new ArrayList<>(config.codecRegistry().factoryBindings().entrySet()); bindings.sort(Comparator.comparing(entry -> entry.getKey().getName())); @@ -276,6 +259,14 @@ private boolean registerProvider(DuringAnalysisAccess access, Class providerC return changed; } + private void addDefaultConfiguration() { + if (defaultConfigurationAdded) { + return; + } + defaultConfigurationAdded = true; + hostedConfigurations.add(new HostedConfiguration(ForyJson.builder().build().config())); + } + private boolean addFactoryRoot( DuringAnalysisAccess access, HostedConfiguration configuration, Class type) { boolean changed = configuration.factoryModels.add(type); @@ -344,49 +335,46 @@ private static List providerMethods(Class providerClass) { private boolean generateConfigurations(DuringAnalysisAccess access) { boolean changed = false; - for (Map.Entry> entry : - hostedConfigurations.entrySet()) { - for (HostedConfiguration configuration : entry.getValue()) { - LinkedHashSet> selectedModels = new LinkedHashSet<>(processedModels); - selectedModels.addAll(configuration.factoryModels); - if (configuration.scalaJsonCodecs) { - selectedModels.addAll(scalaDerivedModels); + for (HostedConfiguration configuration : hostedConfigurations) { + LinkedHashSet> selectedModels = new LinkedHashSet<>(processedModels); + selectedModels.addAll(configuration.factoryModels); + if (configuration.scalaJsonCodecs) { + selectedModels.addAll(scalaDerivedModels); + } + for (Map.Entry, Set>> mixin : reachableMixins.entrySet()) { + if (mixin.getValue().contains(configuration.mixins.get(mixin.getKey()))) { + selectedModels.add(mixin.getKey()); } - 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) { + // A raw generic Class is not a schema. Hosted capabilities are generated only when a + // concrete TypeRef occurrence is reached from a selected non-generic root; eagerly + // resolving the raw class would also make unreached bindings available in the image. + if (model.getTypeParameters().length != 0) { + continue; } - ArrayList> models = new ArrayList<>(selectedModels); - models.sort(Comparator.comparing(Class::getName)); - for (Class model : models) { - // A raw generic Class is not a schema. Hosted capabilities are generated only when a - // concrete TypeRef occurrence is reached from a selected non-generic root; eagerly - // resolving the raw class would also make unreached bindings available in the image. - if (model.getTypeParameters().length != 0) { - continue; - } - if (!configuration.processedModels.add(model)) { - continue; - } - List> objectModels; - try { - objectModels = configuration.resolver.generateHostedCodecs(model); - } catch (RuntimeException | LinkageError e) { - throw new IllegalStateException( - "Cannot generate Fory JSON codecs for " + model.getName(), e); - } - objectModels.sort(Comparator.comparing(codec -> codec.type().getName())); - for (ObjectCodec objectModel : objectModels) { - registerObjectModel(access, objectModel); - } - Set> generatedClasses = - JsonGeneratedClassRegistry.register(entry.getKey(), configuration.registry); - for (Class generatedClass : generatedClasses) { - registerGeneratedClass(generatedClass); - } - changed = true; + if (!configuration.processedModels.add(model)) { + continue; + } + List> objectModels; + try { + objectModels = configuration.resolver.generateHostedCodecs(model); + } catch (RuntimeException | LinkageError e) { + throw new IllegalStateException( + "Cannot generate Fory JSON codecs for " + model.getName(), e); } + objectModels.sort(Comparator.comparing(codec -> codec.type().getName())); + for (ObjectCodec objectModel : objectModels) { + registerObjectModel(access, objectModel); + } + Set> generatedClasses = + JsonGeneratedClassRegistry.register(configuration.registry); + for (Class generatedClass : generatedClasses) { + registerGeneratedClass(generatedClass); + } + changed = true; } } return changed; @@ -662,6 +650,7 @@ private void registerReflectiveDeclarations(Set declarations) @Override public void afterAnalysis(AfterAnalysisAccess access) { JsonGeneratedClassRegistry.freeze(); + JsonCodegen.resetGeneratedClassCache(); } private void registerModelHierarchy(DuringAnalysisAccess access, Class type) { @@ -1108,7 +1097,6 @@ 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; @@ -1117,7 +1105,6 @@ private static final class HostedConfiguration { private final Set> factoryModels = new LinkedHashSet<>(); private HostedConfiguration(JsonConfig config) { - classLoader = config.classLoader(); registry = JsonSharedRegistry.forHostedCodegen(config); resolver = new JsonTypeResolver(registry); mixins = config.mixins(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index ecdac1756f..5dff34b30e 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -1330,7 +1330,6 @@ private static ControlledJson controlledJson(CodecRegistry codecs, int concurren Collections., Class>emptyMap(), new JsonCodecFactory[0], Collections.emptyList(), - Collections.emptyList(), null); ControlledExecutor executor = new ControlledExecutor(); Constructor constructor = diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java new file mode 100644 index 0000000000..4a03671442 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java @@ -0,0 +1,156 @@ +/* + * 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 static org.apache.fory.json.JsonTestSupport.nullCodec; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; + +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.resolver.CodecRegistry; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.reflect.TypeRef; +import org.testng.annotations.Test; + +public class JsonCodecRegistrationTest { + private static final Class[] PROTECTED_TYPES = { + boolean.class, + Boolean.class, + byte.class, + Byte.class, + short.class, + Short.class, + int.class, + Integer.class, + long.class, + Long.class, + float.class, + Float.class, + double.class, + Double.class, + char.class, + Character.class, + String.class, + CharSequence.class, + Number.class, + BigInteger.class, + BigDecimal.class, + UUID.class, + LocalDate.class, + LocalTime.class, + LocalDateTime.class, + Instant.class, + Duration.class, + ZoneOffset.class, + ZonedDateTime.class, + Year.class, + YearMonth.class, + MonthDay.class, + Period.class, + OffsetTime.class, + OffsetDateTime.class, + byte[].class, + String[].class, + long[].class + }; + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void protectedBuiltinRegistrationsRejected() { + JsonCodecFactory factory = (type, resolver, runtimeType) -> null; + for (Class type : PROTECTED_TYPES) { + assertTrue(CodecRegistry.isProtectedBuiltinType(type), type.getTypeName()); + assertThrows( + IllegalArgumentException.class, + () -> ForyJson.builder().registerCodec((Class) type, nullCodec())); + assertThrows( + IllegalArgumentException.class, + () -> ForyJson.builder().registerCodec((Class) type, factory)); + } + } + + @Test + public void factoryHandledBuiltinRejectedBeforeMutation() { + CodecRegistry registry = new CodecRegistry(); + JsonCodecFactory factory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return null; + } + + @Override + public List> handledRuntimeClasses() { + return Collections.singletonList(String.class); + } + }; + assertThrows( + IllegalArgumentException.class, () -> registry.registerFactory(Object.class, factory)); + assertFalse(registry.contains(Object.class)); + } + + @Test + public void moduleExactBuiltinRejected() { + assertThrows( + IllegalArgumentException.class, + () -> + ForyJson.builder() + .withModule(context -> context.registerCodec(String.class, nullCodec())) + .build()); + } + + @Test + public void applicationTypeRegistrationAllowed() { + CodecRegistry registry = new CodecRegistry(); + registry.register(ApplicationValue.class, nullCodec()); + assertTrue(registry.contains(ApplicationValue.class)); + } + + @Test + public void otherBuiltinRegistrationAllowed() { + CodecRegistry registry = new CodecRegistry(); + assertFalse(CodecRegistry.isProtectedBuiltinType(File.class)); + registry.register(File.class, nullCodec()); + assertTrue(registry.contains(File.class)); + } + + public static final class ApplicationValue {} +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java index a23c984f5c..e0e88aaf4d 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java @@ -19,7 +19,6 @@ package org.apache.fory.json; -import static org.apache.fory.json.JsonTestSupport.nullCodec; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; import static org.testng.Assert.fail; @@ -269,8 +268,8 @@ public void primitiveNullRejected() { } @Test - public void customPrimitiveNullRejected() { - ForyJson json = newJsonBuilder().registerCodec(int.class, nullCodec()).build(); + public void creatorPrimitiveNullRejected() { + ForyJson json = newJson(); assertThrows( ForyJsonException.class, () -> json.fromJson("{\"id\":null}", CustomPrimitiveCreator.class)); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonFieldNameCacheTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonFieldNameCacheTest.java index 168cc05598..0feb7b1871 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonFieldNameCacheTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonFieldNameCacheTest.java @@ -19,7 +19,6 @@ package org.apache.fory.json; -import static org.apache.fory.json.JsonTestSupport.generatedCodecIdentity; import static org.apache.fory.json.JsonTestSupport.generatedUtf8WriterClass; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; @@ -67,9 +66,9 @@ public void configuration() { .build(); oneEntry.toJsonBytes(new TypedFields()); twoEntries.toJsonBytes(new TypedFields()); - assertEquals( - generatedCodecIdentity(generatedUtf8WriterClass(oneEntry, TypedFields.class)), - generatedCodecIdentity(generatedUtf8WriterClass(twoEntries, TypedFields.class))); + assertSame( + generatedUtf8WriterClass(oneEntry, TypedFields.class), + generatedUtf8WriterClass(twoEntries, TypedFields.class)); } @Test diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java index 768b3c8e37..996ca0de13 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java @@ -19,10 +19,14 @@ package org.apache.fory.json; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertSame; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Constructor; import java.nio.charset.StandardCharsets; import java.util.Collections; @@ -31,10 +35,17 @@ import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonType; import org.apache.fory.json.codec.JsonObjectModel; +import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.data.PublicFields; +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.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonTypeInfo; 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.meta.TypeExtMeta; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.StringSerializer; @@ -55,7 +66,7 @@ public void outerNullabilityReusesObjectClasses() { JsonTypeInfo tracked = resolver.getTypeInfo( TypeRef.of(Model.class, TypeExtMeta.of(Types.UNKNOWN, false, true, false, false))); - assertNotSame(raw.utf8Reader().getClass(), tracked.utf8Reader().getClass()); + assertSame(raw.utf8Reader().getClass(), tracked.utf8Reader().getClass()); } @Test @@ -75,12 +86,12 @@ public void outerNullabilityReusesCollectionClasses() { resolver.getTypeInfo(listType(null, TypeRef.of(String.class, ordinary(false)))); JsonTypeInfo nullableElement = resolver.getTypeInfo(listType(null, TypeRef.of(String.class, ordinary(true)))); - assertNotSame(nonNullElement.utf8Writer().getClass(), nullableElement.utf8Writer().getClass()); - assertNotSame(nonNullElement.utf8Reader().getClass(), nullableElement.utf8Reader().getClass()); + assertSame(nonNullElement.utf8Writer().getClass(), nullableElement.utf8Writer().getClass()); + assertSame(nonNullElement.utf8Reader().getClass(), nullableElement.utf8Reader().getClass()); } @Test - public void componentMetadataRemainsDistinct() { + public void injectedComponentMetadataReusesParentClass() { JsonTypeResolver resolver = resolver(); TypeRef nonNullArray = TypeRef.of( @@ -89,8 +100,8 @@ public void componentMetadataRemainsDistinct() { TypeRef.of(String[].class, ordinary(false), null, TypeRef.of(String.class, ordinary(true))); JsonTypeInfo nonNull = resolver.getTypeInfo(boxType(nonNullArray)); JsonTypeInfo nullable = resolver.getTypeInfo(boxType(nullableArray)); - assertNotSame(nonNull.utf8Writer().getClass(), nullable.utf8Writer().getClass()); - assertNotSame(nonNull.utf8Reader().getClass(), nullable.utf8Reader().getClass()); + assertSame(nonNull.utf8Writer().getClass(), nullable.utf8Writer().getClass()); + assertSame(nonNull.utf8Reader().getClass(), nullable.utf8Reader().getClass()); } @Test @@ -149,11 +160,157 @@ public void hostedModelNeedsNoCompanion() throws Exception { assertSame(typeInfo.rawType(), HostedAnnotatedModel.class); } + @Test + public void directCodecClassVersionsParent() { + JsonTypeInfo first = parentType(new ChildCodecA()); + JsonTypeInfo equivalent = parentType(new ChildCodecA()); + JsonTypeInfo different = parentType(new ChildCodecB()); + + assertObjectClasses(first, equivalent); + assertDifferentObjectClasses(first, different); + } + + @Test + public void directCodecStateStaysInstanceOwned() { + ForyJson first = parentJson(new StatefulChildCodec("first:")); + ForyJson second = parentJson(new StatefulChildCodec("second:")); + JsonTypeInfo firstType = parentType(first); + JsonTypeInfo secondType = parentType(second); + assertObjectClasses(firstType, secondType); + + Parent value = new Parent(); + value.child = new Child(); + value.child.value = "value"; + assertEquals(first.toJson(value), "{\"child\":\"first:value\"}"); + assertEquals(second.toJson(value), "{\"child\":\"second:value\"}"); + assertEquals(first.fromJson("{\"child\":\"first:value\"}", Parent.class).child.value, "value"); + assertEquals( + second.fromJson("{\"child\":\"second:value\"}", Parent.class).child.value, "value"); + } + + @Test + public void unrelatedRegistrationDoesNotVersionParent() { + JsonTypeInfo first = parentType(new ChildCodecA()); + ForyJson json = parentJson(new ChildCodecA(), true); + assertObjectClasses(first, parentType(json)); + } + + @Test + public void configuredLoaderDoesNotVersionClass() { + ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); + ForyJson second = + ForyJson.builder() + .withClassLoader(new ClassLoader(Model.class.getClassLoader()) {}) + .withAsyncCompilation(false) + .build(); + JsonTypeInfo firstType = + JsonTestSupport.currentTypeResolver(first).getTypeInfo(Model.class, Model.class); + JsonTypeInfo secondType = + JsonTestSupport.currentTypeResolver(second).getTypeInfo(Model.class, Model.class); + assertObjectClasses(firstType, secondType); + } + + @Test + public void collectionClassIgnoresElementCodec() { + JsonTypeInfo first = collectionType(new ChildCodecA()); + JsonTypeInfo different = collectionType(new ChildCodecB()); + + assertSame(first.utf8Writer().getClass(), different.utf8Writer().getClass()); + assertSame(first.utf8Reader().getClass(), different.utf8Reader().getClass()); + } + + @Test + public void sameNamedLoaderClassesDoNotCollide() throws Exception { + byte[] bytes = classBytes(PublicFields.class); + Class firstClass = shadowClass(PublicFields.class, bytes); + Class secondClass = shadowClass(PublicFields.class, bytes); + assertNotSame(firstClass, secondClass); + + JsonTypeInfo first = loaderType(firstClass); + JsonTypeInfo second = loaderType(secondClass); + assertDifferentObjectClasses(first, second); + } + private static JsonTypeResolver resolver() { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); return JsonTestSupport.currentTypeResolver(json); } + private static JsonTypeInfo parentType(JsonValueCodec codec) { + return parentType(parentJson(codec)); + } + + private static ForyJson parentJson(JsonValueCodec codec) { + return parentJson(codec, false); + } + + private static ForyJson parentJson(JsonValueCodec codec, boolean unrelated) { + ForyJsonBuilder builder = + ForyJson.builder().registerCodec(Child.class, codec).withAsyncCompilation(false); + if (unrelated) { + builder.registerCodec(Unrelated.class, JsonTestSupport.nullCodec()); + } + return builder.build(); + } + + private static JsonTypeInfo parentType(ForyJson json) { + return JsonTestSupport.currentTypeResolver(json).getTypeInfo(Parent.class, Parent.class); + } + + private static JsonTypeInfo collectionType(JsonValueCodec codec) { + ForyJson json = + ForyJson.builder().registerCodec(Child.class, codec).withAsyncCompilation(false).build(); + return JsonTestSupport.currentTypeResolver(json).getTypeInfo(new TypeRef>() {}); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static JsonTypeInfo loaderType(Class type) { + ForyJson json = + ForyJson.builder() + .withClassLoader(type.getClassLoader()) + .withAsyncCompilation(false) + .build(); + return JsonTestSupport.currentTypeResolver(json).getTypeInfo((Class) type, type); + } + + private static Class shadowClass(Class type, byte[] bytes) throws ClassNotFoundException { + String name = type.getName(); + ClassLoader loader = + new ClassLoader(type.getClassLoader()) { + @Override + protected Class loadClass(String className, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(className)) { + if (!name.equals(className)) { + return super.loadClass(className, resolve); + } + Class loaded = findLoadedClass(className); + if (loaded == null) { + loaded = defineClass(className, bytes, 0, bytes.length); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + } + }; + return loader.loadClass(name); + } + + private static byte[] classBytes(Class type) throws IOException { + String resource = "/" + type.getName().replace('.', '/') + ".class"; + try (InputStream input = type.getResourceAsStream(resource); + ByteArrayOutputStream output = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int read; + while ((read = input.read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + return output.toByteArray(); + } + } + private static TypeExtMeta ordinary(boolean nullable) { return TypeExtMeta.of(Types.UNKNOWN, nullable, false, false, false); } @@ -176,6 +333,14 @@ private static void assertObjectClasses(JsonTypeInfo expected, JsonTypeInfo actu assertSame(expected.utf8Reader().getClass(), actual.utf8Reader().getClass()); } + private static void assertDifferentObjectClasses(JsonTypeInfo expected, JsonTypeInfo actual) { + assertNotSame(expected.stringWriter().getClass(), actual.stringWriter().getClass()); + assertNotSame(expected.utf8Writer().getClass(), actual.utf8Writer().getClass()); + assertNotSame(expected.latin1Reader().getClass(), actual.latin1Reader().getClass()); + assertNotSame(expected.utf16Reader().getClass(), actual.utf16Reader().getClass()); + assertNotSame(expected.utf8Reader().getClass(), actual.utf8Reader().getClass()); + } + private static void assertGeneratedObject(JsonTypeInfo typeInfo) { assertFalse(ObjectCodec.class.isAssignableFrom(typeInfo.stringWriter().getClass())); assertFalse(ObjectCodec.class.isAssignableFrom(typeInfo.utf8Writer().getClass())); @@ -190,6 +355,94 @@ public static final class Model { public Model() {} } + public static final class Parent { + public Child child; + + public Parent() {} + } + + public static final class Child { + public String value; + + public Child() {} + } + + public static class ChildCodecA implements JsonValueCodec { + @Override + public void writeString(StringJsonWriter writer, Child value) { + writer.writeString(value.value); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, Child value) { + writer.writeString(value.value); + } + + @Override + public Child readLatin1(Latin1JsonReader reader) { + return child(reader.readString()); + } + + @Override + public Child readUtf16(Utf16JsonReader reader) { + return child(reader.readString()); + } + + @Override + public Child readUtf8(Utf8JsonReader reader) { + return child(reader.readString()); + } + + private static Child child(String text) { + Child value = new Child(); + value.value = text; + return value; + } + } + + public static final class ChildCodecB extends ChildCodecA {} + + public static final class StatefulChildCodec extends ChildCodecA { + private final String prefix; + + public StatefulChildCodec(String prefix) { + this.prefix = prefix; + } + + @Override + public void writeString(StringJsonWriter writer, Child value) { + writer.writeString(prefix + value.value); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, Child value) { + writer.writeString(prefix + value.value); + } + + @Override + public Child readLatin1(Latin1JsonReader reader) { + return childWithoutPrefix(reader.readString()); + } + + @Override + public Child readUtf16(Utf16JsonReader reader) { + return childWithoutPrefix(reader.readString()); + } + + @Override + public Child readUtf8(Utf8JsonReader reader) { + return childWithoutPrefix(reader.readString()); + } + + private Child childWithoutPrefix(String text) { + Child value = new Child(); + value.value = text.substring(prefix.length()); + return value; + } + } + + public static final class Unrelated {} + public static final class Box { public T value; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 2f819663cb..c49fa15999 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -19,13 +19,13 @@ package org.apache.fory.json; -import static org.apache.fory.json.JsonTestSupport.generatedCodecIdentity; import static org.apache.fory.json.JsonTestSupport.generatedUtf8WriterClass; import static org.apache.fory.json.JsonTestSupport.newLatin1Reader; import static org.apache.fory.json.JsonTestSupport.newUtf8Reader; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import java.lang.reflect.Field; @@ -51,7 +51,6 @@ import org.testng.annotations.Test; public class JsonGeneratedCodecTest extends ForyJsonTestModels { - private static final String GENERATED_SUFFIX = "ForyJsonCodec"; @Test(dataProvider = "enableCodegen") public void writeRecursiveGeneratedTypes(boolean codegen) { @@ -183,7 +182,7 @@ public void readGeneratedCollectionFields(boolean codegen) { } @Test(dataProvider = "enableCodegen") - public void sameConfigUsesSameId(boolean codegen) throws Exception { + public void sameConfigUsesSameClass(boolean codegen) throws Exception { ForyJson first = newJson(codegen); ForyJson second = newJson(codegen); ForyJson writeNullFields = newJsonBuilder(codegen).writeNullFields(true).build(); @@ -210,29 +209,9 @@ public void sameConfigUsesSameId(boolean codegen) throws Exception { assertEquals(firstCodecClass.getPackage().getName(), PublicFields.class.getPackage().getName()); assertEquals( secondCodecClass.getPackage().getName(), PublicFields.class.getPackage().getName()); - assertGeneratedName(firstCodecClass, PublicFields.class, "Utf8Writer"); - assertGeneratedName(secondCodecClass, PublicFields.class, "Utf8Writer"); - assertGeneratedName(writeNullCodecClass, PublicFields.class, "Utf8Writer"); - assertGeneratedName(snakeCaseCodecClass, PublicFields.class, "Utf8Writer"); - assertEquals(generatedCodecIdentity(secondCodecClass), generatedCodecIdentity(firstCodecClass)); - assertNotEquals( - generatedCodecIdentity(writeNullCodecClass), generatedCodecIdentity(firstCodecClass)); - assertNotEquals( - generatedCodecIdentity(snakeCaseCodecClass), generatedCodecIdentity(firstCodecClass)); - } - - @Test - public void boundedGeneratedName() { - ForyJson json = newJson(true); - json.toJsonBytes(new ModelWithANameLongEnoughToRequireDeterministicPrefixTruncation()); - Class generated = - generatedUtf8WriterClass( - json, ModelWithANameLongEnoughToRequireDeterministicPrefixTruncation.class); - assertTrue( - generated.getSimpleName().length() - <= 32 + "Utf8Writer".length() + GENERATED_SUFFIX.length() + 1 + 64, - generated.getName()); - assertEquals(generatedCodecIdentity(generated).length(), 64); + assertSame(secondCodecClass, firstCodecClass); + assertNotSame(writeNullCodecClass, firstCodecClass); + assertNotSame(snakeCaseCodecClass, firstCodecClass); } @Test @@ -484,10 +463,6 @@ public static class PrefixFields { public int altar; } - public static final class ModelWithANameLongEnoughToRequireDeterministicPrefixTruncation { - public int value; - } - public static class WideFields { public int f0; public String f1; @@ -579,13 +554,4 @@ private static void assertObjectCollections(ObjectCollections value, String name assertEquals(value.set.size(), 2); assertEquals(value.set.iterator().next().name, name + 10); } - - private static void assertGeneratedName( - Class generatedClass, Class valueType, String role) { - String simpleName = generatedClass.getSimpleName(); - assertTrue(simpleName.startsWith(valueType.getSimpleName()), generatedClass.getName()); - assertTrue(simpleName.contains(role + GENERATED_SUFFIX), generatedClass.getName()); - assertTrue(simpleName.contains(GENERATED_SUFFIX + "_"), generatedClass.getName()); - assertEquals(generatedCodecIdentity(generatedClass).length(), 64, generatedClass.getName()); - } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java index 06d4f6d3d1..687b425add 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGraphMemoryBudgetTest.java @@ -41,10 +41,6 @@ import org.apache.fory.json.annotation.JsonCreator; import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonUnwrapped; -import org.apache.fory.json.codec.AbstractJsonValueCodec; -import org.apache.fory.json.codec.Base64ByteArrayCodec; -import org.apache.fory.json.reader.JsonReader; -import org.apache.fory.json.writer.JsonWriter; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.GraphMemoryEstimates; import org.testng.annotations.Factory; @@ -206,14 +202,8 @@ public void primitiveArrayBatches() { assertEquals(values[1023], 1023); long budget = headerBytes + 1023L * Integer.BYTES; - CountingIntCodec.reads = 0; - ForyJson json = - newJsonBuilder() - .withMaxGraphMemoryBytes(budget) - .registerCodec(int.class, new CountingIntCodec()) - .build(); + ForyJson json = newJsonBuilder().withMaxGraphMemoryBytes(budget).build(); assertThrows(ForyJsonException.class, () -> json.fromJson(intArray(1024), int[].class)); - assertEquals(CountingIntCodec.reads, 1023); } @Test @@ -371,13 +361,6 @@ public void dedicatedLeavesAreUncharged() { ForyJson json = jsonWithBudget(1); assertEquals(json.fromJson("123", Long.class), Long.valueOf(123)); assertEquals(json.fromJson("\"a long leaf string\"", String.class), "a long leaf string"); - - ForyJson base64 = - newJsonBuilder() - .withMaxGraphMemoryBytes(1) - .registerCodec(byte[].class, new Base64ByteArrayCodec()) - .build(); - assertEquals(base64.fromJson("\"AQIDBA==\"", byte[].class), new byte[] {1, 2, 3, 4}); } @Test @@ -527,21 +510,6 @@ public CountingChild(int number) { } } - private static final class CountingIntCodec extends AbstractJsonValueCodec { - static int reads; - - @Override - public void write(JsonWriter writer, Integer value) { - writer.writeInt(value.intValue()); - } - - @Override - public Integer read(JsonReader reader) { - reads++; - return Integer.valueOf(reader.readInt()); - } - } - public static final class CountingList extends ArrayList { static int adds; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java index 0fcd99fc0c..04016bf198 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonMixinTest.java @@ -19,10 +19,10 @@ package org.apache.fory.json; -import static org.apache.fory.json.JsonTestSupport.generatedCodecIdentity; import static org.apache.fory.json.JsonTestSupport.generatedUtf8WriterClass; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertSame; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; @@ -216,12 +216,12 @@ public void registrationLifecycle() { second.toJsonBytes(new NameTarget("second")); repeated.toJsonBytes(new NameTarget("repeat")); equivalent.toJsonBytes(new NameTarget("equal")); - assertEquals( - generatedCodecIdentity(generatedUtf8WriterClass(repeated, NameTarget.class)), - generatedCodecIdentity(generatedUtf8WriterClass(equivalent, NameTarget.class))); - assertNotEquals( - generatedCodecIdentity(generatedUtf8WriterClass(first, NameTarget.class)), - generatedCodecIdentity(generatedUtf8WriterClass(second, NameTarget.class))); + assertSame( + generatedUtf8WriterClass(repeated, NameTarget.class), + generatedUtf8WriterClass(equivalent, NameTarget.class)); + assertNotSame( + generatedUtf8WriterClass(first, NameTarget.class), + generatedUtf8WriterClass(second, NameTarget.class)); } assertGeneratedWhenSupported(first, NameTarget.class); assertGeneratedWhenSupported(second, NameTarget.class); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java index 670631f94a..0842c39ca2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonModuleTest.java @@ -125,12 +125,9 @@ public void semanticPrimitiveUsesModuleCodec() { json.fromJson("4294967295".getBytes(StandardCharsets.UTF_8), unsignedInt), Integer.valueOf(-1)); - ForyJson application = - ForyJson.builder() - .withModule(context -> context.registerCodecFactory(factory)) - .registerCodec(int.class, ScalarCodecs.IntCodec.PRIMITIVE) - .build(); - assertEquals(application.toJson(-1, unsignedInt), "-1"); + assertThrows( + IllegalArgumentException.class, + () -> ForyJson.builder().registerCodec(int.class, ScalarCodecs.IntCodec.PRIMITIVE)); } @Test diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonRawValueAnnotationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonRawValueAnnotationTest.java index 9c7d557648..6589cc5ee2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonRawValueAnnotationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRawValueAnnotationTest.java @@ -31,12 +31,6 @@ import org.apache.fory.json.annotation.JsonIgnore; import org.apache.fory.json.annotation.JsonProperty; import org.apache.fory.json.annotation.JsonRawValue; -import org.apache.fory.json.codec.JsonValueCodec; -import org.apache.fory.json.reader.Latin1JsonReader; -import org.apache.fory.json.reader.Utf16JsonReader; -import org.apache.fory.json.reader.Utf8JsonReader; -import org.apache.fory.json.writer.StringJsonWriter; -import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.platform.JdkVersion; import org.testng.SkipException; import org.testng.annotations.Factory; @@ -120,17 +114,6 @@ public void rawTextIsNotValidated() { new String(json.toJsonBytes(value), StandardCharsets.UTF_8), "{\"first\":not-json}"); } - @Test - public void rawWriteOverridesTypeCodec() { - ForyJson json = - newJsonBuilder().registerCodec(String.class, new ReplacingStringCodec()).build(); - RawFields value = new RawFields(); - value.first = "{\"id\":1}"; - assertEquals(json.toJson(value), "{\"first\":{\"id\":1}}"); - assertEquals( - new String(json.toJsonBytes(value), StandardCharsets.UTF_8), "{\"first\":{\"id\":1}}"); - } - @Test public void rejectInvalidDeclarations() { ForyJson json = newJson(); @@ -209,31 +192,4 @@ public Map getValues() { return null; } } - - public static final class ReplacingStringCodec implements JsonValueCodec { - @Override - public void writeString(StringJsonWriter writer, String value) { - writer.writeString("replacement"); - } - - @Override - public void writeUtf8(Utf8JsonWriter writer, String value) { - writer.writeString("replacement"); - } - - @Override - public String readLatin1(Latin1JsonReader reader) { - return reader.readString(); - } - - @Override - public String readUtf16(Utf16JsonReader reader) { - return reader.readString(); - } - - @Override - public String readUtf8(Utf8JsonReader reader) { - return reader.readString(); - } - } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java index bc60e927ec..d34f385836 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java @@ -19,7 +19,6 @@ package org.apache.fory.json; -import static org.apache.fory.json.JsonTestSupport.nullCodec; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; @@ -75,7 +74,7 @@ public void writeReadRecordClass() throws Exception { } @Test - public void customPrimitiveNull() throws Exception { + public void primitiveNull() throws Exception { if (JdkVersion.MAJOR_VERSION < 17) { throw new SkipException("Java record test requires JDK 17+"); } @@ -84,7 +83,7 @@ public void customPrimitiveNull() throws Exception { "JsonPrimitiveRecord", "package org.apache.fory.json.records;\n" + "public record JsonPrimitiveRecord(int value) {}\n"); - ForyJson json = newJsonBuilder().registerCodec(int.class, nullCodec()).build(); + ForyJson json = newJson(); assertThrows(ForyJsonException.class, () -> json.fromJson("{\"value\":null}", type)); assertThrows( ForyJsonException.class, diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 38b8409741..d7b2b7b6c1 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -25,7 +25,6 @@ import static org.apache.fory.json.JsonTestSupport.newUtf16Reader; import static org.apache.fory.json.JsonTestSupport.newUtf8Reader; import static org.apache.fory.json.JsonTestSupport.newUtf8Writer; -import static org.apache.fory.json.JsonTestSupport.nullCodec; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; @@ -886,19 +885,6 @@ public void handleBigNumberSubtypes(boolean codegen) { containers.bigIntegers = Arrays.asList(integer); assertSubtypeRejected(() -> json.toJson(containers), BigIntegerSubtype.class); - ForyJson custom = - newJsonBuilder(codegen) - .registerCodec( - BigInteger.class, new TaggedNumberCodec<>("integer", BigInteger.valueOf(42))) - .registerCodec( - BigDecimal.class, new TaggedNumberCodec<>("decimal", new BigDecimal("1.25"))) - .build(); - fields.integer = integer; - fields.decimal = decimal; - String expected = "{\"decimal\":\"decimal\",\"integer\":\"integer\"}"; - assertEquals(custom.toJson(fields), expected); - assertEquals(new String(custom.toJsonBytes(fields), StandardCharsets.UTF_8), expected); - ForyJson subtypeCustom = newJsonBuilder(codegen) .registerCodec( @@ -2212,44 +2198,9 @@ public void generatedFloatingReadersAllowWhitespace(boolean codegen) { assertGeneratedWhenSupported(json, GeneratedFloatingFields.class, codegen); } - @Test(dataProvider = "enableCodegen") - public void customNumericCodecsOwnFields(boolean codegen) { - ForyJson json = - newJsonBuilder(codegen) - .registerCodec(float.class, new TaggedNumberCodec<>("float", Float.valueOf(11.5f))) - .registerCodec(Float.class, new TaggedNumberCodec<>("float", Float.valueOf(11.5f))) - .registerCodec(double.class, new TaggedNumberCodec<>("double", Double.valueOf(22.5d))) - .registerCodec(Double.class, new TaggedNumberCodec<>("double", Double.valueOf(22.5d))) - .registerCodec( - BigDecimal.class, new TaggedNumberCodec<>("decimal", new BigDecimal("33.5"))) - .build(); - CustomNumericFields fields = new CustomNumericFields(); - fields.floatValue = 1.25f; - fields.floatBoxed = Float.valueOf(1.25f); - fields.doubleValue = 2.5d; - fields.doubleBoxed = Double.valueOf(2.5d); - fields.decimal = new BigDecimal("3.75"); - String expected = - "{\"decimal\":\"decimal\",\"doubleBoxed\":\"double\"," - + "\"doubleValue\":\"double\",\"floatBoxed\":\"float\"," - + "\"floatValue\":\"float\"}"; - assertEquals(json.toJson(fields), expected); - assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); - assertCustomNumericFields(json.fromJson(expected, CustomNumericFields.class)); - assertCustomNumericFields( - json.fromJson(expected.getBytes(StandardCharsets.UTF_8), CustomNumericFields.class)); - assertCustomNumericFields( - json.fromJson( - "{\"ignored\":\"\u0100\",\"decimal\":\"decimal\"," - + "\"doubleBoxed\":\"double\",\"doubleValue\":\"double\"," - + "\"floatBoxed\":\"float\",\"floatValue\":\"float\"}", - CustomNumericFields.class)); - assertGeneratedWhenSupported(json, CustomNumericFields.class, codegen); - } - @Test(dataProvider = "enableCodegen") public void customPrimitiveNull(boolean codegen) { - ForyJson json = newJsonBuilder(codegen).registerCodec(int.class, nullCodec()).build(); + ForyJson json = newJson(codegen); assertThrows(ForyJsonException.class, () -> json.fromJson("null", int.class)); assertThrows( ForyJsonException.class, @@ -2282,60 +2233,6 @@ public void customPrimitiveNull(boolean codegen) { assertGeneratedWhenSupported(json, CustomPrimitiveSetter.class, codegen); } - @Test(dataProvider = "enableCodegen") - public void customNumericCodecsOwnContainers(boolean codegen) { - ForyJson json = - newJsonBuilder(codegen) - .registerCodec(float.class, new TaggedNumberCodec<>("float", Float.valueOf(11.5f))) - .registerCodec(Float.class, new TaggedNumberCodec<>("float", Float.valueOf(11.5f))) - .registerCodec( - BigDecimal.class, new TaggedNumberCodec<>("decimal", new BigDecimal("33.5"))) - .build(); - CustomNumericContainers value = new CustomNumericContainers(); - value.decimalArray = new BigDecimal[] {new BigDecimal("1.25")}; - value.decimals = new LinkedHashMap<>(); - value.decimals.put("a", new BigDecimal("1.25")); - value.floatArray = new Float[] {Float.valueOf(2.5f)}; - value.floats = Arrays.asList(Float.valueOf(2.5f)); - value.primitiveFloats = new float[] {2.5f}; - String expected = - "{\"decimalArray\":[\"decimal\"],\"decimals\":{\"a\":\"decimal\"}," - + "\"floatArray\":[\"float\"],\"floats\":[\"float\"]," - + "\"primitiveFloats\":[\"float\"]}"; - assertEquals(json.toJson(value), expected); - assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); - assertCustomNumericContainers(json.fromJson(expected, CustomNumericContainers.class)); - assertCustomNumericContainers( - json.fromJson(expected.getBytes(StandardCharsets.UTF_8), CustomNumericContainers.class)); - assertCustomNumericContainers( - json.fromJson( - "{\"ignored\":\"\u0100\"," + expected.substring(1), CustomNumericContainers.class)); - assertGeneratedWhenSupported(json, CustomNumericContainers.class, codegen); - } - - @Test(dataProvider = "enableCodegen") - public void customScalarCodecsOwnDirectContainers(boolean codegen) { - ForyJson json = - newJsonBuilder(codegen) - .registerCodec(String.class, new TaggedStringCodec("string", "decoded")) - .registerCodec(long.class, new TaggedNumberCodec<>("long", Long.valueOf(7L))) - .build(); - CustomDirectContainers value = new CustomDirectContainers(); - value.longs = new long[] {1L}; - value.names = Arrays.asList("source"); - value.strings = new String[] {"source"}; - String expected = "{\"longs\":[\"long\"],\"names\":[\"string\"]," + "\"strings\":[\"string\"]}"; - assertEquals(json.toJson(value), expected); - assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); - assertCustomDirectContainers(json.fromJson(expected, CustomDirectContainers.class)); - assertCustomDirectContainers( - json.fromJson(expected.getBytes(StandardCharsets.UTF_8), CustomDirectContainers.class)); - assertCustomDirectContainers( - json.fromJson( - "{\"ignored\":\"\u0100\"," + expected.substring(1), CustomDirectContainers.class)); - assertGeneratedWhenSupported(json, CustomDirectContainers.class, codegen); - } - @Test public void floatingFallbackErrorPositions() { assertFloatingErrorPosition(" 01", 3, "Leading zero in number"); @@ -2462,14 +2359,6 @@ public static final class GeneratedFloatingFields { public float floatValue; } - public static final class CustomNumericFields { - public BigDecimal decimal; - public Double doubleBoxed; - public double doubleValue; - public Float floatBoxed; - public float floatValue; - } - public static final class CustomPrimitiveField { public int value; } @@ -2482,20 +2371,6 @@ public void setValue(int value) { } } - public static final class CustomNumericContainers { - public BigDecimal[] decimalArray; - public Map decimals; - public Float[] floatArray; - public List floats; - public float[] primitiveFloats; - } - - public static final class CustomDirectContainers { - public long[] longs; - public List names; - public String[] strings; - } - public static final class FloatingArrays { public Double[] boxedDoubles; public Float[] boxedFloats; @@ -3024,77 +2899,12 @@ public T readUtf8(Utf8JsonReader reader) { } } - private static final class TaggedStringCodec implements JsonValueCodec { - private final String token; - private final String decoded; - - private TaggedStringCodec(String token, String decoded) { - this.token = token; - this.decoded = decoded; - } - - @Override - public void writeString(StringJsonWriter writer, String value) { - writer.writeString(token); - } - - @Override - public void writeUtf8(Utf8JsonWriter writer, String value) { - writer.writeString(token); - } - - @Override - public String readLatin1(Latin1JsonReader reader) { - assertEquals(reader.readString(), token); - return decoded; - } - - @Override - public String readUtf16(Utf16JsonReader reader) { - assertEquals(reader.readString(), token); - return decoded; - } - - @Override - public String readUtf8(Utf8JsonReader reader) { - assertEquals(reader.readString(), token); - return decoded; - } - } - private static Utf16JsonReader utf16Reader(String input) { byte[] bytes = new byte[input.length() << 1]; StringSerializer.copyStringCharsToBytes(input, bytes); return newUtf16Reader().reset(input, bytes); } - private static void assertCustomNumericFields(CustomNumericFields fields) { - assertEquals(Float.floatToRawIntBits(fields.floatValue), Float.floatToRawIntBits(11.5f)); - assertEquals( - Float.floatToRawIntBits(fields.floatBoxed.floatValue()), Float.floatToRawIntBits(11.5f)); - assertEquals(Double.doubleToRawLongBits(fields.doubleValue), Double.doubleToRawLongBits(22.5d)); - assertEquals( - Double.doubleToRawLongBits(fields.doubleBoxed.doubleValue()), - Double.doubleToRawLongBits(22.5d)); - assertEquals(fields.decimal, new BigDecimal("33.5")); - } - - private static void assertCustomNumericContainers(CustomNumericContainers value) { - assertEquals(value.decimalArray[0], new BigDecimal("33.5")); - assertEquals(value.decimals.get("a"), new BigDecimal("33.5")); - assertEquals( - Float.floatToRawIntBits(value.floatArray[0].floatValue()), Float.floatToRawIntBits(11.5f)); - assertEquals( - Float.floatToRawIntBits(value.floats.get(0).floatValue()), Float.floatToRawIntBits(11.5f)); - assertEquals(Float.floatToRawIntBits(value.primitiveFloats[0]), Float.floatToRawIntBits(11.5f)); - } - - private static void assertCustomDirectContainers(CustomDirectContainers value) { - assertEquals(value.longs, new long[] {7L}); - assertEquals(value.names, Arrays.asList("decoded")); - assertEquals(value.strings, new String[] {"decoded"}); - } - private static void assertGeneratedFloatingFields(GeneratedFloatingFields value) { assertEquals( Double.doubleToRawLongBits(value.doubleBoxed.doubleValue()), diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java index 8e89090528..80d2393797 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java @@ -37,7 +37,6 @@ import org.apache.fory.serializer.StringSerializer; final class JsonTestSupport { - private static final String GENERATED_CODEC_SUFFIX = "ForyJsonCodec"; private static final JsonConfig CONFIG = new JsonConfig( false, @@ -55,7 +54,6 @@ final class JsonTestSupport { Collections., Class>emptyMap(), new JsonCodecFactory[0], Collections.emptyList(), - Collections.emptyList(), null); private static final JsonSharedRegistry REGISTRY = new JsonSharedRegistry(CONFIG); private static final JsonValueCodec NULL_CODEC = @@ -210,19 +208,6 @@ static Class generatedUtf8WriterClass(ForyJson json, TypeRef type) { return codec.getClass(); } - static String generatedCodecIdentity(Class generatedClass) { - String simpleName = generatedClass.getSimpleName(); - int suffixStart = simpleName.lastIndexOf(GENERATED_CODEC_SUFFIX + "_"); - if (suffixStart < 0) { - throw new AssertionError("Unexpected generated class " + generatedClass.getName()); - } - String identity = simpleName.substring(suffixStart + GENERATED_CODEC_SUFFIX.length() + 1); - if (!identity.matches("[0-9a-f]{64}")) { - throw new AssertionError("Unexpected generated class " + generatedClass.getName()); - } - return identity; - } - static String stringReaderPath(String input) { return StringSerializer.isBytesBackedString() && StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(input)) diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java index 86be77de6b..a91950f60e 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java @@ -27,13 +27,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.apache.fory.exception.InsecureException; import org.apache.fory.json.annotation.JsonCodec; -import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.data.Kind; import org.apache.fory.reflect.TypeRef; import org.apache.fory.type.Float16; @@ -41,8 +39,6 @@ import org.testng.annotations.Test; public class JsonTypeCheckerTest extends ForyJsonTestModels { - private static final JsonValueCodec STRING_NULL_CODEC = nullCodec(); - @Factory(dataProvider = "enableCodegen") public JsonTypeCheckerTest(boolean codegen) { super(codegen); @@ -93,10 +89,10 @@ public void defaultExactSkipsChecker() { public void customExactUsesChecker() { ForyJson json = newJsonBuilder() - .registerCodec(String.class, STRING_NULL_CODEC) - .withTypeChecker((className, context) -> !className.equals(String.class.getName())) + .registerCodec(CheckedBean.class, nullCodec()) + .withTypeChecker((className, context) -> !className.equals(CheckedBean.class.getName())) .build(); - assertThrows(InsecureException.class, () -> json.toJson("value")); + assertThrows(InsecureException.class, () -> json.toJson(new CheckedBean())); } @Test @@ -109,29 +105,13 @@ public void annotatedExactUsesChecker() { } @Test - public void mapKeyIgnoresValueCodec() { + public void customCodecUsesChecker() { ForyJson json = newJsonBuilder() - .registerCodec(String.class, STRING_NULL_CODEC) - .withTypeChecker((className, context) -> !className.equals(String.class.getName())) + .registerCodec(CheckedBean.class, nullCodec()) + .withTypeChecker((className, context) -> !className.equals(CheckedBean.class.getName())) .build(); - StringKeyMap value = new StringKeyMap(); - value.values = new LinkedHashMap<>(); - value.values.put("key", 1); - assertEquals(json.toJson(value), "{\"values\":{\"key\":1}}"); - assertEquals( - json.fromJson("{\"values\":{\"key\":2}}", StringKeyMap.class).values.get("key"), - Integer.valueOf(2)); - } - - @Test - public void customPrimitiveUsesChecker() { - ForyJson json = - newJsonBuilder() - .registerCodec(int.class, nullCodec()) - .withTypeChecker((className, context) -> !className.equals(int.class.getName())) - .build(); - assertThrows(InsecureException.class, () -> json.fromJson("1", int.class)); + assertThrows(InsecureException.class, () -> json.fromJson("{}", CheckedBean.class)); } @Test @@ -195,26 +175,26 @@ public void nestedFieldRejected() { } @Test - public void collectionScalarChecked() { + public void collectionCustomCodecChecked() { ForyJson json = newJsonBuilder() - .registerCodec(Integer.class, nullCodec()) - .withTypeChecker((className, context) -> !className.equals(Integer.class.getName())) + .registerCodec(CheckedBean.class, nullCodec()) + .withTypeChecker((className, context) -> !className.equals(CheckedBean.class.getName())) .build(); assertThrows( - InsecureException.class, () -> json.fromJson("[1]", new TypeRef>() {})); + InsecureException.class, () -> json.fromJson("[{}]", new TypeRef>() {})); } @Test - public void mapScalarChecked() { + public void mapCustomCodecChecked() { ForyJson json = newJsonBuilder() - .registerCodec(Integer.class, nullCodec()) - .withTypeChecker((className, context) -> !className.equals(Integer.class.getName())) + .registerCodec(CheckedBean.class, nullCodec()) + .withTypeChecker((className, context) -> !className.equals(CheckedBean.class.getName())) .build(); assertThrows( InsecureException.class, - () -> json.fromJson("{\"one\":1}", new TypeRef>() {})); + () -> json.fromJson("{\"one\":{}}", new TypeRef>() {})); } @Test @@ -263,10 +243,6 @@ public static final class RejectedHolder { public RejectedValue value; } - public static final class StringKeyMap { - public Map values; - } - public static final class AnnotatedStringHolder { public @JsonCodec(CountingStringCodec.class) String value = "value"; } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistryTest.java b/java/fory-json/src/test/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistryTest.java index 4e0ccab1d5..478bc65e55 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistryTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistryTest.java @@ -20,7 +20,6 @@ package org.apache.fory.json.resolver; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import static org.testng.Assert.expectThrows; @@ -28,38 +27,37 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; import org.apache.fory.json.annotation.JsonCreator; import org.apache.fory.json.annotation.JsonValue; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.meta.JsonFieldAccessor; -import org.apache.fory.meta.TypeExtMeta; +import org.apache.fory.json.resolver.JsonGeneratedClassRegistry.CompanionKey; import org.apache.fory.reflect.TypeRef; -import org.apache.fory.type.Types; import org.testng.annotations.Test; public class JsonGeneratedClassRegistryTest { @Test public void mergeSourceCodecs() { TypeRef type = TypeRef.of(String.class); - Map, GeneratedJsonCodec> codecs = new HashMap<>(); + CompanionKey key = new CompanionKey(type, null); + Map> codecs = new HashMap<>(); Set> added = new LinkedHashSet<>(); SourceCodec first = new SourceCodec(); SourceCodec second = new SourceCodec(); JsonGeneratedClassRegistry.mergeSourceCodecs( - Collections.singletonMap(type, first), codecs, added); + Collections.singletonMap(key, first), codecs, added); JsonGeneratedClassRegistry.mergeSourceCodecs( - Collections.singletonMap(type, second), codecs, added); - assertSame(codecs.get(type), first); - assertEquals(codecs.get(type).getClass(), SourceCodec.class); + Collections.singletonMap(key, second), codecs, added); + assertSame(codecs.get(key), first); + assertEquals(codecs.get(key).getClass(), SourceCodec.class); assertEquals(added, Collections.singleton(SourceCodec.class)); expectThrows( IllegalStateException.class, () -> JsonGeneratedClassRegistry.mergeSourceCodecs( - Collections.singletonMap(type, new OtherSourceCodec()), codecs, added)); + Collections.singletonMap(key, new OtherSourceCodec()), codecs, added)); } @Test @@ -69,46 +67,6 @@ public void validateGeneratedNonRecordCreator() throws Exception { assertTrue(codec.matchesCreator(CreatorValue.class.getConstructor(String.class))); } - @Test - public void generatedCapabilityType() { - TypeRef raw = TypeRef.of(String.class); - TypeRef nonNull = TypeRef.of(String.class, ordinary(false)); - TypeRef nullable = TypeRef.of(String.class, ordinary(true)); - assertEquals(JsonSharedRegistry.generatedCapabilityType(nonNull), raw); - assertEquals(JsonSharedRegistry.generatedCapabilityType(nullable), raw); - - assertPreserved(TypeExtMeta.of(Types.UINT8, false, false, false, false)); - assertPreserved(TypeExtMeta.of(Types.UNKNOWN, false, true, false, false)); - assertPreserved(TypeExtMeta.of(Types.UNKNOWN, false, false, true, false)); - assertPreserved(TypeExtMeta.of(Types.UNKNOWN, false, false, false, true)); - - TypeRef nullableElement = TypeRef.of(String.class, ordinary(true)); - TypeRef list = - TypeRef.ofDeclaredTypeArguments( - java.util.List.class, - ordinary(false), - Collections.singletonList(nullableElement), - null); - TypeRef generated = JsonSharedRegistry.generatedCapabilityType(list); - assertEquals(generated.getTypeArguments().get(0), nullableElement); - assertNotEquals(generated, TypeRef.of(list.getType())); - - TypeRef key = TypeRef.of(Integer.class, ordinary(false)); - TypeRef value = TypeRef.of(String.class, ordinary(true)); - List> mapArguments = java.util.Arrays.asList(key, value); - TypeRef map = TypeRef.ofDeclaredTypeArguments(Map.class, ordinary(true), mapArguments, null); - assertEquals(JsonSharedRegistry.generatedCapabilityType(map).getTypeArguments(), mapArguments); - } - - private static void assertPreserved(TypeExtMeta metadata) { - TypeRef type = TypeRef.of(String.class, metadata); - assertSame(JsonSharedRegistry.generatedCapabilityType(type), type); - } - - private static TypeExtMeta ordinary(boolean nullable) { - return TypeExtMeta.of(Types.UNKNOWN, nullable, false, false, false); - } - private static class SourceCodec extends GeneratedJsonCodec { @Override public Class type() { From 261edaf5412446e47ff68d03bf7dc38aebbd9c99 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 01:08:37 +0800 Subject: [PATCH 03/11] docs(json): clarify protected codec registration --- docs/json/custom-codecs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json/custom-codecs.md b/docs/json/custom-codecs.md index 85eb1c11c1..f4aeb32b20 100644 --- a/docs/json/custom-codecs.md +++ b/docs/json/custom-codecs.md @@ -72,7 +72,7 @@ ForyJson json = .build(); ``` -Exact `registerCodec` and exact-class factory registration are not supported for types with +Exact `registerCodec` and exact-class factory registration are not allowed for types with dedicated reader/writer operations: - `boolean`, `byte`, `short`, `int`, `long`, `float`, `double`, and `char`, including their boxed From f10ce5b3f32c8977fde1db33e4f60d3eba18250f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 02:02:59 +0800 Subject: [PATCH 04/11] fix(json): close generated codec cache review gaps --- .../apache/fory/graalvm/ForyJsonExample.java | 16 +- .../graalvm/closed/ClosedJsonConfigs.java | 3 + .../apache/fory/json/codec/ArrayCodec.java | 9 + .../apache/fory/json/codegen/JsonCodegen.java | 180 ++++++-- .../fory/json/resolver/CodecRegistry.java | 2 +- .../json/resolver/JsonSharedRegistry.java | 10 +- .../fory/json/resolver/JsonTypeResolver.java | 19 +- .../json/JsonGeneratedCapabilityKeyTest.java | 386 ++++++++++++++++++ 8 files changed, 575 insertions(+), 50 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 6276badc93..bd6b9cd29e 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 @@ -28,6 +28,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; @@ -132,13 +133,14 @@ private static void testHostedCodegenConfigurations() { ForyJson interpretedJson = newInterpretedJson(); exerciseCodegenConfiguration(DEFAULT_JSON, true, true); exerciseCodegenConfiguration(providerJson, true, true); - exerciseCodegenConfiguration(interpretedJson, false, true); + exerciseCodegenConfiguration(interpretedJson, false, false); testEmptyMixin(providerJson, true, true); - testEmptyMixin(interpretedJson, false, true); + testEmptyMixin(interpretedJson, false, false); testInterpretedMetadata(interpretedJson); testPrimitiveProperties(interpretedJson); testIndependentChildCodegen(); testExternalModuleMixin(); + testBootstrapMixin(providerJson); } private static ForyJson newProviderJson() { @@ -148,9 +150,16 @@ private static ForyJson newProviderJson() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) + .registerMixin(SimpleEntryMixin.class) .build(); } + private static void testBootstrapMixin(ForyJson json) { + SimpleEntry value = new SimpleEntry<>("left", "right"); + String encoded = json.toJson(value); + Preconditions.checkArgument(encoded.contains("left") && encoded.contains("right")); + } + private static ForyJson newInterpretedJson() { return ForyJson.builder() .withPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) @@ -719,6 +728,9 @@ public EmptyMixinTarget() {} @JsonMixin(target = EmptyMixinTarget.class) public interface EmptyMixin {} + @JsonMixin(target = SimpleEntry.class) + public interface SimpleEntryMixin {} + public static final class InterpretedMixinTarget { private String name; 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 d49898f505..4dbd3876e7 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 @@ -24,6 +24,7 @@ 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.graalvm.ForyJsonExample.SimpleEntryMixin; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; import org.apache.fory.json.annotation.ForyJsonProvider; @@ -42,6 +43,7 @@ public ForyJson aRestrictedConfiguration() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) + .registerMixin(SimpleEntryMixin.class) .withTypeChecker((className, context) -> false) .build(); } @@ -53,6 +55,7 @@ public ForyJson generatedConfiguration() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) + .registerMixin(SimpleEntryMixin.class) .build(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index cf3499eec8..22ec1218e4 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -175,6 +175,15 @@ private static ArrayCodec bind(ArrayCodec codec) { return (ArrayCodec) codec; } + /** Returns whether {@code codec} is the canonical protected exact-array implementation. */ + @Internal + public static boolean isCanonicalProtectedCodec(Class arrayType, Object codec) { + return arrayType == byte[].class && codec == ByteArrayCodec.INSTANCE + || arrayType == String[].class + && (codec == StringArrayCodec.INSTANCE || codec == StringArrayCodec.NON_NULL) + || arrayType == long[].class && codec == LongArrayCodec.INSTANCE; + } + // Package visibility lets Java 8 nested codecs call these helpers without synthetic accessors. static void reserveReferenceBatch(JsonReader reader, int size) { // Reserve each batch before reading its final element. This bounds unreserved reference storage 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 41ce70eb7e..4e200bf895 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 @@ -19,6 +19,7 @@ package org.apache.fory.json.codegen; +import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -94,6 +95,9 @@ public final class JsonCodegen { private final CodeGenerator codeGenerator; private final ClassLoader jsonLoader; private final boolean hostedCodegen; + // Hosted visibility must be checked against the loader that will own the defined class, not the + // composed loader that Janino uses to read source dependencies. + private final Class hostedDefinitionOwner; private final String generatedClassName; static String generatedCodecType(CodegenContext ctx, Class codecType) { @@ -108,17 +112,19 @@ static String generatedCodecArrayType(CodegenContext ctx, Class arrayType) { } public JsonCodegen(boolean hostedCodegen) { - this(null, null, hostedCodegen, null); + this(null, null, hostedCodegen, null, null); } private JsonCodegen( CodeGenerator codeGenerator, ClassLoader jsonLoader, boolean hostedCodegen, + Class hostedDefinitionOwner, String generatedClassName) { this.jsonLoader = jsonLoader; this.hostedCodegen = hostedCodegen; this.codeGenerator = codeGenerator; + this.hostedDefinitionOwner = hostedDefinitionOwner; this.generatedClassName = generatedClassName; } @@ -247,7 +253,7 @@ private Class compile( if (completed != null) { return completed; } - JsonCodegen compiler = compiler(key, entry.className); + JsonCodegen compiler = compiler(key, entry.className, generatedPackage); Class generatedClass = operation.compile(compiler); if (generatedClass != null) { entry.publish(generatedClass); @@ -255,7 +261,7 @@ private Class compile( return generatedClass; } - private JsonCodegen compiler(GeneratedCodecKey key, String className) { + private JsonCodegen compiler(GeneratedCodecKey key, String className, String generatedPackage) { ClassLoader[] loaders = canonicalLoaders(key); if (hostedCodegen) { // Use the canonical loader tuple only for source compilation and visibility decisions. The @@ -263,13 +269,18 @@ private JsonCodegen compiler(GeneratedCodecKey key, String className) { // loader cannot become reachable from the frozen Native Image registry. ClassLoader loader = loaders.length == 1 ? loaders[0] : new ClassLoaderUtils.ComposedClassLoader(loaders); - return new JsonCodegen(null, loader, true, className); + return new JsonCodegen( + null, + loader, + true, + hostedDefinitionOwner(key.targetClass(), generatedPackage), + className); } CodeGenerator generator = loaders.length == 1 ? CodeGenerator.getSharedCodeGenerator(loaders[0]) : CodeGenerator.getSharedCodeGenerator(loaders); - return new JsonCodegen(generator, generator.getClassLoader(), false, className); + return new JsonCodegen(generator, generator.getClassLoader(), false, null, className); } private ClassLoader[] canonicalLoaders(GeneratedCodecKey key) { @@ -968,11 +979,9 @@ private Class compileObjectCodecClass( return compileCodecClass(generatedPackage, className, code, invocations); } try { - // Hosted classes live beside their source owner. Concealed models need that placement for - // member access; exported models use it as well so the transient canonical compilation - // loader cannot become reachable through the generated Class mirror in the image heap. CompileUnit unit = new CompileUnit(generatedPackage, className, code); - return compileHostedClass(ownerType, unit, invocations); + return compileHostedClass( + hostedDefinitionOwner(ownerType, generatedPackage), unit, invocations); } catch (Throwable e) { throw new ForyJsonException("Cannot compile generated JSON codec " + className, e); } @@ -998,21 +1007,24 @@ private Class compileCollectionCodecClass( if (!hostedCodegen) { return compileCodecClass(generatedPackage, className, code); } - Class definitionOwner = elementType; - while (definitionOwner.isArray()) { - definitionOwner = definitionOwner.getComponentType(); - } - if (definitionOwner.isPrimitive() - || definitionOwner.getClassLoader() == null - || !CodeGenerator.getPackage(definitionOwner).equals(generatedPackage)) { - definitionOwner = Generated.class; - } return compileHostedClass( - definitionOwner, + hostedDefinitionOwner(elementType, generatedPackage), new CompileUnit(generatedPackage, className, code), new DirectInvocation[0]); } + private static Class hostedDefinitionOwner(Class sourceOwner, String generatedPackage) { + while (sourceOwner.isArray()) { + sourceOwner = sourceOwner.getComponentType(); + } + if (sourceOwner.isPrimitive() + || sourceOwner.getClassLoader() == null + || !CodeGenerator.getPackage(sourceOwner).equals(generatedPackage)) { + return Generated.class; + } + return sourceOwner; + } + private Class compileHostedClass( Class ownerType, CompileUnit unit, DirectInvocation[] invocations) { Map classes = JaninoUtils.toBytecode(jsonLoader, "", unit); @@ -1069,7 +1081,8 @@ public boolean canCompileWriter(ObjectCodec codec) { /** Checks source visibility through the same canonical loader tuple used by compilation. */ @Internal public boolean canCompileWriter(GeneratedCodecKey key, ObjectCodec codec) { - return compiler(key, "ForyJsonCodecProbe").canCompileWriter(codec); + return compiler(key, "ForyJsonCodecProbe", CodeGenerator.getPackage(codec.type())) + .canCompileWriter(codec); } private boolean canCompileUnwrappedWrite( @@ -1085,7 +1098,7 @@ private boolean canCompileUnwrappedWrite( if (getter != null && !canCall(getter)) { return false; } - if (!isVisible(entry.group().childCodec().type()) + if (!isGeneratedClassVisible(entry.group().childCodec().type()) || !canCompileUnwrappedWrite(owner, entry.group().writeEntries())) { return false; } @@ -1102,11 +1115,21 @@ public boolean canCompileReader(ObjectCodec codec) { } JsonCreatorInfo creator = codec.creatorInfo(); if (creator != null) { + if (!canResolveExecutable(creator.invocationExecutable()) + || creator.defaultConstructor() != null + && !canResolveExecutable(creator.defaultConstructor())) { + return false; + } for (Class parameterType : creator.executable().getParameterTypes()) { if (!canCompileType(parameterType)) { return false; } } + for (JsonCreatorFieldInfo field : creator.fields()) { + if (!canCompileUnboxed(field.unboxedValueCodec(), true)) { + return false; + } + } } JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -1125,7 +1148,8 @@ public boolean canCompileReader(ObjectCodec codec) { /** Checks source visibility through the same canonical loader tuple used by compilation. */ @Internal public boolean canCompileReader(GeneratedCodecKey key, ObjectCodec codec) { - return compiler(key, "ForyJsonCodecProbe").canCompileReader(codec); + return compiler(key, "ForyJsonCodecProbe", CodeGenerator.getPackage(codec.type())) + .canCompileReader(codec); } private boolean canCompileUnwrappedRead(ObjectCodec owner, JsonUnwrappedInfo unwrapped) { @@ -1141,16 +1165,26 @@ private boolean canCompileUnwrappedRead(ObjectCodec owner, JsonUnwrappedInfo if (setter != null && !canCall(setter)) { return false; } - if (!isVisible(group.childCodec().type())) { + if (!isGeneratedClassVisible(group.childCodec().type())) { return false; } JsonCreatorInfo creator = group.childCodec().creatorInfo(); if (creator != null) { + if (!canResolveExecutable(creator.invocationExecutable()) + || creator.defaultConstructor() != null + && !canResolveExecutable(creator.defaultConstructor())) { + return false; + } for (Class parameterType : creator.executable().getParameterTypes()) { if (!canCompileType(parameterType)) { return false; } } + for (JsonCreatorFieldInfo field : creator.fields()) { + if (!canCompileUnboxed(field.unboxedValueCodec(), true)) { + return false; + } + } } } for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { @@ -1159,8 +1193,11 @@ private boolean canCompileUnwrappedRead(ObjectCodec owner, JsonUnwrappedInfo return false; } JsonCreatorFieldInfo creatorField = route.creatorField(); - if (creatorField != null && !canCompileType(creatorField.rawType())) { - return false; + if (creatorField != null) { + if (!canCompileType(creatorField.rawType()) + || !canCompileUnboxed(creatorField.unboxedValueCodec(), true)) { + return false; + } } } AnyInfo any = owner.anyInfo(); @@ -1180,7 +1217,7 @@ private boolean canCompileAnyWrite(AnyInfo any) { return false; } Class mapType = getter == null ? field.getType() : getter.getReturnType(); - return isVisible(mapType) && isVisible(any.valueRawType()); + return isGeneratedClassVisible(mapType) && isGeneratedClassVisible(any.valueRawType()); } private boolean canCompileAnyRead(AnyInfo any, boolean creator) { @@ -1194,7 +1231,7 @@ private boolean canCompileAnyRead(AnyInfo any, boolean creator) { if (setter != null && (!canCall(setter) || !canCompileType(setter.getParameterTypes()[1]))) { return false; } - if (field != null && !isVisible(field.getType())) { + if (field != null && !isGeneratedClassVisible(field.getType())) { return false; } if (field != null && !canCompileField(field)) { @@ -1203,7 +1240,7 @@ private boolean canCompileAnyRead(AnyInfo any, boolean creator) { if (setter != null && creator) { return false; } - return isVisible(any.valueRawType()); + return isGeneratedClassVisible(any.valueRawType()); } Class stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1215,7 +1252,7 @@ Class stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) } Object codec = typeInfo.stringWriter(); Class type = codec.getClass(); - if (isPublicSourceType(type) && isVisible(type)) { + if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { return type; } return StringWriterCodec.class; @@ -1233,7 +1270,7 @@ Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { } Object codec = typeInfo.utf8Writer(); Class type = codec.getClass(); - if (isPublicSourceType(type) && isVisible(type)) { + if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { return type; } return Utf8WriterCodec.class; @@ -1247,7 +1284,7 @@ Class latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) return Latin1ReaderCodec.class; } Class type = typeInfo.latin1Reader().getClass(); - if (isPublicSourceType(type) && isVisible(type)) { + if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { return type; } return Latin1ReaderCodec.class; @@ -1261,7 +1298,7 @@ Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) return Utf16ReaderCodec.class; } Class type = typeInfo.utf16Reader().getClass(); - if (isPublicSourceType(type) && isVisible(type)) { + if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { return type; } return Utf16ReaderCodec.class; @@ -1278,7 +1315,7 @@ Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { return Utf8ReaderCodec.class; } Class type = typeInfo.utf8Reader().getClass(); - if (isPublicSourceType(type) && isVisible(type)) { + if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { return type; } return Utf8ReaderCodec.class; @@ -1343,10 +1380,10 @@ private boolean canCompileWrite(JsonFieldInfo property) { return false; } Class rawType = property.writeRawType(); - if (rawType != null && !rawType.isPrimitive() && !isVisible(rawType)) { + if (rawType != null && !rawType.isPrimitive() && !isGeneratedClassVisible(rawType)) { return false; } - return true; + return canCompileUnboxed(property.writeUnboxedValueCodec(), false); } private boolean canCompileRead(JsonFieldInfo property) { @@ -1365,14 +1402,14 @@ private boolean canCompileRead(JsonFieldInfo property) { return false; } Class rawType = property.readRawType(); - if (rawType != null && !rawType.isPrimitive() && !isVisible(rawType)) { + if (rawType != null && !rawType.isPrimitive() && !isGeneratedClassVisible(rawType)) { return false; } - return true; + return canCompileUnboxed(property.readUnboxedValueCodec(), true); } private boolean canCompileType(Class type) { - return isPublicSourceType(type) && isVisible(type); + return isPublicSourceType(type) && isGeneratedClassVisible(type); } private boolean canCompileField(Field field) { @@ -1384,7 +1421,72 @@ private boolean canCompileField(Field field) { private boolean canCall(Method method) { return Modifier.isPublic(method.getModifiers()) - && isPublicSourceType(method.getDeclaringClass()); + && isPublicSourceType(method.getDeclaringClass()) + && isGeneratedClassVisible(method.getDeclaringClass()) + && canResolveExecutable(method); + } + + private boolean canCompileUnboxed(UnboxedValueCodec codec, boolean reader) { + if (!hostedCodegen || codec == null) { + return true; + } + if (codec instanceof DirectUnboxedValueCodec) { + DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) codec; + return canResolveExecutable( + reader ? direct.readCarrierMethod() : direct.writeCarrierMethod()); + } + TransparentUnboxedValueCodec transparent = (TransparentUnboxedValueCodec) codec; + Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); + for (Method method : methods) { + if (!canResolveExecutable(method)) { + return false; + } + } + UnboxedValueCodec terminal = transparent.valueTypeInfo().unboxedValueCodec(); + if (terminal instanceof DirectUnboxedValueCodec) { + DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) terminal; + return canResolveExecutable( + reader ? direct.readCarrierMethod() : direct.writeCarrierMethod()); + } + return true; + } + + private boolean canResolveExecutable(Executable executable) { + if (!hostedCodegen || !isDefinitionVisible(executable.getDeclaringClass())) { + return !hostedCodegen; + } + if (executable instanceof Method + && !isDefinitionVisible(((Method) executable).getReturnType())) { + return false; + } + for (Class parameterType : executable.getParameterTypes()) { + if (!isDefinitionVisible(parameterType)) { + return false; + } + } + return true; + } + + private boolean isGeneratedClassVisible(Class type) { + return isVisible(type) && isDefinitionVisible(type); + } + + private boolean isDefinitionVisible(Class type) { + if (!hostedCodegen || type.isPrimitive()) { + return true; + } + while (type.isArray()) { + type = type.getComponentType(); + } + if (type.isPrimitive()) { + return true; + } + ClassLoader loader = hostedDefinitionOwner.getClassLoader(); + try { + return Class.forName(type.getName(), false, loader) == type; + } catch (ClassNotFoundException e) { + return false; + } } private boolean isVisible(Class type) { 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 562b2451fc..265095beb2 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 @@ -190,7 +190,7 @@ private static Set> protectedBuiltinTypes() { private static void checkRegistrationType(Class type) { if (isProtectedBuiltinType(type)) { throw new IllegalArgumentException( - "JSON codec registration is not supported for built-in type " + type.getTypeName()); + "JSON codec registration is not allowed for built-in type " + type.getTypeName()); } } 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 57f6d7a99e..57130ea2d4 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 @@ -231,8 +231,8 @@ private JsonSharedRegistry( } } } - // 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 + // Hosted compilation shares classes only for equal generated-class keys. Runtime type policy + // is intentionally not part of that key and remains enforced by each // runtime resolver before it installs a generated capability. typeChecker = hostedCodegen ? null : config.typeChecker(); typeCheckContext = hostedCodegen ? null : config.typeCheckContext(); @@ -1217,8 +1217,10 @@ Class mixinType(Class targetType) { } boolean canonicalProtectedBuiltin(JsonTypeInfo typeInfo, Object capability) { - return CodecRegistry.isProtectedBuiltinType(typeInfo.rawType()) - && exactCodecs.get(typeInfo.rawType()) == capability; + Class rawType = typeInfo.rawType(); + return CodecRegistry.isProtectedBuiltinType(rawType) + && (exactCodecs.get(rawType) == capability + || ArrayCodec.isCanonicalProtectedCodec(rawType, capability)); } /** Adds exact pair context to a cold effective-schema validation failure. */ 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 01370209b9..789ebfbb35 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 @@ -1749,9 +1749,7 @@ GeneratedCodecKey generatedObjectKey( JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { ArrayList projection = new ArrayList<>(); ArrayList> classes = new ArrayList<>(); - if (!readerKind(kind)) { - projection.add(sharedRegistry.writeNullFields()); - } + projection.add(sharedRegistry.writeNullFields()); projection.add(sharedRegistry.propertyDiscoveryEnabled()); projection.add(sharedRegistry.propertyNamingStrategy()); addMixinProjection(owner.type(), projection, classes); @@ -1997,7 +1995,12 @@ private void addAnyProjection( ArrayList> classes, CapabilityKind kind) { AnyInfo any = owner.anyInfo(); - if (any == null) { + boolean active = + any != null + && (readerKind(kind) + ? any.readField() != null || any.readSetter() != null + : any.writeField() != null || any.writeGetter() != null); + if (!active) { projection.add(null); return; } @@ -2076,6 +2079,14 @@ private static void addUnboxedProjection( projection.add(terminal.rawType()); projection.add(terminal.kind()); addClass(terminal.rawType(), classes); + UnboxedValueCodec terminalCodec = terminal.unboxedValueCodec(); + if (terminalCodec instanceof DirectUnboxedValueCodec) { + DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) terminalCodec; + addMember( + reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), projection, classes); + } else { + projection.add(null); + } Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); projection.add(methods.length); for (Method method : methods) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java index 996ca0de13..b94f9d4042 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java @@ -21,29 +21,53 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.tools.JavaCompiler; +import javax.tools.ToolProvider; +import org.apache.fory.json.annotation.JsonAnyGetter; +import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonType; +import org.apache.fory.json.codec.AbstractJsonValueCodec; +import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.JsonObjectModel; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codec.TransparentUnboxedValueCodec; +import org.apache.fory.json.codec.UnboxedValueCodec; +import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.data.PublicFields; +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; import org.apache.fory.json.resolver.JsonSharedRegistry; import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; +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.meta.TypeExtMeta; @@ -210,6 +234,125 @@ public void configuredLoaderDoesNotVersionClass() { assertObjectClasses(firstType, secondType); } + @Test + public void rawWriteNullVersionsReaders() { + ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); + ForyJson second = ForyJson.builder().writeNullFields(true).withAsyncCompilation(false).build(); + assertDifferentObjectClasses( + JsonTestSupport.currentTypeResolver(first).getTypeInfo(Model.class, Model.class), + JsonTestSupport.currentTypeResolver(second).getTypeInfo(Model.class, Model.class)); + } + + @Test + public void inactiveAnyDirectionReusesClass() { + JsonTypeInfo getterA = anyType(GetterAny.class, new ChildCodecA()); + JsonTypeInfo getterB = anyType(GetterAny.class, new ChildCodecB()); + assertNotSame(getterA.stringWriter().getClass(), getterB.stringWriter().getClass()); + assertNotSame(getterA.utf8Writer().getClass(), getterB.utf8Writer().getClass()); + assertSame(getterA.latin1Reader().getClass(), getterB.latin1Reader().getClass()); + assertSame(getterA.utf16Reader().getClass(), getterB.utf16Reader().getClass()); + assertSame(getterA.utf8Reader().getClass(), getterB.utf8Reader().getClass()); + + JsonTypeInfo setterA = anyType(SetterAny.class, new ChildCodecA()); + JsonTypeInfo setterB = anyType(SetterAny.class, new ChildCodecB()); + assertSame(setterA.stringWriter().getClass(), setterB.stringWriter().getClass()); + assertSame(setterA.utf8Writer().getClass(), setterB.utf8Writer().getClass()); + assertNotSame(setterA.latin1Reader().getClass(), setterB.latin1Reader().getClass()); + assertNotSame(setterA.utf16Reader().getClass(), setterB.utf16Reader().getClass()); + assertNotSame(setterA.utf8Reader().getClass(), setterB.utf8Reader().getClass()); + } + + @Test + public void terminalDirectMethodsVersionProjection() throws Exception { + JsonTypeInfo first = directTerminal(new VariableDirectCodec(false)); + JsonTypeInfo second = directTerminal(new VariableDirectCodec(true)); + ProjectionTransparentCodec firstCodec = new ProjectionTransparentCodec(first); + ProjectionTransparentCodec secondCodec = new ProjectionTransparentCodec(second); + + assertNotEquals(unboxedProjection(firstCodec, false), unboxedProjection(secondCodec, false)); + assertNotEquals(unboxedProjection(firstCodec, true), unboxedProjection(secondCodec, true)); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void hostedSiblingCodecUsesInterface() throws Exception { + String packageName = "org.apache.fory.json.sibling"; + Path targetOutput = Files.createTempDirectory("fory-json-sibling-target"); + compileSource( + targetOutput, + packageName, + "SiblingModel", + "public final class SiblingModel { public " + Child.class.getCanonicalName() + " child; }"); + Path codecOutput = Files.createTempDirectory("fory-json-sibling-codec"); + compileSource( + codecOutput, + packageName, + "SiblingChildCodec", + "public final class SiblingChildCodec extends " + + ChildCodecA.class.getCanonicalName() + + " {}"); + try (URLClassLoader targetLoader = + new URLClassLoader( + new URL[] {targetOutput.toUri().toURL()}, getClass().getClassLoader()); + URLClassLoader codecLoader = + new URLClassLoader( + new URL[] {codecOutput.toUri().toURL()}, getClass().getClassLoader())) { + Class target = Class.forName(packageName + ".SiblingModel", true, targetLoader); + Class codecType = Class.forName(packageName + ".SiblingChildCodec", true, codecLoader); + JsonValueCodec codec = + (JsonValueCodec) codecType.getDeclaredConstructor().newInstance(); + ForyJson configured = parentJson(codec); + JsonTypeResolver resolver = hostedResolver(configured); + List> models = resolver.generateHostedCodecs(target); + assertTrue(models.stream().anyMatch(model -> model.type() == target)); + } + } + + @Test + public void concurrentInstancesShareFirstClass() throws Exception { + ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); + ForyJson second = ForyJson.builder().withAsyncCompilation(false).build(); + CountDownLatch start = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + Future firstType = + executor.submit( + () -> { + start.await(); + return JsonTestSupport.currentTypeResolver(first) + .getTypeInfo(Model.class, Model.class); + }); + Future secondType = + executor.submit( + () -> { + start.await(); + return JsonTestSupport.currentTypeResolver(second) + .getTypeInfo(Model.class, Model.class); + }); + start.countDown(); + assertObjectClasses(firstType.get(), secondType.get()); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void cacheResetAllowsEquivalentClass() { + ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeInfo firstType = + JsonTestSupport.currentTypeResolver(first).getTypeInfo(Model.class, Model.class); + JsonCodegen.resetGeneratedClassCache(); + ForyJson second = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeInfo secondType = + JsonTestSupport.currentTypeResolver(second).getTypeInfo(Model.class, Model.class); + assertDifferentObjectClasses(firstType, secondType); + + Model value = new Model(); + value.value = "retained"; + assertEquals(first.fromJson(first.toJson(value), Model.class).value, "retained"); + assertEquals(second.fromJson(second.toJson(value), Model.class).value, "retained"); + } + @Test public void collectionClassIgnoresElementCodec() { JsonTypeInfo first = collectionType(new ChildCodecA()); @@ -263,6 +406,49 @@ private static JsonTypeInfo collectionType(JsonValueCodec codec) { return JsonTestSupport.currentTypeResolver(json).getTypeInfo(new TypeRef>() {}); } + @SuppressWarnings({"rawtypes", "unchecked"}) + private static JsonTypeInfo anyType(Class type, JsonValueCodec codec) { + ForyJson json = parentJson(codec); + return JsonTestSupport.currentTypeResolver(json).getTypeInfo((Class) type, type); + } + + private static JsonTypeResolver hostedResolver(ForyJson json) throws Exception { + Constructor constructor = + JsonSharedRegistry.class.getDeclaredConstructor( + JsonConfig.class, ExecutorService.class, boolean.class); + constructor.setAccessible(true); + return new JsonTypeResolver(constructor.newInstance(json.config(), null, true)); + } + + private static JsonTypeInfo directTerminal(VariableDirectCodec codec) { + JsonCodecFactory factory = + (type, resolver, runtimeType) -> + type.getRawType() == int.class + && type.getTypeExtMeta() != null + && type.getTypeExtMeta().typeId() == Types.UINT32 + ? codec + : null; + ForyJson json = + ForyJson.builder().withModule(context -> context.registerCodecFactory(factory)).build(); + return JsonTestSupport.currentTypeResolver(json) + .getTypeInfo(TypeRef.of(int.class, TypeExtMeta.of(Types.UINT32, false, false))); + } + + private static List unboxedProjection(UnboxedValueCodec codec, boolean reader) + throws Exception { + Method method = + JsonTypeResolver.class.getDeclaredMethod( + "addUnboxedProjection", + UnboxedValueCodec.class, + boolean.class, + ArrayList.class, + ArrayList.class); + method.setAccessible(true); + ArrayList projection = new ArrayList<>(); + method.invoke(null, codec, reader, projection, new ArrayList>()); + return projection; + } + @SuppressWarnings({"rawtypes", "unchecked"}) private static JsonTypeInfo loaderType(Class type) { ForyJson json = @@ -311,6 +497,26 @@ private static byte[] classBytes(Class type) throws IOException { } } + private static void compileSource( + Path output, String packageName, String simpleName, String declaration) throws IOException { + Path source = output.resolve(simpleName + ".java"); + Files.write( + source, ("package " + packageName + "; " + declaration).getBytes(StandardCharsets.UTF_8)); + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertEquals( + compiler.run( + null, + null, + null, + "-proc:none", + "-classpath", + System.getProperty("java.class.path"), + "-d", + output.toString(), + source.toString()), + 0); + } + private static TypeExtMeta ordinary(boolean nullable) { return TypeExtMeta.of(Types.UNKNOWN, nullable, false, false, false); } @@ -361,6 +567,24 @@ public static final class Parent { public Parent() {} } + public static final class GetterAny { + private final Map values = new LinkedHashMap<>(); + + @JsonAnyGetter + public Map values() { + return values; + } + } + + public static final class SetterAny { + private final Map values = new LinkedHashMap<>(); + + @JsonAnySetter + public void put(String name, Child value) { + values.put(name, value); + } + } + public static final class Child { public String value; @@ -441,6 +665,168 @@ private Child childWithoutPrefix(String text) { } } + public static class VariableDirectCodec extends AbstractJsonValueCodec + implements DirectUnboxedValueCodec { + private final boolean alternate; + + public VariableDirectCodec(boolean alternate) { + this.alternate = alternate; + } + + @Override + public void write(JsonWriter writer, Integer value) { + writer.writeInt(value); + } + + @Override + public Integer read(JsonReader reader) { + return reader.readInt(); + } + + @Override + public Class carrierType() { + return int.class; + } + + @Override + public Object readLatin1Carrier(Latin1JsonReader reader) { + return reader.readInt(); + } + + @Override + public Object readUtf16Carrier(Utf16JsonReader reader) { + return reader.readInt(); + } + + @Override + public Object readUtf8Carrier(Utf8JsonReader reader) { + return reader.readInt(); + } + + @Override + public void writeStringCarrier(StringJsonWriter writer, Object carrier) { + writer.writeInt((Integer) carrier); + } + + @Override + public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { + writer.writeInt((Integer) carrier); + } + + @Override + public Method readCarrierMethod() { + return method(alternate ? "readSecond" : "readFirst", JsonReader.class); + } + + @Override + public Method writeCarrierMethod() { + return method(alternate ? "writeSecond" : "writeFirst", JsonWriter.class, int.class); + } + + public static int readFirst(JsonReader reader) { + return reader.readInt(); + } + + public static int readSecond(JsonReader reader) { + return reader.readInt(); + } + + public static void writeFirst(JsonWriter writer, int value) { + writer.writeInt(value); + } + + public static void writeSecond(JsonWriter writer, int value) { + writer.writeInt(value); + } + + private static Method method(String name, Class... parameters) { + try { + return VariableDirectCodec.class.getMethod(name, parameters); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + } + + public static final class ProjectionTransparentCodec extends AbstractJsonValueCodec + implements TransparentUnboxedValueCodec { + private final JsonTypeInfo valueTypeInfo; + + public ProjectionTransparentCodec(JsonTypeInfo valueTypeInfo) { + this.valueTypeInfo = valueTypeInfo; + } + + @Override + public JsonTypeInfo valueTypeInfo() { + return valueTypeInfo; + } + + @Override + public Object constructCarrier(JsonReader reader, Object value) { + return value; + } + + @Override + public Object extractValue(Object carrier) { + return carrier; + } + + @Override + public Method[] constructMethods() { + return new Method[0]; + } + + @Override + public int[] constructBoxBytes() { + return new int[0]; + } + + @Override + public Method[] extractMethods() { + return new Method[0]; + } + + @Override + public void write(JsonWriter writer, Integer value) { + writer.writeInt(value); + } + + @Override + public Integer read(JsonReader reader) { + return reader.readInt(); + } + + @Override + public Class carrierType() { + return int.class; + } + + @Override + public Object readLatin1Carrier(Latin1JsonReader reader) { + return reader.readInt(); + } + + @Override + public Object readUtf16Carrier(Utf16JsonReader reader) { + return reader.readInt(); + } + + @Override + public Object readUtf8Carrier(Utf8JsonReader reader) { + return reader.readInt(); + } + + @Override + public void writeStringCarrier(StringJsonWriter writer, Object carrier) { + writer.writeInt((Integer) carrier); + } + + @Override + public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { + writer.writeInt((Integer) carrier); + } + } + public static final class Unrelated {} public static final class Box { From f632e3d956e9239ac0330312c35919a1dd44e334 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 02:29:06 +0800 Subject: [PATCH 05/11] fix(json): validate hosted codec definition context --- .../apache/fory/graalvm/ForyJsonExample.java | 55 +++- .../graalvm/closed/ClosedJsonConfigs.java | 6 +- .../apache/fory/json/codegen/JsonCodegen.java | 125 +++++++--- .../json/JsonGeneratedCapabilityKeyTest.java | 235 +++++++++++++++++- 4 files changed, 359 insertions(+), 62 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 bd6b9cd29e..5a7cffb6c1 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 @@ -28,7 +28,6 @@ import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; @@ -150,14 +149,14 @@ private static ForyJson newProviderJson() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) - .registerMixin(SimpleEntryMixin.class) + .registerMixin(StackTraceElementMixin.class) .build(); } private static void testBootstrapMixin(ForyJson json) { - SimpleEntry value = new SimpleEntry<>("left", "right"); + StackTraceElement value = new StackTraceElement("Owner", "method", "Owner.java", 12); String encoded = json.toJson(value); - Preconditions.checkArgument(encoded.contains("left") && encoded.contains("right")); + Preconditions.checkArgument(encoded.contains("Owner") && encoded.contains("method")); } private static ForyJson newInterpretedJson() { @@ -728,8 +727,11 @@ public EmptyMixinTarget() {} @JsonMixin(target = EmptyMixinTarget.class) public interface EmptyMixin {} - @JsonMixin(target = SimpleEntry.class) - public interface SimpleEntryMixin {} + @JsonMixin(target = StackTraceElement.class) + public interface StackTraceElementMixin { + @JsonCodec(BootstrapProbeCodec.class) + String getClassName(); + } public static final class InterpretedMixinTarget { private String name; @@ -923,6 +925,47 @@ private static void checkCapability(Object capability, boolean expectGenerated) } } + public static final class BootstrapProbeCodec implements JsonValueCodec { + public BootstrapProbeCodec() {} + + @Override + public void writeString(StringJsonWriter writer, String value) { + CodegenProbeCodec.checkCapability( + writer + .typeResolver() + .getTypeInfo(StackTraceElement.class, StackTraceElement.class) + .stringWriter(), + true); + writer.writeString(value); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, String value) { + CodegenProbeCodec.checkCapability( + writer + .typeResolver() + .getTypeInfo(StackTraceElement.class, StackTraceElement.class) + .utf8Writer(), + true); + writer.writeString(value); + } + + @Override + public String readLatin1(Latin1JsonReader reader) { + return reader.readString(); + } + + @Override + public String readUtf16(Utf16JsonReader reader) { + return reader.readString(); + } + + @Override + public String readUtf8(Utf8JsonReader reader) { + return reader.readString(); + } + } + @JsonType static final class PackagePrivateOwner { public PublicChild child = new PublicChild(); 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 4dbd3876e7..6da8f73032 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 @@ -24,7 +24,7 @@ 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.graalvm.ForyJsonExample.SimpleEntryMixin; +import org.apache.fory.graalvm.ForyJsonExample.StackTraceElementMixin; import org.apache.fory.json.ForyJson; import org.apache.fory.json.PropertyNamingStrategy; import org.apache.fory.json.annotation.ForyJsonProvider; @@ -43,7 +43,7 @@ public ForyJson aRestrictedConfiguration() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) - .registerMixin(SimpleEntryMixin.class) + .registerMixin(StackTraceElementMixin.class) .withTypeChecker((className, context) -> false) .build(); } @@ -55,7 +55,7 @@ public ForyJson generatedConfiguration() { .registerCodec(CodegenProbeValue.class, new CodegenProbeCodec()) .registerMixin(CoreCompileStateMixin.class) .registerMixin(EmptyMixin.class) - .registerMixin(SimpleEntryMixin.class) + .registerMixin(StackTraceElementMixin.class) .build(); } 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 4e200bf895..a3068b218b 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 @@ -60,6 +60,7 @@ import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.resolver.JsonTypeInfo; import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.internal.DefineClass; import org.apache.fory.platform.internal._JDKAccess; import org.apache.fory.reflect.TypeRef; @@ -978,10 +979,13 @@ private Class compileObjectCodecClass( if (!hostedCodegen) { return compileCodecClass(generatedPackage, className, code, invocations); } + Class definitionOwner = hostedDefinitionOwner(ownerType, generatedPackage); + if (definitionOwner == null) { + return null; + } try { CompileUnit unit = new CompileUnit(generatedPackage, className, code); - return compileHostedClass( - hostedDefinitionOwner(ownerType, generatedPackage), unit, invocations); + return compileHostedClass(definitionOwner, unit, invocations); } catch (Throwable e) { throw new ForyJsonException("Cannot compile generated JSON codec " + className, e); } @@ -1007,8 +1011,12 @@ private Class compileCollectionCodecClass( if (!hostedCodegen) { return compileCodecClass(generatedPackage, className, code); } + Class definitionOwner = hostedDefinitionOwner(elementType, generatedPackage); + if (definitionOwner == null) { + return null; + } return compileHostedClass( - hostedDefinitionOwner(elementType, generatedPackage), + definitionOwner, new CompileUnit(generatedPackage, className, code), new DirectInvocation[0]); } @@ -1017,12 +1025,14 @@ private static Class hostedDefinitionOwner(Class sourceOwner, String gener while (sourceOwner.isArray()) { sourceOwner = sourceOwner.getComponentType(); } - if (sourceOwner.isPrimitive() - || sourceOwner.getClassLoader() == null - || !CodeGenerator.getPackage(sourceOwner).equals(generatedPackage)) { + if (sourceOwner.getClassLoader() != null + && CodeGenerator.getPackage(sourceOwner).equals(generatedPackage)) { + return sourceOwner; + } + if (CodeGenerator.getPackage(Generated.class).equals(generatedPackage)) { return Generated.class; } - return sourceOwner; + return null; } private Class compileHostedClass( @@ -1114,22 +1124,8 @@ public boolean canCompileReader(ObjectCodec codec) { return false; } JsonCreatorInfo creator = codec.creatorInfo(); - if (creator != null) { - if (!canResolveExecutable(creator.invocationExecutable()) - || creator.defaultConstructor() != null - && !canResolveExecutable(creator.defaultConstructor())) { - return false; - } - for (Class parameterType : creator.executable().getParameterTypes()) { - if (!canCompileType(parameterType)) { - return false; - } - } - for (JsonCreatorFieldInfo field : creator.fields()) { - if (!canCompileUnboxed(field.unboxedValueCodec(), true)) { - return false; - } - } + if (!canCompileCreator(creator)) { + return false; } JsonUnwrappedInfo unwrapped = codec.unwrappedInfo(); if (unwrapped != null) { @@ -1169,22 +1165,8 @@ private boolean canCompileUnwrappedRead(ObjectCodec owner, JsonUnwrappedInfo return false; } JsonCreatorInfo creator = group.childCodec().creatorInfo(); - if (creator != null) { - if (!canResolveExecutable(creator.invocationExecutable()) - || creator.defaultConstructor() != null - && !canResolveExecutable(creator.defaultConstructor())) { - return false; - } - for (Class parameterType : creator.executable().getParameterTypes()) { - if (!canCompileType(parameterType)) { - return false; - } - } - for (JsonCreatorFieldInfo field : creator.fields()) { - if (!canCompileUnboxed(field.unboxedValueCodec(), true)) { - return false; - } - } + if (!canCompileCreator(creator)) { + return false; } } for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { @@ -1204,6 +1186,36 @@ private boolean canCompileUnwrappedRead(ObjectCodec owner, JsonUnwrappedInfo return any == null || canCompileAnyRead(any, owner.creatorInfo() != null); } + private boolean canCompileCreator(JsonCreatorInfo creator) { + if (creator == null) { + return true; + } + if (!canResolveExecutable(creator.invocationExecutable()) + || creator.defaultConstructor() != null + && !canResolveExecutable(creator.defaultConstructor())) { + return false; + } + Class[] parameterTypes = creator.executable().getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + if (!canCompileType(parameterTypes[i])) { + return false; + } + Method defaultMethod = creator.defaultMethod(i); + // JsonCreatorInfo guarantees that a default method belongs to the creator owner and that its + // dependency types are the preceding creator parameters. The generated reader still invokes + // that exact method, so validate its access from the final definition context as well. + if (defaultMethod != null && !canCall(defaultMethod)) { + return false; + } + } + for (JsonCreatorFieldInfo field : creator.fields()) { + if (!canCompileUnboxed(field.unboxedValueCodec(), true)) { + return false; + } + } + return true; + } + private boolean canCompileAnyWrite(AnyInfo any) { Field field = any.writeField(); Method getter = any.writeGetter(); @@ -1436,6 +1448,9 @@ private boolean canCompileUnboxed(UnboxedValueCodec codec, boolean reader) { reader ? direct.readCarrierMethod() : direct.writeCarrierMethod()); } TransparentUnboxedValueCodec transparent = (TransparentUnboxedValueCodec) codec; + if (!canCompileType(transparent.valueTypeInfo().rawType())) { + return false; + } Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); for (Method method : methods) { if (!canResolveExecutable(method)) { @@ -1475,6 +1490,9 @@ private boolean isDefinitionVisible(Class type) { if (!hostedCodegen || type.isPrimitive()) { return true; } + if (hostedDefinitionOwner == null) { + return false; + } while (type.isArray()) { type = type.getComponentType(); } @@ -1483,10 +1501,35 @@ private boolean isDefinitionVisible(Class type) { } ClassLoader loader = hostedDefinitionOwner.getClassLoader(); try { - return Class.forName(type.getName(), false, loader) == type; - } catch (ClassNotFoundException e) { + return Class.forName(type.getName(), false, loader) == type + && isDefinitionModuleVisible(type); + } catch (ReflectiveOperationException e) { + return false; + } + } + + private boolean isDefinitionModuleVisible(Class type) throws ReflectiveOperationException { + if (JdkVersion.MAJOR_VERSION < 9) { + return true; + } + Object ownerModule = _JDKAccess.getModule(hostedDefinitionOwner); + Object typeModule = _JDKAccess.getModule(type); + if (ownerModule == typeModule) { + return true; + } + // Source compilation uses the composed loader, but the generated class belongs to the + // definition owner's module. Loader visibility alone cannot make a concealed package or an + // unread module legal at linkage time. + Class moduleType = ownerModule.getClass(); + if (!(Boolean) moduleType.getMethod("canRead", moduleType).invoke(ownerModule, typeModule)) { return false; } + Package typePackage = type.getPackage(); + String packageName = typePackage == null ? "" : typePackage.getName(); + return (Boolean) + moduleType + .getMethod("isExported", String.class, moduleType) + .invoke(typeModule, packageName, ownerModule); } private boolean isVisible(Class type) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java index b94f9d4042..2c1ba853c8 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java @@ -23,6 +23,7 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; @@ -45,8 +46,10 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import javax.security.auth.Subject; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; +import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.json.annotation.JsonAnyGetter; import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonSubTypes; @@ -71,6 +74,7 @@ import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; import org.apache.fory.meta.TypeExtMeta; +import org.apache.fory.platform.JdkVersion; import org.apache.fory.reflect.TypeRef; import org.apache.fory.serializer.StringSerializer; import org.apache.fory.type.Types; @@ -308,6 +312,75 @@ public void hostedSiblingCodecUsesInterface() throws Exception { } } + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void hostedConcealedTypeUsesInterpretedRole() throws Exception { + if (JdkVersion.MAJOR_VERSION < 9) { + return; + } + String packageName = "org.apache.fory.json.concealed"; + Path targetOutput = Files.createTempDirectory("fory-json-concealed-target"); + compileSource( + targetOutput, + packageName, + "ConcealedModel", + "public final class ConcealedModel { public jdk.internal.misc.Unsafe value; }", + "--add-exports", + "java.base/jdk.internal.misc=ALL-UNNAMED"); + try (URLClassLoader targetLoader = + new URLClassLoader(new URL[] {targetOutput.toUri().toURL()}, getClass().getClassLoader())) { + Class target = Class.forName(packageName + ".ConcealedModel", true, targetLoader); + Class concealed = Class.forName("jdk.internal.misc.Unsafe"); + ForyJson configured = + ForyJson.builder().registerCodec((Class) concealed, JsonTestSupport.nullCodec()).build(); + JsonTypeResolver resolver = hostedResolver(configured); + resolver.generateHostedCodecs(target); + assertInterpretedObject(resolver.getTypeInfo((Class) target, target)); + } + } + + @Test + public void hostedBootstrapPackageNeedsOwner() throws Exception { + Method method = + JsonCodegen.class.getDeclaredMethod("hostedDefinitionOwner", Class.class, String.class); + method.setAccessible(true); + assertNull(method.invoke(null, Subject.class, CodeGenerator.getPackage(Subject.class))); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + public void hostedTransparentTerminalUsesInterpretedRole() throws Exception { + String packageName = "org.apache.fory.json.terminal"; + Path terminalOutput = Files.createTempDirectory("fory-json-terminal"); + compileSource( + terminalOutput, + packageName, + "SiblingTerminal", + "public final class SiblingTerminal implements " + + ProjectionCarrier.class.getCanonicalName() + + " {}"); + try (URLClassLoader terminalLoader = + new URLClassLoader( + new URL[] {terminalOutput.toUri().toURL()}, getClass().getClassLoader())) { + Class terminal = Class.forName(packageName + ".SiblingTerminal", true, terminalLoader); + JsonObjectModel model = transparentModel(); + ForyJson configured = + ForyJson.builder() + .registerCodec((Class) terminal, JsonTestSupport.nullCodec()) + .registerCodec( + ProjectionValue.class, + (type, resolver, runtimeType) -> + new SiblingTransparentCodec(resolver.getTypeInfo((Class) terminal, terminal))) + .registerCodec( + TransparentModel.class, + (type, resolver, runtimeType) -> resolver.createObjectCodec(type, model)) + .build(); + JsonTypeResolver resolver = hostedResolver(configured); + resolver.generateHostedCodecs(TransparentModel.class); + assertInterpretedObject(resolver.getTypeInfo(TransparentModel.class, TransparentModel.class)); + } + } + @Test public void concurrentInstancesShareFirstClass() throws Exception { ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); @@ -499,22 +572,39 @@ private static byte[] classBytes(Class type) throws IOException { private static void compileSource( Path output, String packageName, String simpleName, String declaration) throws IOException { + compileSource(output, packageName, simpleName, declaration, new String[0]); + } + + private static void compileSource( + Path output, String packageName, String simpleName, String declaration, String... options) + throws IOException { Path source = output.resolve(simpleName + ".java"); Files.write( source, ("package " + packageName + "; " + declaration).getBytes(StandardCharsets.UTF_8)); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - assertEquals( - compiler.run( - null, - null, - null, - "-proc:none", - "-classpath", - System.getProperty("java.class.path"), - "-d", - output.toString(), - source.toString()), - 0); + ArrayList arguments = new ArrayList<>(); + Collections.addAll( + arguments, "-proc:none", "-classpath", System.getProperty("java.class.path")); + Collections.addAll(arguments, options); + Collections.addAll(arguments, "-d", output.toString(), source.toString()); + assertEquals(compiler.run(null, null, null, arguments.toArray(new String[0])), 0); + } + + private static JsonObjectModel transparentModel() throws Exception { + TypeRef logicalType = TypeRef.of(ProjectionValue.class, ordinary(false)); + return new JsonObjectModel( + TransparentModel.class.getConstructor(), + null, + new String[0], + new Method[0], + new Method[0], + new int[0], + new boolean[0], + new TypeRef[0], + new String[] {"value"}, + new Method[] {TransparentModel.class.getMethod("getValue")}, + new Method[] {TransparentModel.class.getMethod("setValue", ProjectionCarrier.class)}, + new TypeRef[] {logicalType}); } private static TypeExtMeta ordinary(boolean nullable) { @@ -555,6 +645,14 @@ private static void assertGeneratedObject(JsonTypeInfo typeInfo) { assertFalse(ObjectCodec.class.isAssignableFrom(typeInfo.utf8Reader().getClass())); } + private static void assertInterpretedObject(JsonTypeInfo typeInfo) { + assertTrue(typeInfo.stringWriter() instanceof ObjectCodec); + assertTrue(typeInfo.utf8Writer() instanceof ObjectCodec); + assertTrue(typeInfo.latin1Reader() instanceof ObjectCodec); + assertTrue(typeInfo.utf16Reader() instanceof ObjectCodec); + assertTrue(typeInfo.utf8Reader() instanceof ObjectCodec); + } + public static final class Model { public String value; @@ -591,6 +689,24 @@ public static final class Child { public Child() {} } + public interface ProjectionCarrier {} + + public static final class ProjectionValue {} + + public static final class TransparentModel { + private ProjectionCarrier value; + + public TransparentModel() {} + + public ProjectionCarrier getValue() { + return value; + } + + public void setValue(ProjectionCarrier value) { + this.value = value; + } + } + public static class ChildCodecA implements JsonValueCodec { @Override public void writeString(StringJsonWriter writer, Child value) { @@ -827,6 +943,101 @@ public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { } } + public static final class SiblingTransparentCodec extends AbstractJsonValueCodec + implements TransparentUnboxedValueCodec { + private final JsonTypeInfo valueTypeInfo; + + public SiblingTransparentCodec(JsonTypeInfo valueTypeInfo) { + this.valueTypeInfo = valueTypeInfo; + } + + @Override + public JsonTypeInfo valueTypeInfo() { + return valueTypeInfo; + } + + @Override + public Object constructCarrier(JsonReader reader, Object value) { + return construct(value); + } + + @Override + public Object extractValue(Object carrier) { + return extract((ProjectionCarrier) carrier); + } + + @Override + public Method[] constructMethods() { + return new Method[] {method("construct", Object.class)}; + } + + @Override + public int[] constructBoxBytes() { + return new int[] {0}; + } + + @Override + public Method[] extractMethods() { + return new Method[] {method("extract", ProjectionCarrier.class)}; + } + + @Override + public void write(JsonWriter writer, ProjectionValue value) { + writer.writeNull(); + } + + @Override + public ProjectionValue read(JsonReader reader) { + return null; + } + + @Override + public Class carrierType() { + return ProjectionCarrier.class; + } + + @Override + public Object readLatin1Carrier(Latin1JsonReader reader) { + return null; + } + + @Override + public Object readUtf16Carrier(Utf16JsonReader reader) { + return null; + } + + @Override + public Object readUtf8Carrier(Utf8JsonReader reader) { + return null; + } + + @Override + public void writeStringCarrier(StringJsonWriter writer, Object carrier) { + writer.writeNull(); + } + + @Override + public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { + writer.writeNull(); + } + + public static ProjectionCarrier construct(Object value) { + return (ProjectionCarrier) value; + } + + public static Object extract(ProjectionCarrier carrier) { + return carrier; + } + + private static Method method(String name, Class... parameterTypes) { + try { + return SiblingTransparentCodec.class.getMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + } + public static final class Unrelated {} public static final class Box { From f0cba975e5917e5b6affe650c187c441a57d1e5e Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 02:45:54 +0800 Subject: [PATCH 06/11] refactor(json): isolate generated codec key builder --- .../fory/json/codegen/GeneratedCodecKey.java | 21 +- .../resolver/GeneratedCodecKeyBuilder.java | 459 ++++++++++++++++++ .../fory/json/resolver/JsonTypeResolver.java | 406 +--------------- .../json/JsonGeneratedCapabilityKeyTest.java | 71 +-- 4 files changed, 515 insertions(+), 442 deletions(-) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java index 1ef94b2587..66cb04a382 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java @@ -167,7 +167,7 @@ private static String descriptor(Class type) { private final Class targetClass; private final Role role; - private final Object[] projection; + private final Object[] keyParts; private final Class[] referencedClasses; private final Class anchorClass; private final int hash; @@ -175,25 +175,25 @@ private static String descriptor(Class type) { private GeneratedCodecKey( Class targetClass, Role role, - Object[] projection, + Object[] keyParts, Class[] referencedClasses, Class preferredAnchor) { this.targetClass = Objects.requireNonNull(targetClass); this.role = Objects.requireNonNull(role); - this.projection = projection.clone(); - this.referencedClasses = uniqueClasses(targetClass, referencedClasses, projection); + this.keyParts = keyParts.clone(); + this.referencedClasses = uniqueClasses(targetClass, referencedClasses, keyParts); anchorClass = anchor(preferredAnchor, this.referencedClasses); hash = ((System.identityHashCode(targetClass) * 31 + role.hashCode()) * 31 + CLASS_VERSION) * 31 - + valuesHash(this.projection); + + valuesHash(this.keyParts); } public static GeneratedCodecKey object( - Class targetClass, Role role, Object[] projection, Class[] referencedClasses) { + Class targetClass, Role role, Object[] keyParts, Class[] referencedClasses) { if (role == Role.UTF8_COLLECTION_WRITER || role == Role.UTF8_COLLECTION_READER) { throw new IllegalArgumentException("Collection role requires a collection key"); } - return new GeneratedCodecKey(targetClass, role, projection, referencedClasses, targetClass); + return new GeneratedCodecKey(targetClass, role, keyParts, referencedClasses, targetClass); } public static GeneratedCodecKey collection( @@ -238,7 +238,7 @@ public boolean equals(Object other) { GeneratedCodecKey that = (GeneratedCodecKey) other; return targetClass == that.targetClass && role == that.role - && valuesEqual(projection, that.projection); + && valuesEqual(keyParts, that.keyParts); } @Override @@ -246,15 +246,14 @@ public int hashCode() { return hash; } - private static Class[] uniqueClasses( - Class target, Class[] explicit, Object[] projection) { + private static Class[] uniqueClasses(Class target, Class[] explicit, Object[] keyParts) { ArrayList> classes = new ArrayList<>(); IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); addClass(target, classes, seen); for (Class type : explicit) { addClass(type, classes, seen); } - collectClasses(projection, classes, seen); + collectClasses(keyParts, classes, seen); return classes.toArray(new Class[0]); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java new file mode 100644 index 0000000000..ada849e176 --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java @@ -0,0 +1,459 @@ +/* + * 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.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import org.apache.fory.json.codec.ClosedSubtypeCodec; +import org.apache.fory.json.codec.CodecUtils; +import org.apache.fory.json.codec.CollectionCodec; +import org.apache.fory.json.codec.DirectUnboxedValueCodec; +import org.apache.fory.json.codec.JsonUnwrappedInfo; +import org.apache.fory.json.codec.ObjectCodec; +import org.apache.fory.json.codec.ObjectCodec.AnyInfo; +import org.apache.fory.json.codec.TransparentUnboxedValueCodec; +import org.apache.fory.json.codec.UnboxedValueCodec; +import org.apache.fory.json.codegen.GeneratedCodecKey; +import org.apache.fory.json.codegen.GeneratedCodecKey.MemberDescriptor; +import org.apache.fory.json.codegen.GeneratedCodecKey.Role; +import org.apache.fory.json.meta.JsonCreatorFieldInfo; +import org.apache.fory.json.meta.JsonCreatorInfo; +import org.apache.fory.json.meta.JsonFieldAccessor; +import org.apache.fory.json.meta.JsonFieldInfo; + +/** Builds exact generated-codec keys from resolved JSON metadata. */ +final class GeneratedCodecKeyBuilder { + private GeneratedCodecKeyBuilder() {} + + static GeneratedCodecKey object( + JsonTypeResolver resolver, + JsonTypeInfo typeInfo, + ObjectCodec owner, + JsonTypeResolver.CapabilityKind kind) { + JsonSharedRegistry registry = resolver.sharedRegistry(); + ArrayList keyParts = new ArrayList<>(); + ArrayList> referencedClasses = new ArrayList<>(); + keyParts.add(registry.writeNullFields()); + keyParts.add(registry.propertyDiscoveryEnabled()); + keyParts.add(registry.propertyNamingStrategy()); + addMixinKeyParts(registry, owner.type(), keyParts, referencedClasses); + if (JsonTypeResolver.readerKind(kind)) { + keyParts.add(owner.graphMemoryBytes()); + keyParts.add(owner.hasValidators()); + } + JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); + if (unwrapped != null) { + for (JsonUnwrappedInfo.Group group : unwrapped.groups()) { + ObjectCodec child = group.childCodec(); + Class childType = child.type(); + keyParts.add(childType); + referencedClasses.add(childType); + addMixinKeyParts(registry, childType, keyParts, referencedClasses); + keyParts.add(MemberDescriptor.of(accessorMember(group.declaration().writeAccessor()))); + keyParts.add(MemberDescriptor.of(accessorMember(group.declaration().readAccessor()))); + keyParts.add(group.readIndex()); + keyParts.add(group.parent() == null ? -1 : group.parent().readIndex()); + keyParts.add(group.declaration().constructionIndex()); + Class parentType = group.parentCodec().type(); + keyParts.add(parentType); + referencedClasses.add(parentType); + keyParts.add(group.writeEnabled()); + keyParts.add(group.readEnabled()); + if (JsonTypeResolver.readerKind(kind)) { + keyParts.add(child.graphMemoryBytes()); + keyParts.add(child.hasValidators()); + addCreatorKeyParts(resolver, child.creatorInfo(), keyParts, referencedClasses, kind); + } + } + } + if (JsonTypeResolver.readerKind(kind)) { + addCreatorKeyParts(resolver, owner.creatorInfo(), keyParts, referencedClasses, kind); + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + addReadFields(resolver, owner, owner.readFields(), keyParts, referencedClasses, kind); + } else { + addCreatorFields(resolver, owner, creator.fields(), keyParts, referencedClasses, kind); + } + if (unwrapped != null) { + for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { + keyParts.add("route"); + keyParts.add(route.group().readIndex()); + if (route.field() != null) { + addReadField(resolver, owner, route.field(), keyParts, referencedClasses, kind); + } else { + addCreatorField( + resolver, owner, route.creatorField(), keyParts, referencedClasses, kind); + } + } + } + } else { + if (unwrapped != null) { + addUnwrappedWriteOrder(unwrapped.writeEntries(), keyParts, referencedClasses); + } + JsonFieldInfo[] fields = unwrapped == null ? owner.writeFields() : unwrapped.writeFields(); + for (JsonFieldInfo field : fields) { + addWriteField(resolver, owner, field, keyParts, referencedClasses, kind); + } + } + addAnyKeyParts(resolver, owner, keyParts, referencedClasses, kind); + return GeneratedCodecKey.object( + typeInfo.rawType(), + role(kind), + keyParts.toArray(), + referencedClasses.toArray(new Class[0])); + } + + static GeneratedCodecKey collection( + JsonTypeInfo typeInfo, CollectionCodec owner, JsonTypeResolver.CapabilityKind kind) { + Type type = typeInfo.type(); + Class rawType = CodecUtils.rawType(type, Collection.class); + Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + return GeneratedCodecKey.collection( + rawType, + elementType, + kind == JsonTypeResolver.CapabilityKind.UTF8_WRITER + ? Role.UTF8_COLLECTION_WRITER + : Role.UTF8_COLLECTION_READER, + owner instanceof CollectionCodec.StringCollectionCodec); + } + + private static void addUnwrappedWriteOrder( + JsonUnwrappedInfo.WriteEntry[] entries, + ArrayList keyParts, + ArrayList> referencedClasses) { + keyParts.add(entries.length); + for (JsonUnwrappedInfo.WriteEntry entry : entries) { + keyParts.add(entry.kind()); + if (entry.kind() == JsonUnwrappedInfo.DIRECT) { + JsonFieldInfo field = entry.field(); + keyParts.add(field.name()); + addMember(field.writeField(), keyParts, referencedClasses); + addMember(field.writeGetter(), keyParts, referencedClasses); + } else if (entry.kind() == JsonUnwrappedInfo.GROUP) { + keyParts.add(entry.group().readIndex()); + addUnwrappedWriteOrder(entry.group().writeEntries(), keyParts, referencedClasses); + } + } + } + + private static void addMixinKeyParts( + JsonSharedRegistry registry, + Class target, + ArrayList keyParts, + ArrayList> referencedClasses) { + Class mixin = registry.mixinType(target); + keyParts.add(mixin); + if (mixin != null) { + referencedClasses.add(mixin); + } + } + + private static void addCreatorKeyParts( + JsonTypeResolver resolver, + JsonCreatorInfo creator, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + if (creator == null) { + keyParts.add(null); + return; + } + keyParts.add("creator"); + addMember(creator.executable(), keyParts, referencedClasses); + addMember(creator.invocationExecutable(), keyParts, referencedClasses); + addMember(creator.defaultConstructor(), keyParts, referencedClasses); + keyParts.add(creator.argumentCount()); + keyParts.add(creator.defaultMaskCount()); + keyParts.add(creator.tracksArgumentPresence()); + for (int i = 0; i < creator.argumentCount(); i++) { + keyParts.add(creator.defaultMaskBit(i)); + keyParts.add(creator.hasDefault(i)); + addMember(creator.defaultMethod(i), keyParts, referencedClasses); + } + JsonFieldInfo[] deferred = creator.deferredFields(); + keyParts.add(deferred.length); + for (int i = 0; i < deferred.length; i++) { + keyParts.add(creator.deferredRequired(i)); + addReadField(resolver, null, deferred[i], keyParts, referencedClasses, kind); + } + } + + private static void addWriteField( + JsonTypeResolver resolver, + ObjectCodec owner, + JsonFieldInfo field, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + JsonTypeInfo child = field.writeTypeInfo(); + keyParts.add("write"); + keyParts.add(field.name()); + keyParts.add(field.writeRawType()); + keyParts.add(child.rawType()); + keyParts.add(field.writeKind()); + keyParts.add(field.writeNull()); + keyParts.add(field.requiresNonNullWrite()); + keyParts.add(field.writesRawString()); + keyParts.add(field.writesUnboxedValue()); + keyParts.add(resolver.usesWriterSlot(owner, child)); + addMember(field.writeField(), keyParts, referencedClasses); + addMember(field.writeGetter(), keyParts, referencedClasses); + addCapabilityKeyParts(resolver, child, kind, keyParts, referencedClasses); + addUnboxedKeyParts(field.writeUnboxedValueCodec(), false, keyParts, referencedClasses); + addClass(field.writeRawType(), referencedClasses); + addClass(child.rawType(), referencedClasses); + } + + private static void addReadFields( + JsonTypeResolver resolver, + ObjectCodec owner, + JsonFieldInfo[] fields, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + for (JsonFieldInfo field : fields) { + addReadField(resolver, owner, field, keyParts, referencedClasses, kind); + } + } + + private static void addReadField( + JsonTypeResolver resolver, + ObjectCodec owner, + JsonFieldInfo field, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + JsonTypeInfo child = field.readTypeInfo(); + keyParts.add("read"); + keyParts.add(field.name()); + keyParts.add(field.readRawType()); + keyParts.add(child.rawType()); + keyParts.add(field.readKind()); + keyParts.add(field.readIndex()); + keyParts.add(field.hasOccurrenceNullability()); + keyParts.add(field.occurrenceNullable()); + keyParts.add(field.occurrenceWrapsNull()); + keyParts.add(field.readsUnboxedValue()); + keyParts.add(owner != null && resolver.usesReaderSlot(owner, child)); + addMember(field.readField(), keyParts, referencedClasses); + addMember(field.readSetter(), keyParts, referencedClasses); + addCapabilityKeyParts(resolver, child, kind, keyParts, referencedClasses); + addUnboxedKeyParts(field.readUnboxedValueCodec(), true, keyParts, referencedClasses); + addClass(field.readRawType(), referencedClasses); + addClass(child.rawType(), referencedClasses); + } + + private static void addCreatorFields( + JsonTypeResolver resolver, + ObjectCodec owner, + JsonCreatorFieldInfo[] fields, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + for (JsonCreatorFieldInfo field : fields) { + addCreatorField(resolver, owner, field, keyParts, referencedClasses, kind); + } + } + + private static void addCreatorField( + JsonTypeResolver resolver, + ObjectCodec owner, + JsonCreatorFieldInfo field, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + JsonTypeInfo child = field.typeInfo(); + keyParts.add("argument"); + keyParts.add(field.name()); + keyParts.add(field.argumentIndex()); + keyParts.add(field.rawType()); + keyParts.add(child.rawType()); + keyParts.add(child.kind()); + keyParts.add(child.nullable()); + keyParts.add(child.rejectsNull()); + keyParts.add(field.materializesNullCarrier()); + keyParts.add(owner != null && resolver.usesReaderSlot(owner, child)); + addCapabilityKeyParts(resolver, child, kind, keyParts, referencedClasses); + addUnboxedKeyParts(field.unboxedValueCodec(), true, keyParts, referencedClasses); + addClass(field.rawType(), referencedClasses); + addClass(child.rawType(), referencedClasses); + } + + private static void addAnyKeyParts( + JsonTypeResolver resolver, + ObjectCodec owner, + ArrayList keyParts, + ArrayList> referencedClasses, + JsonTypeResolver.CapabilityKind kind) { + AnyInfo any = owner.anyInfo(); + boolean active = + any != null + && (JsonTypeResolver.readerKind(kind) + ? any.readField() != null || any.readSetter() != null + : any.writeField() != null || any.writeGetter() != null); + if (!active) { + keyParts.add(null); + return; + } + keyParts.add("any"); + keyParts.add(any.valueRawType()); + keyParts.add(any.writeIndex()); + keyParts.add(any.constructionIndex()); + boolean storesCodec = resolver.storesAnyCodec(owner, any); + keyParts.add(storesCodec); + keyParts.add( + storesCodec + && (JsonTypeResolver.readerKind(kind) + ? resolver.usesReaderSlot(owner, any.valueTypeInfo()) + : resolver.usesWriterSlot(owner, any.valueTypeInfo()))); + if (JsonTypeResolver.readerKind(kind)) { + addMember(any.readField(), keyParts, referencedClasses); + addMember(any.readSetter(), keyParts, referencedClasses); + } else { + addMember(any.writeField(), keyParts, referencedClasses); + addMember(any.writeGetter(), keyParts, referencedClasses); + } + addCapabilityKeyParts(resolver, any.valueTypeInfo(), kind, keyParts, referencedClasses); + addClass(any.valueRawType(), referencedClasses); + } + + private static void addCapabilityKeyParts( + JsonTypeResolver resolver, + JsonTypeInfo typeInfo, + JsonTypeResolver.CapabilityKind kind, + ArrayList keyParts, + ArrayList> referencedClasses) { + Object capability = JsonTypeResolver.currentCapability(typeInfo, kind); + Class capabilityClass = + resolver.sharedRegistry().canonicalProtectedBuiltin(typeInfo, capability) + ? null + : logicalCapabilityClass(resolver, typeInfo, capability); + keyParts.add(capabilityClass); + keyParts.add(typeInfo.kind()); + keyParts.add(typeInfo.nullable()); + keyParts.add(typeInfo.rejectsNull()); + keyParts.add(typeInfo.transparentNull()); + addClass(capabilityClass, referencedClasses); + } + + private static Class logicalCapabilityClass( + JsonTypeResolver resolver, JsonTypeInfo typeInfo, Object capability) { + if (resolver.canonicalObjectOwner(typeInfo) != null) { + return ObjectCodec.class; + } + CollectionCodec collection = resolver.collectionCodecOwner(typeInfo); + if (collection != null) { + return collection.getClass(); + } + return capability instanceof ClosedSubtypeCodec + ? ClosedSubtypeCodec.class + : capability.getClass(); + } + + private static void addUnboxedKeyParts( + UnboxedValueCodec codec, + boolean reader, + ArrayList keyParts, + ArrayList> referencedClasses) { + if (codec == null) { + keyParts.add(null); + return; + } + keyParts.add(codec.getClass()); + addClass(codec.getClass(), referencedClasses); + if (codec instanceof DirectUnboxedValueCodec) { + DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) codec; + addMember( + reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), + keyParts, + referencedClasses); + return; + } + TransparentUnboxedValueCodec transparent = (TransparentUnboxedValueCodec) codec; + JsonTypeInfo terminal = transparent.valueTypeInfo(); + keyParts.add(terminal.rawType()); + keyParts.add(terminal.kind()); + addClass(terminal.rawType(), referencedClasses); + UnboxedValueCodec terminalCodec = terminal.unboxedValueCodec(); + if (terminalCodec instanceof DirectUnboxedValueCodec) { + DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) terminalCodec; + addMember( + reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), + keyParts, + referencedClasses); + } else { + keyParts.add(null); + } + Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); + keyParts.add(methods.length); + for (Method method : methods) { + addMember(method, keyParts, referencedClasses); + } + if (reader) { + int[] boxes = transparent.constructBoxBytes(); + keyParts.add(boxes.length); + for (int box : boxes) { + keyParts.add(box); + } + } + } + + private static Member accessorMember(JsonFieldAccessor accessor) { + if (accessor == null) { + return null; + } + return accessor.getter() != null ? accessor.getter() : accessor.field(); + } + + private static void addMember( + Member member, ArrayList keyParts, ArrayList> referencedClasses) { + MemberDescriptor descriptor = MemberDescriptor.of(member); + keyParts.add(descriptor); + if (member != null) { + addClass(member.getDeclaringClass(), referencedClasses); + } + } + + private static void addClass(Class type, ArrayList> referencedClasses) { + if (type != null) { + referencedClasses.add(type); + } + } + + private static Role role(JsonTypeResolver.CapabilityKind kind) { + switch (kind) { + case STRING_WRITER: + return Role.STRING_WRITER; + case UTF8_WRITER: + return Role.UTF8_WRITER; + case LATIN1_READER: + return Role.LATIN1_READER; + case UTF16_READER: + return Role.UTF16_READER; + case UTF8_READER: + return Role.UTF8_READER; + default: + throw new IllegalStateException("Unknown JSON capability kind " + kind); + } + } +} 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 789ebfbb35..e3339e446b 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 @@ -52,7 +52,6 @@ import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.CompositeJsonCodec; -import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonObjectModel; import org.apache.fory.json.codec.JsonSubTypesInfo; @@ -65,14 +64,11 @@ import org.apache.fory.json.codec.ObjectCodec.AnyInfo; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.StringWriterCodec; -import org.apache.fory.json.codec.TransparentUnboxedValueCodec; import org.apache.fory.json.codec.UnboxedValueCodec; import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; import org.apache.fory.json.codec.Utf8WriterCodec; import org.apache.fory.json.codegen.GeneratedCodecKey; -import org.apache.fory.json.codegen.GeneratedCodecKey.MemberDescriptor; -import org.apache.fory.json.codegen.GeneratedCodecKey.Role; import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.codegen.JsonJITContext; import org.apache.fory.json.meta.JsonCreatorDeclaration; @@ -238,7 +234,7 @@ public ObjectCodec canonicalObjectCodec(JsonTypeInfo typeInfo) { } } - private ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { + ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { ObjectCodec owner = objectCodecs.get(metadataKey(typeInfo)); if (owner != null && canonicalObjectTypeInfos.get(owner) == typeInfo) { return owner; @@ -275,6 +271,10 @@ private CollectionCodec exactUtf8CollectionOwner(JsonTypeInfo typeInfo) { return owner; } + CollectionCodec collectionCodecOwner(JsonTypeInfo typeInfo) { + return collectionCodecs.get(typeInfo); + } + /** Returns an exact declared UTF-8 collection writer owner, or {@code null}. */ @Internal public CollectionCodec exactUtf8WriterCollection(JsonTypeInfo typeInfo) { @@ -1733,7 +1733,7 @@ private static JsonTypeInfo[] unwrappedReadTypeInfos(ObjectCodec owner) { return children; } - private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { + boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); } @@ -1747,398 +1747,12 @@ enum CapabilityKind { GeneratedCodecKey generatedObjectKey( JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { - ArrayList projection = new ArrayList<>(); - ArrayList> classes = new ArrayList<>(); - projection.add(sharedRegistry.writeNullFields()); - projection.add(sharedRegistry.propertyDiscoveryEnabled()); - projection.add(sharedRegistry.propertyNamingStrategy()); - addMixinProjection(owner.type(), projection, classes); - if (readerKind(kind)) { - projection.add(owner.graphMemoryBytes()); - projection.add(owner.hasValidators()); - } - JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); - if (unwrapped != null) { - for (JsonUnwrappedInfo.Group group : unwrapped.groups()) { - ObjectCodec child = group.childCodec(); - Class childType = child.type(); - projection.add(childType); - classes.add(childType); - addMixinProjection(childType, projection, classes); - projection.add(MemberDescriptor.of(accessorMember(group.declaration().writeAccessor()))); - projection.add(MemberDescriptor.of(accessorMember(group.declaration().readAccessor()))); - projection.add(group.readIndex()); - projection.add(group.parent() == null ? -1 : group.parent().readIndex()); - projection.add(group.declaration().constructionIndex()); - Class parentType = group.parentCodec().type(); - projection.add(parentType); - classes.add(parentType); - projection.add(group.writeEnabled()); - projection.add(group.readEnabled()); - if (readerKind(kind)) { - projection.add(child.graphMemoryBytes()); - projection.add(child.hasValidators()); - addCreatorProjection(child.creatorInfo(), projection, classes, kind); - } - } - } - if (readerKind(kind)) { - addCreatorProjection(owner.creatorInfo(), projection, classes, kind); - if (unwrapped == null) { - JsonCreatorInfo creator = owner.creatorInfo(); - if (creator == null) { - addReadFields(owner, owner.readFields(), projection, classes, kind); - } else { - addCreatorFields(owner, creator.fields(), projection, classes, kind); - } - } else { - JsonCreatorInfo creator = owner.creatorInfo(); - if (creator == null) { - addReadFields(owner, owner.readFields(), projection, classes, kind); - } else { - addCreatorFields(owner, creator.fields(), projection, classes, kind); - } - for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { - projection.add("route"); - projection.add(route.group().readIndex()); - if (route.field() != null) { - addReadField(owner, route.field(), projection, classes, kind); - } else { - addCreatorField(owner, route.creatorField(), projection, classes, kind); - } - } - } - } else { - if (unwrapped != null) { - addUnwrappedWriteOrder(unwrapped.writeEntries(), projection, classes); - } - JsonFieldInfo[] fields = unwrapped == null ? owner.writeFields() : unwrapped.writeFields(); - for (int i = 0; i < fields.length; i++) { - addWriteField(owner, fields[i], projection, classes, kind); - } - } - addAnyProjection(owner, projection, classes, kind); - return GeneratedCodecKey.object( - typeInfo.rawType(), role(kind), projection.toArray(), classes.toArray(new Class[0])); - } - - private void addUnwrappedWriteOrder( - JsonUnwrappedInfo.WriteEntry[] entries, - ArrayList projection, - ArrayList> classes) { - projection.add(entries.length); - for (JsonUnwrappedInfo.WriteEntry entry : entries) { - projection.add(entry.kind()); - if (entry.kind() == JsonUnwrappedInfo.DIRECT) { - JsonFieldInfo field = entry.field(); - projection.add(field.name()); - addMember(field.writeField(), projection, classes); - addMember(field.writeGetter(), projection, classes); - } else if (entry.kind() == JsonUnwrappedInfo.GROUP) { - projection.add(entry.group().readIndex()); - addUnwrappedWriteOrder(entry.group().writeEntries(), projection, classes); - } - } + return GeneratedCodecKeyBuilder.object(this, typeInfo, owner, kind); } GeneratedCodecKey generatedCollectionKey( JsonTypeInfo typeInfo, CollectionCodec owner, CapabilityKind kind) { - Type type = typeInfo.type(); - Class rawType = CodecUtils.rawType(type, Collection.class); - Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); - return GeneratedCodecKey.collection( - rawType, - elementType, - kind == CapabilityKind.UTF8_WRITER - ? Role.UTF8_COLLECTION_WRITER - : Role.UTF8_COLLECTION_READER, - owner instanceof CollectionCodec.StringCollectionCodec); - } - - private void addMixinProjection( - Class target, ArrayList projection, ArrayList> classes) { - Class mixin = sharedRegistry.mixinType(target); - projection.add(mixin); - if (mixin != null) { - classes.add(mixin); - } - } - - private void addCreatorProjection( - JsonCreatorInfo creator, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - if (creator == null) { - projection.add(null); - return; - } - projection.add("creator"); - addMember(creator.executable(), projection, classes); - addMember(creator.invocationExecutable(), projection, classes); - addMember(creator.defaultConstructor(), projection, classes); - projection.add(creator.argumentCount()); - projection.add(creator.defaultMaskCount()); - projection.add(creator.tracksArgumentPresence()); - for (int i = 0; i < creator.argumentCount(); i++) { - projection.add(creator.defaultMaskBit(i)); - projection.add(creator.hasDefault(i)); - addMember(creator.defaultMethod(i), projection, classes); - } - JsonFieldInfo[] deferred = creator.deferredFields(); - projection.add(deferred.length); - for (int i = 0; i < deferred.length; i++) { - projection.add(creator.deferredRequired(i)); - addReadField(null, deferred[i], projection, classes, kind); - } - } - - private void addWriteField( - ObjectCodec owner, - JsonFieldInfo field, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - JsonTypeInfo child = field.writeTypeInfo(); - projection.add("write"); - projection.add(field.name()); - projection.add(field.writeRawType()); - projection.add(child.rawType()); - projection.add(field.writeKind()); - projection.add(field.writeNull()); - projection.add(field.requiresNonNullWrite()); - projection.add(field.writesRawString()); - projection.add(field.writesUnboxedValue()); - projection.add(usesWriterSlot(owner, child)); - addMember(field.writeField(), projection, classes); - addMember(field.writeGetter(), projection, classes); - addCapabilityProjection(child, kind, projection, classes); - addUnboxedProjection(field.writeUnboxedValueCodec(), false, projection, classes); - addClass(field.writeRawType(), classes); - addClass(child.rawType(), classes); - } - - private void addReadFields( - ObjectCodec owner, - JsonFieldInfo[] fields, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - for (JsonFieldInfo field : fields) { - addReadField(owner, field, projection, classes, kind); - } - } - - private void addReadField( - ObjectCodec owner, - JsonFieldInfo field, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - JsonTypeInfo child = field.readTypeInfo(); - projection.add("read"); - projection.add(field.name()); - projection.add(field.readRawType()); - projection.add(child.rawType()); - projection.add(field.readKind()); - projection.add(field.readIndex()); - projection.add(field.hasOccurrenceNullability()); - projection.add(field.occurrenceNullable()); - projection.add(field.occurrenceWrapsNull()); - projection.add(field.readsUnboxedValue()); - projection.add(owner != null && usesReaderSlot(owner, child)); - addMember(field.readField(), projection, classes); - addMember(field.readSetter(), projection, classes); - addCapabilityProjection(child, kind, projection, classes); - addUnboxedProjection(field.readUnboxedValueCodec(), true, projection, classes); - addClass(field.readRawType(), classes); - addClass(child.rawType(), classes); - } - - private void addCreatorFields( - ObjectCodec owner, - JsonCreatorFieldInfo[] fields, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - for (JsonCreatorFieldInfo field : fields) { - addCreatorField(owner, field, projection, classes, kind); - } - } - - private void addCreatorField( - ObjectCodec owner, - JsonCreatorFieldInfo field, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - JsonTypeInfo child = field.typeInfo(); - projection.add("argument"); - projection.add(field.name()); - projection.add(field.argumentIndex()); - projection.add(field.rawType()); - projection.add(child.rawType()); - projection.add(child.kind()); - projection.add(child.nullable()); - projection.add(child.rejectsNull()); - projection.add(field.materializesNullCarrier()); - projection.add(owner != null && usesReaderSlot(owner, child)); - addCapabilityProjection(child, kind, projection, classes); - addUnboxedProjection(field.unboxedValueCodec(), true, projection, classes); - addClass(field.rawType(), classes); - addClass(child.rawType(), classes); - } - - private void addAnyProjection( - ObjectCodec owner, - ArrayList projection, - ArrayList> classes, - CapabilityKind kind) { - AnyInfo any = owner.anyInfo(); - boolean active = - any != null - && (readerKind(kind) - ? any.readField() != null || any.readSetter() != null - : any.writeField() != null || any.writeGetter() != null); - if (!active) { - projection.add(null); - return; - } - projection.add("any"); - projection.add(any.valueRawType()); - projection.add(any.writeIndex()); - projection.add(any.constructionIndex()); - boolean storesCodec = storesAnyCodec(owner, any); - projection.add(storesCodec); - projection.add( - storesCodec - && (readerKind(kind) - ? usesReaderSlot(owner, any.valueTypeInfo()) - : usesWriterSlot(owner, any.valueTypeInfo()))); - if (readerKind(kind)) { - addMember(any.readField(), projection, classes); - addMember(any.readSetter(), projection, classes); - } else { - addMember(any.writeField(), projection, classes); - addMember(any.writeGetter(), projection, classes); - } - addCapabilityProjection(any.valueTypeInfo(), kind, projection, classes); - addClass(any.valueRawType(), classes); - } - - private void addCapabilityProjection( - JsonTypeInfo typeInfo, - CapabilityKind kind, - ArrayList projection, - ArrayList> classes) { - Object capability = currentCapability(typeInfo, kind); - Class capabilityClass = - sharedRegistry.canonicalProtectedBuiltin(typeInfo, capability) - ? null - : logicalCapabilityClass(typeInfo, capability); - projection.add(capabilityClass); - projection.add(typeInfo.kind()); - projection.add(typeInfo.nullable()); - projection.add(typeInfo.rejectsNull()); - projection.add(typeInfo.transparentNull()); - addClass(capabilityClass, classes); - } - - private Class logicalCapabilityClass(JsonTypeInfo typeInfo, Object capability) { - if (canonicalObjectOwner(typeInfo) != null) { - return ObjectCodec.class; - } - CollectionCodec collection = collectionCodecs.get(typeInfo); - if (collection != null) { - return collection.getClass(); - } - return capability instanceof ClosedSubtypeCodec - ? ClosedSubtypeCodec.class - : capability.getClass(); - } - - private static void addUnboxedProjection( - UnboxedValueCodec codec, - boolean reader, - ArrayList projection, - ArrayList> classes) { - if (codec == null) { - projection.add(null); - return; - } - projection.add(codec.getClass()); - addClass(codec.getClass(), classes); - if (codec instanceof DirectUnboxedValueCodec) { - DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) codec; - addMember( - reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), projection, classes); - return; - } - TransparentUnboxedValueCodec transparent = (TransparentUnboxedValueCodec) codec; - JsonTypeInfo terminal = transparent.valueTypeInfo(); - projection.add(terminal.rawType()); - projection.add(terminal.kind()); - addClass(terminal.rawType(), classes); - UnboxedValueCodec terminalCodec = terminal.unboxedValueCodec(); - if (terminalCodec instanceof DirectUnboxedValueCodec) { - DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) terminalCodec; - addMember( - reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), projection, classes); - } else { - projection.add(null); - } - Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); - projection.add(methods.length); - for (Method method : methods) { - addMember(method, projection, classes); - } - if (reader) { - int[] boxes = transparent.constructBoxBytes(); - projection.add(boxes.length); - for (int box : boxes) { - projection.add(box); - } - } - } - - private static java.lang.reflect.Member accessorMember( - org.apache.fory.json.meta.JsonFieldAccessor accessor) { - if (accessor == null) { - return null; - } - return accessor.getter() != null ? accessor.getter() : accessor.field(); - } - - private static void addMember( - java.lang.reflect.Member member, ArrayList projection, ArrayList> classes) { - MemberDescriptor descriptor = MemberDescriptor.of(member); - projection.add(descriptor); - if (member != null) { - addClass(member.getDeclaringClass(), classes); - } - } - - private static void addClass(Class type, ArrayList> classes) { - if (type != null) { - classes.add(type); - } - } - - private static Role role(CapabilityKind kind) { - switch (kind) { - case STRING_WRITER: - return Role.STRING_WRITER; - case UTF8_WRITER: - return Role.UTF8_WRITER; - case LATIN1_READER: - return Role.LATIN1_READER; - case UTF16_READER: - return Role.UTF16_READER; - case UTF8_READER: - return Role.UTF8_READER; - default: - throw new IllegalStateException("Unknown JSON capability kind " + kind); - } + return GeneratedCodecKeyBuilder.collection(typeInfo, owner, kind); } /** Returns the nested object type inlined by generated readers, or {@code null}. */ @@ -2439,7 +2053,7 @@ private Class nativeObjectClass( return sharedRegistry.nativeGeneratedClass(generatedObjectKey(typeInfo, owner, kind)); } - private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { + static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { switch (kind) { case STRING_WRITER: return typeInfo.stringWriter(); @@ -2655,7 +2269,7 @@ private ObjectCodec requireObjectOwner(JsonTypeInfo typeInfo) { return owner; } - private static boolean readerKind(CapabilityKind kind) { + static boolean readerKind(CapabilityKind kind) { return kind == CapabilityKind.LATIN1_READER || kind == CapabilityKind.UTF16_READER || kind == CapabilityKind.UTF8_READER; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java index 2c1ba853c8..fff9d1ad05 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java @@ -267,14 +267,14 @@ public void inactiveAnyDirectionReusesClass() { } @Test - public void terminalDirectMethodsVersionProjection() throws Exception { + public void terminalDirectMethodsChangeKey() throws Exception { JsonTypeInfo first = directTerminal(new VariableDirectCodec(false)); JsonTypeInfo second = directTerminal(new VariableDirectCodec(true)); - ProjectionTransparentCodec firstCodec = new ProjectionTransparentCodec(first); - ProjectionTransparentCodec secondCodec = new ProjectionTransparentCodec(second); + TerminalTransparentCodec firstCodec = new TerminalTransparentCodec(first); + TerminalTransparentCodec secondCodec = new TerminalTransparentCodec(second); - assertNotEquals(unboxedProjection(firstCodec, false), unboxedProjection(secondCodec, false)); - assertNotEquals(unboxedProjection(firstCodec, true), unboxedProjection(secondCodec, true)); + assertNotEquals(unboxedKeyParts(firstCodec, false), unboxedKeyParts(secondCodec, false)); + assertNotEquals(unboxedKeyParts(firstCodec, true), unboxedKeyParts(secondCodec, true)); } @Test @@ -357,7 +357,7 @@ public void hostedTransparentTerminalUsesInterpretedRole() throws Exception { packageName, "SiblingTerminal", "public final class SiblingTerminal implements " - + ProjectionCarrier.class.getCanonicalName() + + SiblingCarrier.class.getCanonicalName() + " {}"); try (URLClassLoader terminalLoader = new URLClassLoader( @@ -368,7 +368,7 @@ public void hostedTransparentTerminalUsesInterpretedRole() throws Exception { ForyJson.builder() .registerCodec((Class) terminal, JsonTestSupport.nullCodec()) .registerCodec( - ProjectionValue.class, + SiblingValue.class, (type, resolver, runtimeType) -> new SiblingTransparentCodec(resolver.getTypeInfo((Class) terminal, terminal))) .registerCodec( @@ -507,19 +507,20 @@ private static JsonTypeInfo directTerminal(VariableDirectCodec codec) { .getTypeInfo(TypeRef.of(int.class, TypeExtMeta.of(Types.UINT32, false, false))); } - private static List unboxedProjection(UnboxedValueCodec codec, boolean reader) + private static List unboxedKeyParts(UnboxedValueCodec codec, boolean reader) throws Exception { Method method = - JsonTypeResolver.class.getDeclaredMethod( - "addUnboxedProjection", - UnboxedValueCodec.class, - boolean.class, - ArrayList.class, - ArrayList.class); + Class.forName("org.apache.fory.json.resolver.GeneratedCodecKeyBuilder") + .getDeclaredMethod( + "addUnboxedKeyParts", + UnboxedValueCodec.class, + boolean.class, + ArrayList.class, + ArrayList.class); method.setAccessible(true); - ArrayList projection = new ArrayList<>(); - method.invoke(null, codec, reader, projection, new ArrayList>()); - return projection; + ArrayList keyParts = new ArrayList<>(); + method.invoke(null, codec, reader, keyParts, new ArrayList>()); + return keyParts; } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -591,7 +592,7 @@ private static void compileSource( } private static JsonObjectModel transparentModel() throws Exception { - TypeRef logicalType = TypeRef.of(ProjectionValue.class, ordinary(false)); + TypeRef logicalType = TypeRef.of(SiblingValue.class, ordinary(false)); return new JsonObjectModel( TransparentModel.class.getConstructor(), null, @@ -603,7 +604,7 @@ private static JsonObjectModel transparentModel() throws Exception { new TypeRef[0], new String[] {"value"}, new Method[] {TransparentModel.class.getMethod("getValue")}, - new Method[] {TransparentModel.class.getMethod("setValue", ProjectionCarrier.class)}, + new Method[] {TransparentModel.class.getMethod("setValue", SiblingCarrier.class)}, new TypeRef[] {logicalType}); } @@ -689,20 +690,20 @@ public static final class Child { public Child() {} } - public interface ProjectionCarrier {} + public interface SiblingCarrier {} - public static final class ProjectionValue {} + public static final class SiblingValue {} public static final class TransparentModel { - private ProjectionCarrier value; + private SiblingCarrier value; public TransparentModel() {} - public ProjectionCarrier getValue() { + public SiblingCarrier getValue() { return value; } - public void setValue(ProjectionCarrier value) { + public void setValue(SiblingCarrier value) { this.value = value; } } @@ -864,11 +865,11 @@ private static Method method(String name, Class... parameters) { } } - public static final class ProjectionTransparentCodec extends AbstractJsonValueCodec + public static final class TerminalTransparentCodec extends AbstractJsonValueCodec implements TransparentUnboxedValueCodec { private final JsonTypeInfo valueTypeInfo; - public ProjectionTransparentCodec(JsonTypeInfo valueTypeInfo) { + public TerminalTransparentCodec(JsonTypeInfo valueTypeInfo) { this.valueTypeInfo = valueTypeInfo; } @@ -943,7 +944,7 @@ public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { } } - public static final class SiblingTransparentCodec extends AbstractJsonValueCodec + public static final class SiblingTransparentCodec extends AbstractJsonValueCodec implements TransparentUnboxedValueCodec { private final JsonTypeInfo valueTypeInfo; @@ -963,7 +964,7 @@ public Object constructCarrier(JsonReader reader, Object value) { @Override public Object extractValue(Object carrier) { - return extract((ProjectionCarrier) carrier); + return extract((SiblingCarrier) carrier); } @Override @@ -978,22 +979,22 @@ public int[] constructBoxBytes() { @Override public Method[] extractMethods() { - return new Method[] {method("extract", ProjectionCarrier.class)}; + return new Method[] {method("extract", SiblingCarrier.class)}; } @Override - public void write(JsonWriter writer, ProjectionValue value) { + public void write(JsonWriter writer, SiblingValue value) { writer.writeNull(); } @Override - public ProjectionValue read(JsonReader reader) { + public SiblingValue read(JsonReader reader) { return null; } @Override public Class carrierType() { - return ProjectionCarrier.class; + return SiblingCarrier.class; } @Override @@ -1021,11 +1022,11 @@ public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { writer.writeNull(); } - public static ProjectionCarrier construct(Object value) { - return (ProjectionCarrier) value; + public static SiblingCarrier construct(Object value) { + return (SiblingCarrier) value; } - public static Object extract(ProjectionCarrier carrier) { + public static Object extract(SiblingCarrier carrier) { return carrier; } From 0f18882ffa8949ea67426de64122fe3c8953b93c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 03:14:50 +0800 Subject: [PATCH 07/11] fix(json): close generated codec CI gaps --- .../apache/fory/json/codegen/JsonCodegen.java | 24 ++++++++++++----- .../fory-json/native-image.properties | 1 + .../json/kotlin/KotlinRuntimeTestSupport.kt | 27 ++++++++++++++++--- 3 files changed, 43 insertions(+), 9 deletions(-) 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 a3068b218b..b130873a0f 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 @@ -1050,12 +1050,24 @@ private Class compileHostedClass( 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)); + if (JdkVersion.MAJOR_VERSION >= 9) { + 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 those implementation modules, so establish the generated + // class's actual dependencies before defining it in the model module. JDK 8-24 core field + // access also emits sun.misc.Unsafe calls; the generated class, rather than Fory core, owns + // that linkage and therefore needs its own read edge to jdk.unsupported. + _JDKAccess.addReads(ownerModule, _JDKAccess.getModule(JsonCodegen.class)); + _JDKAccess.addReads(ownerModule, _JDKAccess.getModule(DefineClass.class)); + if (JdkVersion.MAJOR_VERSION < 25) { + try { + _JDKAccess.addReads( + ownerModule, _JDKAccess.getModule(Class.forName("sun.misc.Unsafe", false, null))); + } catch (ClassNotFoundException e) { + throw new ForyJsonException("Cannot resolve generated Unsafe field access", e); + } + } + } Class mainClass = DefineClass.defineClass( mainClassName, ownerType, ownerLoader, ownerType.getProtectionDomain(), mainBytecode); 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 cb6707821d..27bcf403d8 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 @@ -37,6 +37,7 @@ Args=--features=org.apache.fory.json.ForyJsonGraalVMFeature \ org.apache.fory.util.function.ToCharFunction,\ org.apache.fory.util.function.ToFloatFunction,\ org.apache.fory.util.function.ToShortFunction,\ + org.apache.fory.util.ClassLoaderUtils$ComposedClassLoader,\ org.apache.fory.json.codegen,\ org.apache.fory.json.codec,\ org.apache.fory.json.meta,\ diff --git a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt index 0a3f29494e..9e9ac56988 100644 --- a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt +++ b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt @@ -20,6 +20,7 @@ package org.apache.fory.json.kotlin import java.io.ByteArrayInputStream +import java.util.concurrent.CompletableFuture import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotNull @@ -28,6 +29,7 @@ import org.apache.fory.codegen.CompileState import org.apache.fory.json.ForyJson import org.apache.fory.json.ForyJsonBuilder import org.apache.fory.json.ForyJsonException +import org.apache.fory.json.codegen.GeneratedCodecKey import org.apache.fory.json.codegen.JsonJITContext import org.apache.fory.reflect.ReflectionUtils import org.apache.fory.reflect.TypeRef @@ -131,9 +133,28 @@ private fun compileStates(json: ForyJson): Map { val resolver = ReflectionUtils.getObjectFieldValue(state, "typeResolver") val registry = ReflectionUtils.getObjectFieldValue(resolver, "sharedRegistry") val codegen = ReflectionUtils.getObjectFieldValue(registry, "codegen") - val generator = ReflectionUtils.getObjectFieldValue(codegen, "codeGenerator") - return ReflectionUtils.getObjectFieldValue(generator, "parallelCompileState") - as Map + val futures = + ReflectionUtils.getObjectFieldValue(registry, "generatedClassFutures") + as Map?>> + val generatedNames = futures.values.mapNotNull { it.join()?.name }.toSet() + val compiler = + codegen.javaClass.getDeclaredMethod( + "compiler", + GeneratedCodecKey::class.java, + String::class.java, + String::class.java, + ) + compiler.isAccessible = true + return futures.keys + .map { compiler.invoke(codegen, it, "", "") } + .map { ReflectionUtils.getObjectFieldValue(it, "codeGenerator") } + .distinct() + .flatMap { + (ReflectionUtils.getObjectFieldValue(it, "parallelCompileState") as Map) + .entries + } + .filter { it.key in generatedNames } + .associate { it.toPair() } } private fun finishedResult(state: CompileState): Map { From 0a0fa55dee6629baae1cd40b337325f58c1facf3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 03:19:18 +0800 Subject: [PATCH 08/11] test(json): sync native image args verifier --- .../org/apache/fory/json/ForyJsonGraalVMFeatureJarVerifier.java | 1 + 1 file changed, 1 insertion(+) 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 20f424062b..34317a63d6 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 @@ -72,6 +72,7 @@ public final class ForyJsonGraalVMFeatureJarVerifier { + "org.apache.fory.util.function.ToCharFunction," + "org.apache.fory.util.function.ToFloatFunction," + "org.apache.fory.util.function.ToShortFunction," + + "org.apache.fory.util.ClassLoaderUtils$ComposedClassLoader," + "org.apache.fory.json.codegen," + "org.apache.fory.json.codec," + "org.apache.fory.json.meta," From 29bf1831262fdc71114f50e2ddbdca9dec2a17ae Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sat, 22 Aug 2026 10:14:37 +0800 Subject: [PATCH 09/11] refactor(json): simplify generated codec key inputs --- .agents/languages/java.md | 8 +- docs/json/annotations.md | 4 +- docs/json/custom-codecs.md | 5 +- docs/json/graalvm.md | 19 +- docs/json/kotlin.md | 6 +- docs/json/object-mapping.md | 8 +- .../apache/fory/graalvm/ForyJsonExample.java | 67 +- .../org/apache/fory/json/ForyJsonBuilder.java | 45 +- .../org/apache/fory/json/ForyJsonModule.java | 2 +- .../apache/fory/json/JsonCodecFactory.java | 4 +- .../org/apache/fory/json/ModuleContext.java | 8 +- .../json/annotation/ForyJsonProvider.java | 6 +- .../apache/fory/json/codec/ArrayCodec.java | 9 - .../json/codec/DirectUnboxedValueCodec.java | 7 +- .../codec/TransparentUnboxedValueCodec.java | 8 +- .../fory/json/codegen/GeneratedCodecKey.java | 265 ++------ .../apache/fory/json/codegen/JsonCodegen.java | 258 +++---- .../fory/json/meta/JsonCreatorFieldInfo.java | 5 + .../apache/fory/json/meta/JsonFieldInfo.java | 30 + .../apache/fory/json/meta/JsonFieldKind.java | 29 +- .../fory/json/resolver/CodecRegistry.java | 19 +- .../resolver/GeneratedCodecKeyBuilder.java | 491 ++++---------- .../resolver/JsonGeneratedClassRegistry.java | 50 +- .../json/resolver/JsonSharedRegistry.java | 195 +++--- .../fory/json/resolver/JsonTypeInfo.java | 27 +- .../fory/json/resolver/JsonTypeResolver.java | 364 +++++----- .../fory/json/ForyJsonGraalVMFeature.java | 21 +- .../fory/json/JsonAsyncCompilationTest.java | 11 +- .../fory/json/JsonCodecRegistrationTest.java | 23 +- .../json/JsonGeneratedCapabilityKeyTest.java | 638 ++++++++++++------ .../fory/json/JsonGeneratedCodecTest.java | 2 +- .../json/kotlin/KotlinRuntimeTestSupport.kt | 3 +- 32 files changed, 1270 insertions(+), 1367 deletions(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index be75624877..e2e80eff26 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -87,9 +87,11 @@ Load this file when changing anything under `java/` or when Java drives a cross- on every Java runtime, not only in GraalVM. Do not expand this set to every default codec: enums, other arrays, collections, maps, atomics, optionals, `File`, `URI`, `Path`, `ByteBuffer`, calendar and locale types, `Float16`, `BFloat16`, and user-defined types remain registerable. Field/type - `@JsonCodec`, `@JsonFormat`, and semantic metadata remain separate from exact registry mutation; - generated-class keys may omit a protected type's codec class only when the resolved role still - uses its canonical built-in path. + `@JsonCodec`, `@JsonFormat`, and semantic metadata remain separate from exact registry mutation + and are fixed by the target class or effective Mixin. +- Fory JSON `ObjectCodec` instances are resolver-owned and must not be registered directly. A + language module that supplies a custom object model must use a `JsonCodecFactory`, whose stable + factory key participates in the generated object-class identity. - Do not add normal-JVM process-global caches keyed by user classes, generated classes, serializer classes, classloaders, or class-bound method handles. Prefer per-runtime state, immutable shared metadata, or build-time-only template data. The only exception is Fory JSON's generated-role diff --git a/docs/json/annotations.md b/docs/json/annotations.md index bf6ad68494..80bc6e98bf 100644 --- a/docs/json/annotations.md +++ b/docs/json/annotations.md @@ -41,8 +41,8 @@ construction operations. In Android builds that use R8 or ProGuard, Kotlin KSP e retention rules for Kotlin `@JsonType` models. It also processes an exact Mixin declared in application source when either the Mixin or its exact target is Kotlin. KSP does not generate codecs or construction operations. GraalVM Native Image discovers reachable Java and Kotlin `JsonType` -declarations directly. It generates the default configuration baseline plus additions from -reachable providers, while exact misses use interpreted codecs. See the +declarations directly. It generates codecs with the default configuration and each reachable +provider configuration; models without a matching generated codec use interpreted codecs. See the [GraalVM guide](graalvm.md) and [Android guide](android.md) for the platform workflows. ## Kotlin use-site targets diff --git a/docs/json/custom-codecs.md b/docs/json/custom-codecs.md index f4aeb32b20..fbb94d4f5f 100644 --- a/docs/json/custom-codecs.md +++ b/docs/json/custom-codecs.md @@ -84,7 +84,10 @@ dedicated reader/writer operations: The restriction is exact; it does not include application subclasses. It also does not disable occurrence-level `JsonCodec`, `JsonFormat`, or other semantic mappings. Use those mechanisms when a -field or parameter of a protected type needs a different representation. +field or parameter of one of these types needs a different representation. + +`ObjectCodec` instances belong to the resolver that created them and cannot be registered directly. +Use an exact `JsonCodecFactory` when a language module needs to supply an object model. Use `JsonCodecFactory` when one factory owns a family of declared or parameterized types: diff --git a/docs/json/graalvm.md b/docs/json/graalvm.md index 8742f36a4a..a0e65c83f4 100644 --- a/docs/json/graalvm.md +++ b/docs/json/graalvm.md @@ -55,8 +55,8 @@ 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. It also generates codecs for reachable models under the default configuration. At runtime, -`ForyJson.builder().build()` uses those exact generated codecs and falls back to interpreted codecs -when no exact generated entry matches, without application reflection configuration, package +`ForyJson.builder().build()` uses those generated codecs and falls back to interpreted codecs when +no matching generated codec is available, without application reflection configuration, package exports or opens, or build-time initialization. An application class configured for build-time initialization may retain a static `ForyJson` in the @@ -99,19 +99,18 @@ public final class JsonConfigs { 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. When the -default and provider configurations produce the same exact model-role key, they reuse one -generated class. +provider may return multiple configurations, and multiple providers may be reachable. 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, and the provider package does not need to be exported or opened to Fory. Static provider methods and fields are not supported. -Generated entries are additive: the default baseline is never removed when a provider is present, -and every reachable provider contributes its own exact entries. A codegen-enabled runtime first -looks up the exact model role for its current configuration; a miss uses the interpreted codec. -Reflection metadata is prepared independently of generated-codec matches. `withCodegen(false)` +Default-configuration codecs remain available when providers are present, and every reachable +provider adds codecs for its configuration. A codegen-enabled runtime uses an interpreted codec +whenever no matching generated codec is available. Reflection metadata remains available in either +case. +`withCodegen(false)` explicitly selects interpreted codecs and does not request generated-codec lookup. Asynchronous compilation is disabled in a native executable. @@ -135,7 +134,7 @@ Annotate each reachable concrete Kotlin model with `@JsonType`, or register an e Mixin for a third-party target. Fory reads and validates Kotlin metadata while building the image, then generates codecs for each reachable Kotlin-enabled provider configuration. A provider configuration with disabled code generation or an unsupported metadata ABI fails image -construction. A Kotlin-enabled runtime configuration with no exact generated match uses its +construction. A Kotlin-enabled runtime configuration with no matching generated codec uses its prepared interpreted codec. An exact generic Kotlin root is available only when its complete binding is reached through a diff --git a/docs/json/kotlin.md b/docs/json/kotlin.md index df8aae4020..ab74b22fca 100644 --- a/docs/json/kotlin.md +++ b/docs/json/kotlin.md @@ -342,9 +342,9 @@ On GraalVM Native Image, use the existing `@ForyJsonProvider` workflow, install `ForyJsonKotlin`, and enable code generation in the returned configuration. Annotate each reachable concrete Kotlin model with `@JsonType`, or register an exact reachable Mixin for a third-party target. Fory reads the Kotlin metadata and adds generated codecs for each reachable Kotlin-enabled -provider configuration while building the image. A runtime exact miss uses the prepared -interpreted codec. Only exact generic bindings reached through concrete roots are available. Do not -add reflection configuration or package-wide opens. +provider configuration while building the image. A model without a matching generated codec uses +the interpreted codec. Only exact generic bindings reached through concrete roots are available. +Do not add reflection configuration or package-wide opens. On Android, use API 26 or later. The runtime reads Kotlin metadata in both debug and release builds, and runtime JSON code generation remains disabled. Follow the [installation](#installation) above diff --git a/docs/json/object-mapping.md b/docs/json/object-mapping.md index 7f8eeb1eb5..43fef10582 100644 --- a/docs/json/object-mapping.md +++ b/docs/json/object-mapping.md @@ -229,7 +229,7 @@ see [Fory JSON Security](security.md). Builder mutation after `build()` does not modify an existing `ForyJson` instance. On Android, runtime code generation and asynchronous compilation are disabled. In a GraalVM native -image, runtime compilation is unavailable. Reachable models receive a default generated baseline, -and reachable `ForyJsonProvider` configurations add exact generated entries. A runtime exact miss -uses an interpreted codec with build-time-prepared access metadata. Every other builder option -keeps the behavior described above. +image, runtime compilation is unavailable. Fory JSON generates codecs for reachable models with the +default configuration and each reachable `ForyJsonProvider` configuration. A model without a +matching generated codec uses an interpreted codec. Every other builder option keeps the behavior +described above. 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 5a7cffb6c1..7b202f46de 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,8 +19,6 @@ 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; @@ -87,44 +85,33 @@ public final class ForyJsonExample { private ForyJsonExample() {} 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 { - Preconditions.checkArgument( - ClosedJsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class)); - if (GraalvmSupport.isGraalRuntime()) { - testHostedCodegenConfigurations(); - } - testModels(); - testConfigurations(); - testCodecs(); - testValueAnnotations(); - testSubtypes(); - testContainerRoots(); - testGenericProperties(); - testUnwrapped(); - testValidator(); - testGraphMemoryBudget(); - testContainerGraphBudget(); - testSpecialContainerBudget(); - testMixin(); - testMixinValue(); - testMixinValueRecord(); - testMixinEnumValue(); - testMixinCodec(); - testBigDecimal(); - testSqlTypes(); - testFormatTimezone(); - testClosedPackage(); - } finally { - System.setOut(originalOut); - } - } - String output = new String(captured.toByteArray(), StandardCharsets.UTF_8); - originalOut.print(output); - originalOut.println("Fory JSON succeed"); + Preconditions.checkArgument( + ClosedJsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class)); + if (GraalvmSupport.isGraalRuntime()) { + testHostedCodegenConfigurations(); + } + testModels(); + testConfigurations(); + testCodecs(); + testValueAnnotations(); + testSubtypes(); + testContainerRoots(); + testGenericProperties(); + testUnwrapped(); + testValidator(); + testGraphMemoryBudget(); + testContainerGraphBudget(); + testSpecialContainerBudget(); + testMixin(); + testMixinValue(); + testMixinValueRecord(); + testMixinEnumValue(); + testMixinCodec(); + testBigDecimal(); + testSqlTypes(); + testFormatTimezone(); + testClosedPackage(); + System.out.println("Fory JSON succeed"); } private static void testHostedCodegenConfigurations() { 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 e20b5445a3..619f89860d 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 @@ -26,6 +26,7 @@ import java.util.Objects; import org.apache.fory.json.annotation.JsonMixin; import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.resolver.CodecRegistry; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; @@ -83,7 +84,7 @@ public ForyJsonBuilder writeNullFields(boolean writeNullFields) { * Enables generated object codecs for supported classes. Enabled by default and automatically * disabled on Android. A GraalVM Native Image includes generated codecs for the default * configuration and for each reachable {@link org.apache.fory.json.annotation.ForyJsonProvider} - * configuration. An exact generated-codec miss uses the interpreted codec. + * configuration. Models without a matching generated codec use the interpreted codec. */ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { this.codegenEnabled = codegenEnabled; @@ -211,6 +212,9 @@ public ForyJsonBuilder withBufferSizeLimitBytes(int bufferSizeLimitBytes) { * be thread-safe. Building snapshots the registration map, although the registered codec objects * themselves are intentionally shared. * + *

Resolver-owned {@link ObjectCodec} instances cannot be registered directly. Register a + * {@link JsonCodecFactory} that creates the object codec for the receiving resolver instead. + * *

Exact registration is rejected for primitive and boxed scalar types, {@link String}, {@link * CharSequence}, {@link Number}, standard big-number, UUID, and {@code java.time} scalar types, * plus {@code byte[]}, {@code String[]}, and {@code long[]}. Those types are owned by dedicated @@ -271,6 +275,10 @@ public ForyJsonBuilder withTypeChecker(JsonTypeChecker typeChecker) { /** Builds a JSON runtime from the current builder state. */ public ForyJson build() { + return new ForyJson(buildConfig()); + } + + JsonConfig buildConfig() { ClassLoader fixedClassLoader = classLoader; if (fixedClassLoader == null) { fixedClassLoader = Thread.currentThread().getContextClassLoader(); @@ -283,23 +291,22 @@ public ForyJson build() { asyncCompilationEnabled && effectiveCodegen && !GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE; ModuleInstaller.InstalledModules installed = ModuleInstaller.install(new ArrayList<>(modules), codecRegistry, mixins); - return new ForyJson( - new JsonConfig( - writeNullFields, - effectiveCodegen, - effectiveAsyncCompilation, - propertyDiscoveryEnabled, - propertyNamingStrategy, - fixedClassLoader, - maxDepth, - maxCachedFieldNames, - maxGraphMemoryBytes, - concurrencyLevel, - bufferSizeLimitBytes, - installed.codecs, - installed.mixins, - installed.factories, - installed.factoryIdentities, - typeChecker)); + return new JsonConfig( + writeNullFields, + effectiveCodegen, + effectiveAsyncCompilation, + propertyDiscoveryEnabled, + propertyNamingStrategy, + fixedClassLoader, + maxDepth, + maxCachedFieldNames, + maxGraphMemoryBytes, + concurrencyLevel, + bufferSizeLimitBytes, + installed.codecs, + installed.mixins, + installed.factories, + installed.factoryIdentities, + typeChecker); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonModule.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonModule.java index fd5161a00a..f832a09fef 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonModule.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonModule.java @@ -25,7 +25,7 @@ public interface ForyJsonModule { * Returns the deterministic semantic identity of this module configuration. * *

A configurable module must override this method and include every setting that can change - * codec selection or generated source. The key must not contain secrets or process-local state. + * its installed JSON behavior. The key must not contain secrets or process-local state. */ default String moduleKey() { return getClass().getName(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java index c74e03c651..234ddef9e6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java @@ -45,8 +45,8 @@ default String factoryKey() { /** * Returns runtime classes represented by an exact closed root codec. * - *

The list must not contain a dedicated reader/writer scalar type or {@code byte[]}, {@code - * String[]}, or {@code long[]}. + *

Dedicated reader/writer scalar types, {@code byte[]}, {@code String[]}, and {@code long[]} + * are not allowed. */ @Internal default List> handledRuntimeClasses() { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java b/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java index 4c9c63fccb..a0cf3f2ed5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ModuleContext.java @@ -20,18 +20,20 @@ package org.apache.fory.json; import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.codec.ObjectCodec; /** Build-time registration surface exposed to a {@link ForyJsonModule}. */ public interface ModuleContext { /** * Registers a complete codec for one eligible exact class. Dedicated reader/writer scalar types - * and {@code byte[]}, {@code String[]}, and {@code long[]} cannot be registered exactly. + * and {@code byte[]}, {@code String[]}, and {@code long[]} cannot be registered exactly. A + * resolver-owned {@link ObjectCodec} must be supplied through a {@link JsonCodecFactory}. */ void registerCodec(Class type, JsonValueCodec codec); /** - * Registers a resolver-owned codec factory for one eligible exact class. The same protected - * built-in types as {@link #registerCodec(Class, JsonValueCodec)} are rejected. + * Registers a resolver-owned codec factory for one eligible exact class. The same types as {@link + * #registerCodec(Class, JsonValueCodec)} are rejected. */ void registerCodec(Class type, JsonCodecFactory factory); 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 c5d48d7477..f5a511ae7f 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 @@ -33,9 +33,9 @@ * 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. Fory JSON always generates reachable models for the - * default configuration, then adds generated codecs for every returned configuration. Equal exact - * model-role keys reuse one generated class. At runtime, an exact miss uses the interpreted codec. - * The provider package does not need to be exported or opened to Fory. + * default configuration, then adds generated codecs for every returned configuration. Models + * without a matching generated codec use the interpreted codec. The provider package does not need + * to be exported or opened to Fory. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 22ec1218e4..cf3499eec8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -175,15 +175,6 @@ private static ArrayCodec bind(ArrayCodec codec) { return (ArrayCodec) codec; } - /** Returns whether {@code codec} is the canonical protected exact-array implementation. */ - @Internal - public static boolean isCanonicalProtectedCodec(Class arrayType, Object codec) { - return arrayType == byte[].class && codec == ByteArrayCodec.INSTANCE - || arrayType == String[].class - && (codec == StringArrayCodec.INSTANCE || codec == StringArrayCodec.NON_NULL) - || arrayType == long[].class && codec == LongArrayCodec.INSTANCE; - } - // Package visibility lets Java 8 nested codecs call these helpers without synthetic accessors. static void reserveReferenceBatch(JsonReader reader, int size) { // Reserve each batch before reading its final element. This bounds unreserved reference storage diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java index a2a4056b7e..4f23740666 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java @@ -22,7 +22,12 @@ import java.lang.reflect.Method; import org.apache.fory.annotation.Internal; -/** Exact parent-carrier operations for a semantic leaf which is not transparent to its carrier. */ +/** + * Exact parent-carrier operations for a semantic leaf which is not transparent to its carrier. + * + *

The generated operations must depend only on the codec implementation class and logical + * serialized class. Resolver-local instance state must not select different methods. + */ @Internal public interface DirectUnboxedValueCodec extends UnboxedValueCodec { /** Returns the exact static {@code (JsonReader) -> carrier} generated invocation. */ diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java index 041480442a..a4551c784d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java @@ -24,7 +24,13 @@ import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.resolver.JsonTypeInfo; -/** Exact terminal conversion for a logical value transparent to one underlying JSON type. */ +/** + * Exact terminal conversion for a logical value transparent to one underlying JSON type. + * + *

The terminal type and every generated operation, including terminal direct operations and + * graph charges, must depend only on the codec implementation class and logical serialized class. + * Resolver-local instance state must not change them. + */ @Internal public interface TransparentUnboxedValueCodec extends UnboxedValueCodec { /** Returns the already-bound terminal value type. */ diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java index 66cb04a382..8a5c6c6830 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java @@ -19,11 +19,6 @@ package org.apache.fory.json.codegen; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Field; -import java.lang.reflect.Member; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.IdentityHashMap; @@ -33,9 +28,7 @@ /** Exact, source-independent identity of one generated JSON capability class. */ @Internal public final class GeneratedCodecKey { - private static final int CLASS_VERSION = 1; - - /** Generated capability roles whose classes have independent source shapes. */ + /** Generated capabilities with independent implementations. */ public enum Role { STRING_WRITER("StringWriter"), UTF8_WRITER("Utf8Writer"), @@ -56,157 +49,35 @@ public String classSuffix() { } } - /** - * Stable JVM identity for one reflected member without retaining a reflection-object identity. - */ - public static final class MemberDescriptor { - private final Class declaringClass; - private final byte kind; - private final String name; - private final String descriptor; - private final int hash; - - private MemberDescriptor(Class declaringClass, byte kind, String name, String descriptor) { - this.declaringClass = declaringClass; - this.kind = kind; - this.name = name; - this.descriptor = descriptor; - hash = - (((System.identityHashCode(declaringClass) * 31 + kind) * 31 + name.hashCode()) * 31) - + descriptor.hashCode(); - } - - public static MemberDescriptor of(Member member) { - if (member == null) { - return null; - } - if (member instanceof Field) { - Field field = (Field) member; - return new MemberDescriptor( - field.getDeclaringClass(), (byte) 1, field.getName(), descriptor(field.getType())); - } - Executable executable = (Executable) member; - StringBuilder descriptor = new StringBuilder("("); - for (Class parameter : executable.getParameterTypes()) { - descriptor.append(descriptor(parameter)); - } - descriptor.append(')'); - byte kind; - String name; - if (executable instanceof Constructor) { - kind = 2; - name = ""; - descriptor.append('V'); - } else { - kind = 3; - name = executable.getName(); - descriptor.append(descriptor(((Method) executable).getReturnType())); - } - return new MemberDescriptor( - executable.getDeclaringClass(), kind, name, descriptor.toString()); - } - - public Class declaringClass() { - return declaringClass; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof MemberDescriptor)) { - return false; - } - MemberDescriptor that = (MemberDescriptor) other; - return declaringClass == that.declaringClass - && kind == that.kind - && name.equals(that.name) - && descriptor.equals(that.descriptor); - } - - @Override - public int hashCode() { - return hash; - } - - private static String descriptor(Class type) { - if (type.isPrimitive()) { - 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"; - } - return "D"; - } - if (type.isArray()) { - return type.getName().replace('.', '/'); - } - return "L" + type.getName().replace('.', '/') + ";"; - } - } - private final Class targetClass; private final Role role; private final Object[] keyParts; - private final Class[] referencedClasses; - private final Class anchorClass; private final int hash; - private GeneratedCodecKey( - Class targetClass, - Role role, - Object[] keyParts, - Class[] referencedClasses, - Class preferredAnchor) { + private GeneratedCodecKey(Class targetClass, Role role, Object[] keyParts) { this.targetClass = Objects.requireNonNull(targetClass); this.role = Objects.requireNonNull(role); this.keyParts = keyParts.clone(); - this.referencedClasses = uniqueClasses(targetClass, referencedClasses, keyParts); - anchorClass = anchor(preferredAnchor, this.referencedClasses); + // Hosted keys are reconstructed at Native runtime. Class names keep hashes stable across that + // boundary; equals still uses Class identity so same-named loader classes remain distinct. hash = - ((System.identityHashCode(targetClass) * 31 + role.hashCode()) * 31 + CLASS_VERSION) * 31 - + valuesHash(this.keyParts); + (targetClass.getName().hashCode() * 31 + role.ordinal()) * 31 + valuesHash(this.keyParts); } - public static GeneratedCodecKey object( - Class targetClass, Role role, Object[] keyParts, Class[] referencedClasses) { - if (role == Role.UTF8_COLLECTION_WRITER || role == Role.UTF8_COLLECTION_READER) { + public static GeneratedCodecKey object(Class targetClass, Role role, Object[] keyParts) { + if (collectionRole(role)) { throw new IllegalArgumentException("Collection role requires a collection key"); } - return new GeneratedCodecKey(targetClass, role, keyParts, referencedClasses, targetClass); + return new GeneratedCodecKey(targetClass, role, keyParts); } public static GeneratedCodecKey collection( Class collectionClass, Class elementClass, Role role, boolean stringElements) { - if (role != Role.UTF8_COLLECTION_WRITER && role != Role.UTF8_COLLECTION_READER) { + if (!collectionRole(role)) { throw new IllegalArgumentException("Object role requires an object key"); } return new GeneratedCodecKey( - collectionClass, - role, - new Object[] {collectionClass, elementClass, stringElements}, - new Class[] {elementClass, collectionClass}, - elementClass); + collectionClass, role, new Object[] {elementClass, stringElements}); } public Class targetClass() { @@ -217,14 +88,46 @@ public Role role() { return role; } + /** Returns the element class which owns collection-codec generation. */ + public Class collectionElementClass() { + requireCollectionRole(); + return (Class) keyParts[0]; + } + + /** Returns whether the collection generator uses its String-specialized body. */ + public boolean stringCollectionElements() { + requireCollectionRole(); + return (Boolean) keyParts[1]; + } + /** Returns the first application-owned class whose lifecycle may retain this key. */ public Class anchorClass() { - return anchorClass; + Class preferred = collectionRole(role) ? collectionElementClass() : targetClass; + if (preferred.getClassLoader() != null) { + return preferred; + } + if (targetClass.getClassLoader() != null) { + return targetClass; + } + for (Object keyPart : keyParts) { + if (keyPart instanceof Class && ((Class) keyPart).getClassLoader() != null) { + return (Class) keyPart; + } + } + return preferred; } /** Returns the identity-deduplicated classes required by compilation in canonical order. */ public Class[] referencedClasses() { - return referencedClasses.clone(); + ArrayList> classes = new ArrayList<>(); + IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); + addClass(targetClass, classes, seen); + for (Object keyPart : keyParts) { + if (keyPart instanceof Class) { + addClass((Class) keyPart, classes, seen); + } + } + return classes.toArray(new Class[0]); } @Override @@ -238,7 +141,7 @@ public boolean equals(Object other) { GeneratedCodecKey that = (GeneratedCodecKey) other; return targetClass == that.targetClass && role == that.role - && valuesEqual(keyParts, that.keyParts); + && Arrays.equals(keyParts, that.keyParts); } @Override @@ -246,73 +149,21 @@ public int hashCode() { return hash; } - private static Class[] uniqueClasses(Class target, Class[] explicit, Object[] keyParts) { - ArrayList> classes = new ArrayList<>(); - IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); - addClass(target, classes, seen); - for (Class type : explicit) { - addClass(type, classes, seen); - } - collectClasses(keyParts, classes, seen); - return classes.toArray(new Class[0]); - } - - private static void collectClasses( - Object value, ArrayList> classes, IdentityHashMap, Boolean> seen) { - if (value instanceof Class) { - addClass((Class) value, classes, seen); - } else if (value instanceof MemberDescriptor) { - addClass(((MemberDescriptor) value).declaringClass, classes, seen); - } else if (value instanceof Object[]) { - for (Object item : (Object[]) value) { - collectClasses(item, classes, seen); - } - } - } - private static void addClass( Class type, ArrayList> classes, IdentityHashMap, Boolean> seen) { - if (type != null && seen.put(type, Boolean.TRUE) == null) { + if (seen.put(type, Boolean.TRUE) == null) { classes.add(type); } } - private static Class anchor(Class preferred, Class[] classes) { - if (preferred.getClassLoader() != null) { - return preferred; + private void requireCollectionRole() { + if (!collectionRole(role)) { + throw new IllegalStateException("Object key has no collection inputs"); } - for (Class type : classes) { - if (type.getClassLoader() != null) { - return type; - } - } - return preferred; } - private static boolean valuesEqual(Object[] left, Object[] right) { - if (left.length != right.length) { - return false; - } - for (int i = 0; i < left.length; i++) { - Object a = left[i]; - Object b = right[i]; - if (a instanceof Class || b instanceof Class) { - if (a != b) { - return false; - } - } else if (a instanceof Object[] && b instanceof Object[]) { - if (!valuesEqual((Object[]) a, (Object[]) b)) { - return false; - } - } else if (a instanceof byte[] && b instanceof byte[]) { - if (!Arrays.equals((byte[]) a, (byte[]) b)) { - return false; - } - } else if (!Objects.equals(a, b)) { - return false; - } - } - return true; + private static boolean collectionRole(Role role) { + return role == Role.UTF8_COLLECTION_WRITER || role == Role.UTF8_COLLECTION_READER; } private static int valuesHash(Object[] values) { @@ -320,13 +171,15 @@ private static int valuesHash(Object[] values) { for (Object value : values) { int valueHash; if (value instanceof Class) { - valueHash = System.identityHashCode(value); - } else if (value instanceof Object[]) { - valueHash = valuesHash((Object[]) value); - } else if (value instanceof byte[]) { - valueHash = Arrays.hashCode((byte[]) value); + valueHash = ((Class) value).getName().hashCode(); + } else if (value instanceof Enum) { + Enum enumValue = (Enum) value; + valueHash = enumValue.getDeclaringClass().getName().hashCode() * 31 + enumValue.ordinal(); + } else if (value instanceof Boolean || value instanceof Integer || value instanceof String) { + valueHash = value.hashCode(); } else { - valueHash = Objects.hashCode(value); + throw new IllegalArgumentException( + "Unsupported generated codec key part " + value.getClass().getName()); } hash = hash * 31 + valueHash; } 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 b130873a0f..b6cc0571f6 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 @@ -23,7 +23,6 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.LinkedHashMap; @@ -41,8 +40,6 @@ import org.apache.fory.codegen.JaninoUtils.DirectInvocation; import org.apache.fory.collection.ClassValueCache; import org.apache.fory.json.ForyJsonException; -import org.apache.fory.json.codec.CodecUtils; -import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.Latin1ReaderCodec; @@ -63,7 +60,6 @@ import org.apache.fory.platform.JdkVersion; import org.apache.fory.platform.internal.DefineClass; import org.apache.fory.platform.internal._JDKAccess; -import org.apache.fory.reflect.TypeRef; import org.apache.fory.util.ClassLoaderUtils; /** @@ -137,106 +133,84 @@ private JsonCodegen( * depends on mutable capability slots. Active codec classes are inspected only for non-canonical * bindings, whose capability fields are never replaced by generated raw-object codecs. * - *

The shared registry caches the resulting class future for every pooled resolver of one Fory - * JSON instance. Resolver-local construction and capability publication belong to {@link + *

The shared registry owns resolver-graph completion futures; they coordinate atomic + * resolver-local installation, not compilation or class-definition single-flight. Resolver-local + * construction and capability publication belong to {@link * org.apache.fory.json.resolver.JsonTypeResolver} and are ordered by its {@link JsonJITContext}. */ @Internal public Class compileStringWriter( GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { - return compileObject(key, codec, compiler -> compiler.buildStringWriter(codec, resolver)); + return compileObject(key, compiler -> compiler.buildStringWriter(codec, resolver)); } @Internal public Class compileUtf8Writer( GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { - return compileObject(key, codec, compiler -> compiler.buildUtf8Writer(codec, resolver)); + return compileObject(key, compiler -> compiler.buildUtf8Writer(codec, resolver)); } @Internal public Class compileLatin1Reader( GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { - return compileObject(key, codec, compiler -> compiler.buildLatin1Reader(codec, resolver)); + return compileObject(key, compiler -> compiler.buildLatin1Reader(codec, resolver)); } @Internal public Class compileUtf16Reader( GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { - return compileObject(key, codec, compiler -> compiler.buildUtf16Reader(codec, resolver)); + return compileObject(key, compiler -> compiler.buildUtf16Reader(codec, resolver)); } @Internal public Class compileUtf8Reader( GeneratedCodecKey key, ObjectCodec codec, JsonTypeResolver resolver) { - return compileObject(key, codec, compiler -> compiler.buildUtf8Reader(codec, resolver)); + return compileObject(key, compiler -> compiler.buildUtf8Reader(codec, resolver)); } @Internal - public Class compileUtf8CollectionWriter( - GeneratedCodecKey key, TypeRef declaredType, CollectionCodec owner) { - Type type = declaredType.getType(); - Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + public Class compileUtf8CollectionWriter(GeneratedCodecKey key) { + Class elementType = key.collectionElementClass(); + String generatedPackage = CodeGenerator.getPackage(elementType); return compile( key, - CodeGenerator.getPackage(elementType), - compiler -> compiler.buildUtf8CollectionWriter(declaredType, owner)); + elementType, + compiler -> + compiler.buildUtf8CollectionWriter(generatedPackage, key.stringCollectionElements())); } - private Class buildUtf8CollectionWriter(TypeRef declaredType, CollectionCodec owner) { - Type type = declaredType.getType(); - Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); - String generatedPackage = CodeGenerator.getPackage(elementType); - boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec; + private Class buildUtf8CollectionWriter(String generatedPackage, boolean stringElements) { String className = className(); String code = new Utf8CollectionWriterCodegen().genCode(generatedPackage, className, stringElements); - return compileCollectionCodecClass(elementType, generatedPackage, className, code); + return compileCodecClass(generatedPackage, className, code); } @Internal - public Class compileUtf8CollectionReader( - GeneratedCodecKey key, TypeRef declaredType, CollectionCodec owner) { - Type type = declaredType.getType(); - Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + public Class compileUtf8CollectionReader(GeneratedCodecKey key) { + Class elementType = key.collectionElementClass(); + String generatedPackage = CodeGenerator.getPackage(elementType); return compile( key, - CodeGenerator.getPackage(elementType), - compiler -> compiler.buildUtf8CollectionReader(declaredType, owner)); + elementType, + compiler -> + compiler.buildUtf8CollectionReader(generatedPackage, key.stringCollectionElements())); } - private Class buildUtf8CollectionReader(TypeRef declaredType, CollectionCodec owner) { - if (!owner.createsArrayList()) { - throw new IllegalArgumentException( - "Generated UTF-8 collection requires an ArrayList binding"); - } - Type type = declaredType.getType(); - Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); - String generatedPackage = CodeGenerator.getPackage(elementType); - boolean stringElements = owner instanceof CollectionCodec.StringCollectionCodec; + private Class buildUtf8CollectionReader(String generatedPackage, boolean stringElements) { String className = className(); String code = new Utf8CollectionReaderCodegen().genCode(generatedPackage, className, stringElements); - return compileCollectionCodecClass(elementType, generatedPackage, className, code); + return compileCodecClass(generatedPackage, className, code); } - private Class compileObject( - GeneratedCodecKey key, ObjectCodec owner, CompilerOperation operation) { - return compile( - key, - CodeGenerator.getPackage(owner.type()), - compiler -> { - boolean writer = - key.role() == GeneratedCodecKey.Role.STRING_WRITER - || key.role() == GeneratedCodecKey.Role.UTF8_WRITER; - if (writer ? !compiler.canCompileWriter(owner) : !compiler.canCompileReader(owner)) { - return null; - } - return operation.compile(compiler); - }); + private Class compileObject(GeneratedCodecKey key, CompilerOperation operation) { + return compile(key, key.targetClass(), operation); } private Class compile( - GeneratedCodecKey key, String generatedPackage, CompilerOperation operation) { + GeneratedCodecKey key, Class sourceOwner, CompilerOperation operation) { + String generatedPackage = CodeGenerator.getPackage(sourceOwner); PerClassGeneratedCodecCache perClass = generatedClasses.get(key.anchorClass(), PerClassGeneratedCodecCache::new); CacheEntry entry = @@ -248,13 +222,13 @@ private Class compile( + key.role().classSuffix() + "ForyJsonCodec_" + GENERATED_CLASS_SUFFIX.incrementAndGet(); - return new CacheEntry(qualifiedClassName(generatedPackage, className), className); + return new CacheEntry(className); }); Class completed = entry.generatedClass; if (completed != null) { return completed; } - JsonCodegen compiler = compiler(key, entry.className, generatedPackage); + JsonCodegen compiler = compiler(key, sourceOwner, entry.className, generatedPackage); Class generatedClass = operation.compile(compiler); if (generatedClass != null) { entry.publish(generatedClass); @@ -262,7 +236,8 @@ private Class compile( return generatedClass; } - private JsonCodegen compiler(GeneratedCodecKey key, String className, String generatedPackage) { + private JsonCodegen compiler( + GeneratedCodecKey key, Class sourceOwner, String className, String generatedPackage) { ClassLoader[] loaders = canonicalLoaders(key); if (hostedCodegen) { // Use the canonical loader tuple only for source compilation and visibility decisions. The @@ -271,11 +246,7 @@ private JsonCodegen compiler(GeneratedCodecKey key, String className, String gen ClassLoader loader = loaders.length == 1 ? loaders[0] : new ClassLoaderUtils.ComposedClassLoader(loaders); return new JsonCodegen( - null, - loader, - true, - hostedDefinitionOwner(key.targetClass(), generatedPackage), - className); + null, loader, true, hostedDefinitionOwner(sourceOwner, generatedPackage), className); } CodeGenerator generator = loaders.length == 1 @@ -323,19 +294,17 @@ private static final class PerClassGeneratedCodecCache { } private static final class CacheEntry { - private final String binaryName; private final String className; private volatile Class generatedClass; - private CacheEntry(String binaryName, String className) { - this.binaryName = binaryName; + private CacheEntry(String className) { this.className = className; } private void publish(Class generatedClass) { Class completed = this.generatedClass; if (completed != null && completed != generatedClass) { - throw new IllegalStateException("Conflicting generated JSON class " + binaryName); + throw new IllegalStateException("Conflicting generated JSON class " + className); } this.generatedClass = generatedClass; } @@ -505,7 +474,7 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv String code = new StringWriterCodegen(this, resolver, codec) .genUnwrappedWriterCode(builder, type, codec, unwrapped); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.writeFields(); @@ -515,7 +484,7 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv String code = new StringWriterCodegen(this, resolver, codec) .genAnyWriterCode(builder, type, properties, any); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } Function source = groupEnds -> { @@ -525,7 +494,6 @@ private Class buildStringWriter(ObjectCodec codec, JsonTypeResolver resolv .genWriterCode(builder, type, properties, groupEnds); }; return compileWriterClass( - type, generatedPackage, className, properties, @@ -547,7 +515,7 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver String code = new Utf8WriterCodegen(this, resolver, codec, false) .genUnwrappedWriterCode(builder, type, codec, unwrapped); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.writeFields(); @@ -557,7 +525,7 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver String code = new Utf8WriterCodegen(this, resolver, codec, false) .genAnyWriterCode(builder, type, properties, any); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } Function normalSource = groupEnds -> { @@ -589,12 +557,10 @@ private Class buildUtf8Writer(ObjectCodec codec, JsonTypeResolver resolver int expandedSize = methodSize(codeStats(generatedPackage, className, expandedSource), "writeUtf8"); if (expandedSize > HOT_INLINE_LIMIT) { - return compileObjectCodecClass( - type, generatedPackage, className, expandedSource, invocations); + return compileCodecClass(generatedPackage, className, expandedSource, invocations); } } return compileUtf8WriterClass( - type, generatedPackage, className, properties, @@ -616,7 +582,7 @@ private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolv String code = new Latin1ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.readFields(); @@ -630,7 +596,6 @@ private Class buildLatin1Reader(ObjectCodec codec, JsonTypeResolver resolv : reader.genAnyReaderCode(builder, codec, properties, codec.creatorInfo(), any); }; return compileReaderClass( - type, generatedPackage, className, properties.length, @@ -652,7 +617,7 @@ private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolve String code = new Utf16ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.readFields(); @@ -666,7 +631,6 @@ private Class buildUtf16Reader(ObjectCodec codec, JsonTypeResolver resolve : reader.genAnyReaderCode(builder, codec, properties, codec.creatorInfo(), any); }; return compileReaderClass( - type, generatedPackage, className, properties.length, @@ -688,7 +652,7 @@ private Class buildUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver String code = new Utf8ReaderCodegen(this, resolver) .genUnwrappedReaderCode(builder, type, codec, unwrapped); - return compileObjectCodecClass(type, generatedPackage, className, code, invocations); + return compileCodecClass(generatedPackage, className, code, invocations); } AnyInfo any = codec.anyInfo(); JsonFieldInfo[] properties = codec.readFields(); @@ -702,7 +666,6 @@ private Class buildUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver : reader.genAnyReaderCode(builder, codec, properties, codec.creatorInfo(), any); }; return compileReaderClass( - type, generatedPackage, className, properties.length, @@ -713,7 +676,6 @@ private Class buildUtf8Reader(ObjectCodec codec, JsonTypeResolver resolver } private Class compileReaderClass( - Class ownerType, String generatedPackage, String className, int propertyCount, @@ -725,12 +687,10 @@ private Class compileReaderClass( groupable ? readerGroupEnds(generatedPackage, className, propertyCount, readMethod, source) : oneGroup(propertyCount); - return compileObjectCodecClass( - ownerType, generatedPackage, className, source.apply(groupEnds), invocations); + return compileCodecClass(generatedPackage, className, source.apply(groupEnds), invocations); } private Class compileWriterClass( - Class ownerType, String generatedPackage, String className, JsonFieldInfo[] properties, @@ -739,8 +699,7 @@ private Class compileWriterClass( Function source, DirectInvocation[] invocations) { if (properties.length < 2) { - return compileObjectCodecClass( - ownerType, generatedPackage, className, source.apply(null), invocations); + return compileCodecClass(generatedPackage, className, source.apply(null), invocations); } // 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. @@ -748,8 +707,7 @@ private Class compileWriterClass( JaninoUtils.CodeStats oneGroupStats = codeStats(generatedPackage, className, source.apply(oneGroup)); if (privateMethodSize(oneGroupStats, writeMethod + "Object") <= HOT_INLINE_LIMIT) { - return compileObjectCodecClass( - ownerType, generatedPackage, className, source.apply(null), invocations); + return compileCodecClass(generatedPackage, className, source.apply(null), invocations); } int[] groupEnds = writerGroupEnds( @@ -760,12 +718,10 @@ private Class compileWriterClass( writeMethod, memberMethod, source); - return compileObjectCodecClass( - ownerType, generatedPackage, className, source.apply(groupEnds), invocations); + return compileCodecClass(generatedPackage, className, source.apply(groupEnds), invocations); } private Class compileUtf8WriterClass( - Class ownerType, String generatedPackage, String className, JsonFieldInfo[] properties, @@ -776,23 +732,19 @@ private Class compileUtf8WriterClass( if (properties.length < 2 || methodSize(codeStats(generatedPackage, className, directSource), writeMethod) <= HOT_INLINE_LIMIT) { - return compileObjectCodecClass( - ownerType, generatedPackage, className, directSource, invocations); + return compileCodecClass(generatedPackage, className, directSource, invocations); } int firstGroupMember = JsonWriterCodegen.firstGroupMember(properties); if (properties.length - firstGroupMember < 2) { - return compileObjectCodecClass( - ownerType, generatedPackage, className, directSource, invocations); + return compileCodecClass(generatedPackage, className, directSource, invocations); } int[] groupEnds = utf8WriterGroupEnds( generatedPackage, className, properties.length, firstGroupMember, writeMethod, source); if (groupEnds.length < 2) { - return compileObjectCodecClass( - ownerType, generatedPackage, className, directSource, invocations); + return compileCodecClass(generatedPackage, className, directSource, invocations); } - return compileObjectCodecClass( - ownerType, generatedPackage, className, source.apply(groupEnds), invocations); + return compileCodecClass(generatedPackage, className, source.apply(groupEnds), invocations); } private int[] utf8WriterGroupEnds( @@ -970,31 +922,15 @@ private int[] toIntArray(List values) { return result; } - private Class compileObjectCodecClass( - Class ownerType, - String generatedPackage, - String className, - String code, - DirectInvocation[] invocations) { - if (!hostedCodegen) { - return compileCodecClass(generatedPackage, className, code, invocations); - } - Class definitionOwner = hostedDefinitionOwner(ownerType, generatedPackage); - if (definitionOwner == null) { - return null; - } - try { - CompileUnit unit = new CompileUnit(generatedPackage, className, code); - return compileHostedClass(definitionOwner, unit, invocations); - } catch (Throwable e) { - throw new ForyJsonException("Cannot compile generated JSON codec " + className, e); - } - } - private Class compileCodecClass( String generatedPackage, String className, String code, DirectInvocation[] invocations) { try { CompileUnit unit = new CompileUnit(generatedPackage, className, code); + if (hostedCodegen) { + return hostedDefinitionOwner == null + ? null + : compileHostedClass(hostedDefinitionOwner, unit, invocations); + } ClassLoader classLoader = codeGenerator.compileDirect(unit, invocations); return classLoader.loadClass(qualifiedClassName(generatedPackage, className)); } catch (Throwable e) { @@ -1006,21 +942,6 @@ private Class compileCodecClass(String generatedPackage, String className, St return compileCodecClass(generatedPackage, className, code, new DirectInvocation[0]); } - private Class compileCollectionCodecClass( - Class elementType, String generatedPackage, String className, String code) { - if (!hostedCodegen) { - return compileCodecClass(generatedPackage, className, code); - } - Class definitionOwner = hostedDefinitionOwner(elementType, generatedPackage); - if (definitionOwner == null) { - return null; - } - return compileHostedClass( - definitionOwner, - new CompileUnit(generatedPackage, className, code), - new DirectInvocation[0]); - } - private static Class hostedDefinitionOwner(Class sourceOwner, String generatedPackage) { while (sourceOwner.isArray()) { sourceOwner = sourceOwner.getComponentType(); @@ -1081,8 +1002,7 @@ private Class compileHostedClass( return mainClass; } - @Internal - public boolean canCompileWriter(ObjectCodec codec) { + private boolean canCompileWriter(ObjectCodec codec) { if (codec.fixedInstance() || !canCompileType(codec.type())) { return false; } @@ -1103,7 +1023,7 @@ public boolean canCompileWriter(ObjectCodec codec) { /** Checks source visibility through the same canonical loader tuple used by compilation. */ @Internal public boolean canCompileWriter(GeneratedCodecKey key, ObjectCodec codec) { - return compiler(key, "ForyJsonCodecProbe", CodeGenerator.getPackage(codec.type())) + return compiler(key, codec.type(), "ForyJsonCodecProbe", CodeGenerator.getPackage(codec.type())) .canCompileWriter(codec); } @@ -1130,8 +1050,7 @@ private boolean canCompileUnwrappedWrite( return any == null || canCompileAnyWrite(any); } - @Internal - public boolean canCompileReader(ObjectCodec codec) { + private boolean canCompileReader(ObjectCodec codec) { if (codec.fixedInstance() || !canCompileType(codec.type())) { return false; } @@ -1156,7 +1075,7 @@ public boolean canCompileReader(ObjectCodec codec) { /** Checks source visibility through the same canonical loader tuple used by compilation. */ @Internal public boolean canCompileReader(GeneratedCodecKey key, ObjectCodec codec) { - return compiler(key, "ForyJsonCodecProbe", CodeGenerator.getPackage(codec.type())) + return compiler(key, codec.type(), "ForyJsonCodecProbe", CodeGenerator.getPackage(codec.type())) .canCompileReader(codec); } @@ -1275,11 +1194,7 @@ Class stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) return StringWriterCodec.class; } Object codec = typeInfo.stringWriter(); - Class type = codec.getClass(); - if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { - return type; - } - return StringWriterCodec.class; + return codecFieldType(typeInfo, codec.getClass(), StringWriterCodec.class); } Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1293,11 +1208,7 @@ Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { return Utf8WriterCodec.class; } Object codec = typeInfo.utf8Writer(); - Class type = codec.getClass(); - if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { - return type; - } - return Utf8WriterCodec.class; + return codecFieldType(typeInfo, codec.getClass(), Utf8WriterCodec.class); } Class latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1307,11 +1218,8 @@ Class latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) if (resolver.canonicalObjectCodec(typeInfo) != null) { return Latin1ReaderCodec.class; } - Class type = typeInfo.latin1Reader().getClass(); - if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { - return type; - } - return Latin1ReaderCodec.class; + return codecFieldType( + typeInfo, typeInfo.latin1Reader().getClass(), Latin1ReaderCodec.class); } Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1321,11 +1229,8 @@ Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf16ReaderCodec.class; } - Class type = typeInfo.utf16Reader().getClass(); - if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { - return type; - } - return Utf16ReaderCodec.class; + return codecFieldType( + typeInfo, typeInfo.utf16Reader().getClass(), Utf16ReaderCodec.class); } Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1338,17 +1243,20 @@ Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf8ReaderCodec.class; } - Class type = typeInfo.utf8Reader().getClass(); - if (isPublicSourceType(type) && isGeneratedClassVisible(type)) { - return type; - } - return Utf8ReaderCodec.class; + return codecFieldType(typeInfo, typeInfo.utf8Reader().getClass(), Utf8ReaderCodec.class); } - static boolean storesReadObjectCodec( - Class type, JsonFieldInfo property, JsonTypeResolver resolver) { - Class nestedType = resolver.readNestedType(property); - return nestedType != null && nestedType != type; + private Class codecFieldType( + JsonTypeInfo typeInfo, Class codecClass, Class capabilityType) { + // Native-hosted object classes must be reusable by the same exact key at image runtime, where + // registered codec instances are reconstructed independently. Keep ordinary registered codecs + // behind the stable role interface; direct unboxed operations are handled separately. + if (hostedCodegen && typeInfo.registeredCodecClass() != null) { + return capabilityType; + } + return isCodecClassSourceAccessible(codecClass) && isGeneratedClassVisible(codecClass) + ? codecClass + : capabilityType; } @Internal @@ -1403,7 +1311,7 @@ private boolean canCompileWrite(JsonFieldInfo property) { if (field != null && !canCompileField(field)) { return false; } - Class rawType = property.writeRawType(); + Class rawType = property.writeAccessorType(); if (rawType != null && !rawType.isPrimitive() && !isGeneratedClassVisible(rawType)) { return false; } @@ -1425,7 +1333,7 @@ private boolean canCompileRead(JsonFieldInfo property) { if (property.readSetter() == null && property.readField() == null) { return false; } - Class rawType = property.readRawType(); + Class rawType = property.readAccessorType(); if (rawType != null && !rawType.isPrimitive() && !isGeneratedClassVisible(rawType)) { return false; } @@ -1544,6 +1452,12 @@ private boolean isDefinitionModuleVisible(Class type) throws ReflectiveOperat .invoke(typeModule, packageName, ownerModule); } + /** Returns whether a codec implementation can be named in generated Java source. */ + @Internal + public static boolean isCodecClassSourceAccessible(Class codecType) { + return isPublicSourceType(codecType); + } + private boolean isVisible(Class type) { if (type.isPrimitive()) { return true; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java index afc567a143..061b9df3e4 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java @@ -229,6 +229,11 @@ public DirectUnboxedValueCodec directUnboxedValueCodec() { : null; } + /** Returns whether this argument uses its primitive reader operation directly. */ + public boolean readsDirectPrimitive() { + return typeInfo.kind().matchesPrimitive(rawType); + } + /** Throws the cold failure used by interpreted and generated readers. */ public Object rejectNullRead() { throw new ForyJsonException("JSON creator property " + name + " is not nullable"); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 7ecbb8127f..d85a121f6b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -450,6 +450,21 @@ public Class writeRawType() { return writeRawType; } + /** Returns the erased Java type exposed by the write field or getter. */ + @Internal + public Class writeAccessorType() { + return writeRawType(writeField, writeGetter); + } + + /** Returns whether the resolved write type differs from the Java member declaration. */ + @Internal + public boolean writeTypeDiffersFromDeclaration() { + Type declaredType = writeType(writeField, writeGetter); + return declaredType != null + && writeTypeRef != null + && !declaredType.equals(writeTypeRef.getType()); + } + private static Class writeRawType(Field field, Method getter) { return getter == null ? fieldRawType(field) : getter.getReturnType(); } @@ -499,6 +514,21 @@ public Class readRawType() { return readRawType; } + /** Returns the erased Java type accepted by the read field or setter. */ + @Internal + public Class readAccessorType() { + return readRawType(readField, readSetter); + } + + /** Returns whether the resolved read type differs from the Java member declaration. */ + @Internal + public boolean readTypeDiffersFromDeclaration() { + Type declaredType = readType(readField, readSetter); + return declaredType != null + && readTypeRef != null + && !declaredType.equals(readTypeRef.getType()); + } + private static Class readRawType(Field field, Method setter) { return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java index d0a9d3f8a9..32560559f6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java @@ -19,6 +19,8 @@ package org.apache.fory.json.meta; +import org.apache.fory.annotation.Internal; + /** * Semantic field families used to select interpreted and generated JSON operations. * @@ -39,5 +41,30 @@ public enum JsonFieldKind { ARRAY, COLLECTION, MAP, - OBJECT + OBJECT; + + /** Returns whether this kind uses the dedicated operation for {@code type}. */ + @Internal + public boolean matchesPrimitive(Class type) { + switch (this) { + case BOOLEAN: + return type == boolean.class; + case BYTE: + return type == byte.class; + case SHORT: + return type == short.class; + case INT: + return type == int.class; + case LONG: + return type == long.class; + case FLOAT: + return type == float.class; + case DOUBLE: + return type == double.class; + case CHAR: + return type == char.class; + default: + return false; + } + } } 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 265095beb2..2a2f6a26f1 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 @@ -48,6 +48,7 @@ import org.apache.fory.annotation.Internal; import org.apache.fory.json.JsonCodecFactory; import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.util.Preconditions; /** @@ -58,7 +59,7 @@ * from an existing {@code ForyJson}. The runtime registry reads that owned snapshot directly. */ public final class CodecRegistry { - private static final Set> PROTECTED_BUILTIN_TYPES = protectedBuiltinTypes(); + private static final Set> DEDICATED_READER_WRITER_TYPES = dedicatedReaderWriterTypes(); private final ConcurrentMap, JsonValueCodec> codecs; private final ConcurrentMap, FactoryBinding> factories; @@ -78,6 +79,10 @@ private CodecRegistry( public void register(Class type, JsonValueCodec codec) { Preconditions.checkNotNull(type); Preconditions.checkNotNull(codec); + if (codec instanceof ObjectCodec) { + throw new IllegalArgumentException( + "ObjectCodec instances are resolver-owned; register a JsonCodecFactory instead"); + } checkRegistrationType(type); codecs.put(type, codec); factories.remove(type); @@ -91,12 +96,6 @@ public void registerFactory(Class type, JsonCodecFactory factory) { codecs.remove(type); } - /** Returns whether the exact type is implemented by a protected built-in reader/writer path. */ - @Internal - public static boolean isProtectedBuiltinType(Class type) { - return PROTECTED_BUILTIN_TYPES.contains(type); - } - public JsonValueCodec get(Class type) { return codecs.get(type); } @@ -142,8 +141,8 @@ public CodecRegistry copy() { return new CodecRegistry(copied, copiedFactories); } - private static Set> protectedBuiltinTypes() { - Set> types = Collections.newSetFromMap(new IdentityHashMap<>()); + private static Set> dedicatedReaderWriterTypes() { + Set> types = new HashSet<>(); Collections.addAll( types, boolean.class, @@ -188,7 +187,7 @@ private static Set> protectedBuiltinTypes() { } private static void checkRegistrationType(Class type) { - if (isProtectedBuiltinType(type)) { + if (DEDICATED_READER_WRITER_TYPES.contains(type)) { throw new IllegalArgumentException( "JSON codec registration is not allowed for built-in type " + type.getTypeName()); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java index ada849e176..509ad4a1b3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java @@ -19,12 +19,9 @@ package org.apache.fory.json.resolver; -import java.lang.reflect.Member; -import java.lang.reflect.Method; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; -import org.apache.fory.json.codec.ClosedSubtypeCodec; +import java.util.IdentityHashMap; import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.DirectUnboxedValueCodec; @@ -34,100 +31,62 @@ import org.apache.fory.json.codec.TransparentUnboxedValueCodec; import org.apache.fory.json.codec.UnboxedValueCodec; import org.apache.fory.json.codegen.GeneratedCodecKey; -import org.apache.fory.json.codegen.GeneratedCodecKey.MemberDescriptor; import org.apache.fory.json.codegen.GeneratedCodecKey.Role; +import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.meta.JsonCreatorFieldInfo; -import org.apache.fory.json.meta.JsonCreatorInfo; -import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.meta.JsonFieldInfo; -/** Builds exact generated-codec keys from resolved JSON metadata. */ +/** Builds generated-codec keys from configuration and direct codec inputs. */ final class GeneratedCodecKeyBuilder { - private GeneratedCodecKeyBuilder() {} + private final JsonTypeResolver resolver; + private final ObjectCodec owner; + private final JsonTypeResolver.CapabilityKind kind; + private final ArrayList keyParts; + private int occurrence; - static GeneratedCodecKey object( + private GeneratedCodecKeyBuilder( + JsonTypeResolver resolver, + ObjectCodec owner, + JsonTypeResolver.CapabilityKind kind, + ArrayList keyParts) { + this.resolver = resolver; + this.owner = owner; + this.kind = kind; + this.keyParts = keyParts; + } + + static GeneratedCodecKeyBuilder object( JsonTypeResolver resolver, JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver.CapabilityKind kind) { JsonSharedRegistry registry = resolver.sharedRegistry(); ArrayList keyParts = new ArrayList<>(); - ArrayList> referencedClasses = new ArrayList<>(); - keyParts.add(registry.writeNullFields()); + if (!JsonTypeResolver.readerKind(kind)) { + keyParts.add(registry.writeNullFields()); + } keyParts.add(registry.propertyDiscoveryEnabled()); keyParts.add(registry.propertyNamingStrategy()); - addMixinKeyParts(registry, owner.type(), keyParts, referencedClasses); - if (JsonTypeResolver.readerKind(kind)) { - keyParts.add(owner.graphMemoryBytes()); - keyParts.add(owner.hasValidators()); + String factoryKey = typeInfo.objectFactoryKey(); + if (factoryKey != null) { + keyParts.add(factoryKey); } + JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); - if (unwrapped != null) { - for (JsonUnwrappedInfo.Group group : unwrapped.groups()) { - ObjectCodec child = group.childCodec(); - Class childType = child.type(); - keyParts.add(childType); - referencedClasses.add(childType); - addMixinKeyParts(registry, childType, keyParts, referencedClasses); - keyParts.add(MemberDescriptor.of(accessorMember(group.declaration().writeAccessor()))); - keyParts.add(MemberDescriptor.of(accessorMember(group.declaration().readAccessor()))); - keyParts.add(group.readIndex()); - keyParts.add(group.parent() == null ? -1 : group.parent().readIndex()); - keyParts.add(group.declaration().constructionIndex()); - Class parentType = group.parentCodec().type(); - keyParts.add(parentType); - referencedClasses.add(parentType); - keyParts.add(group.writeEnabled()); - keyParts.add(group.readEnabled()); - if (JsonTypeResolver.readerKind(kind)) { - keyParts.add(child.graphMemoryBytes()); - keyParts.add(child.hasValidators()); - addCreatorKeyParts(resolver, child.creatorInfo(), keyParts, referencedClasses, kind); - } - } - } - if (JsonTypeResolver.readerKind(kind)) { - addCreatorKeyParts(resolver, owner.creatorInfo(), keyParts, referencedClasses, kind); - JsonCreatorInfo creator = owner.creatorInfo(); - if (creator == null) { - addReadFields(resolver, owner, owner.readFields(), keyParts, referencedClasses, kind); - } else { - addCreatorFields(resolver, owner, creator.fields(), keyParts, referencedClasses, kind); - } - if (unwrapped != null) { - for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { - keyParts.add("route"); - keyParts.add(route.group().readIndex()); - if (route.field() != null) { - addReadField(resolver, owner, route.field(), keyParts, referencedClasses, kind); - } else { - addCreatorField( - resolver, owner, route.creatorField(), keyParts, referencedClasses, kind); - } - } - } - } else { - if (unwrapped != null) { - addUnwrappedWriteOrder(unwrapped.writeEntries(), keyParts, referencedClasses); - } - JsonFieldInfo[] fields = unwrapped == null ? owner.writeFields() : unwrapped.writeFields(); - for (JsonFieldInfo field : fields) { - addWriteField(resolver, owner, field, keyParts, referencedClasses, kind); - } - } - addAnyKeyParts(resolver, owner, keyParts, referencedClasses, kind); - return GeneratedCodecKey.object( - typeInfo.rawType(), - role(kind), - keyParts.toArray(), - referencedClasses.toArray(new Class[0])); + addMixins(registry, owner, unwrapped, keyParts); + addUnwrappedFactoryKeys(resolver, unwrapped, keyParts); + return new GeneratedCodecKeyBuilder(resolver, owner, kind, keyParts); + } + + GeneratedCodecKey build() { + return GeneratedCodecKey.object(owner.type(), role(kind), keyParts.toArray()); } static GeneratedCodecKey collection( JsonTypeInfo typeInfo, CollectionCodec owner, JsonTypeResolver.CapabilityKind kind) { - Type type = typeInfo.type(); - Class rawType = CodecUtils.rawType(type, Collection.class); - Class elementType = CodecUtils.rawType(CodecUtils.elementType(type), Object.class); + Class rawType = CodecUtils.rawType(typeInfo.type(), Collection.class); + Class elementType = + CodecUtils.rawType(CodecUtils.elementType(typeInfo.type()), Object.class); return GeneratedCodecKey.collection( rawType, elementType, @@ -137,307 +96,157 @@ static GeneratedCodecKey collection( owner instanceof CollectionCodec.StringCollectionCodec); } - private static void addUnwrappedWriteOrder( - JsonUnwrappedInfo.WriteEntry[] entries, - ArrayList keyParts, - ArrayList> referencedClasses) { - keyParts.add(entries.length); - for (JsonUnwrappedInfo.WriteEntry entry : entries) { - keyParts.add(entry.kind()); - if (entry.kind() == JsonUnwrappedInfo.DIRECT) { - JsonFieldInfo field = entry.field(); - keyParts.add(field.name()); - addMember(field.writeField(), keyParts, referencedClasses); - addMember(field.writeGetter(), keyParts, referencedClasses); - } else if (entry.kind() == JsonUnwrappedInfo.GROUP) { - keyParts.add(entry.group().readIndex()); - addUnwrappedWriteOrder(entry.group().writeEntries(), keyParts, referencedClasses); + private static void addMixins( + JsonSharedRegistry registry, + ObjectCodec owner, + JsonUnwrappedInfo unwrapped, + ArrayList keyParts) { + IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); + Class ownerMixin = registry.mixinType(owner.type()); + addMixin(ownerMixin, seen, keyParts); + if (unwrapped != null) { + for (JsonUnwrappedInfo.Group group : unwrapped.groups()) { + Class childType = group.childCodec().type(); + addMixin(registry.mixinType(childType), seen, keyParts); } } } - private static void addMixinKeyParts( - JsonSharedRegistry registry, - Class target, - ArrayList keyParts, - ArrayList> referencedClasses) { - Class mixin = registry.mixinType(target); - keyParts.add(mixin); - if (mixin != null) { - referencedClasses.add(mixin); + private static void addMixin( + Class mixin, + IdentityHashMap, Boolean> seen, + ArrayList keyParts) { + if (mixin != null && seen.put(mixin, Boolean.TRUE) == null) { + keyParts.add(mixin); } } - private static void addCreatorKeyParts( + private static void addUnwrappedFactoryKeys( JsonTypeResolver resolver, - JsonCreatorInfo creator, - ArrayList keyParts, - ArrayList> referencedClasses, - JsonTypeResolver.CapabilityKind kind) { - if (creator == null) { - keyParts.add(null); + JsonUnwrappedInfo unwrapped, + ArrayList keyParts) { + if (unwrapped == null) { return; } - keyParts.add("creator"); - addMember(creator.executable(), keyParts, referencedClasses); - addMember(creator.invocationExecutable(), keyParts, referencedClasses); - addMember(creator.defaultConstructor(), keyParts, referencedClasses); - keyParts.add(creator.argumentCount()); - keyParts.add(creator.defaultMaskCount()); - keyParts.add(creator.tracksArgumentPresence()); - for (int i = 0; i < creator.argumentCount(); i++) { - keyParts.add(creator.defaultMaskBit(i)); - keyParts.add(creator.hasDefault(i)); - addMember(creator.defaultMethod(i), keyParts, referencedClasses); - } - JsonFieldInfo[] deferred = creator.deferredFields(); - keyParts.add(deferred.length); - for (int i = 0; i < deferred.length; i++) { - keyParts.add(creator.deferredRequired(i)); - addReadField(resolver, null, deferred[i], keyParts, referencedClasses, kind); + JsonUnwrappedInfo.Group[] groups = unwrapped.groups(); + for (int i = 0; i < groups.length; i++) { + String factoryKey = resolver.objectFactoryKey(groups[i].childCodec()); + if (factoryKey != null) { + keyParts.add(i); + keyParts.add(factoryKey); + } } } - private static void addWriteField( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonFieldInfo field, - ArrayList keyParts, - ArrayList> referencedClasses, - JsonTypeResolver.CapabilityKind kind) { - JsonTypeInfo child = field.writeTypeInfo(); - keyParts.add("write"); - keyParts.add(field.name()); - keyParts.add(field.writeRawType()); - keyParts.add(child.rawType()); - keyParts.add(field.writeKind()); - keyParts.add(field.writeNull()); - keyParts.add(field.requiresNonNullWrite()); - keyParts.add(field.writesRawString()); - keyParts.add(field.writesUnboxedValue()); - keyParts.add(resolver.usesWriterSlot(owner, child)); - addMember(field.writeField(), keyParts, referencedClasses); - addMember(field.writeGetter(), keyParts, referencedClasses); - addCapabilityKeyParts(resolver, child, kind, keyParts, referencedClasses); - addUnboxedKeyParts(field.writeUnboxedValueCodec(), false, keyParts, referencedClasses); - addClass(field.writeRawType(), referencedClasses); - addClass(child.rawType(), referencedClasses); + void addAny(boolean storesCapability) { + AnyInfo any = owner.anyInfo(); + if (any == null || !storesCapability) { + return; + } + boolean slot = + JsonTypeResolver.readerKind(kind) + ? resolver.usesReaderSlot(owner, any.valueTypeInfo()) + : resolver.usesWriterSlot(owner, any.valueTypeInfo()); + if (slot) { + addSlot(keyParts, occurrence); + } } - private static void addReadFields( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonFieldInfo[] fields, - ArrayList keyParts, - ArrayList> referencedClasses, - JsonTypeResolver.CapabilityKind kind) { - for (JsonFieldInfo field : fields) { - addReadField(resolver, owner, field, keyParts, referencedClasses, kind); + void addField(JsonFieldInfo field, boolean storesCapability) { + boolean reader = JsonTypeResolver.readerKind(kind); + JsonTypeInfo typeInfo = reader ? field.readTypeInfo() : field.writeTypeInfo(); + UnboxedValueCodec unboxed = + reader ? field.readUnboxedValueCodec() : field.writeUnboxedValueCodec(); + boolean typeDiffers = + reader ? field.readTypeDiffersFromDeclaration() : field.writeTypeDiffersFromDeclaration(); + Class codecClass = + unboxed != null + ? unboxed.getClass() + : storesCapability && !typeDiffers + ? keyCodecClass(resolver, owner, typeInfo, kind) + : null; + if (codecClass != null) { + Class logicalClass = + CodecUtils.rawType(reader ? field.readType() : field.writeType(), Object.class); + addCodec(keyParts, occurrence, logicalClass, codecClass); + } + addDirectTerminal(keyParts, occurrence, unboxed, typeInfo); + if (storesCapability && usesSlot(resolver, owner, typeInfo, kind)) { + addSlot(keyParts, occurrence); } + occurrence++; } - private static void addReadField( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonFieldInfo field, - ArrayList keyParts, - ArrayList> referencedClasses, - JsonTypeResolver.CapabilityKind kind) { - JsonTypeInfo child = field.readTypeInfo(); - keyParts.add("read"); - keyParts.add(field.name()); - keyParts.add(field.readRawType()); - keyParts.add(child.rawType()); - keyParts.add(field.readKind()); - keyParts.add(field.readIndex()); - keyParts.add(field.hasOccurrenceNullability()); - keyParts.add(field.occurrenceNullable()); - keyParts.add(field.occurrenceWrapsNull()); - keyParts.add(field.readsUnboxedValue()); - keyParts.add(owner != null && resolver.usesReaderSlot(owner, child)); - addMember(field.readField(), keyParts, referencedClasses); - addMember(field.readSetter(), keyParts, referencedClasses); - addCapabilityKeyParts(resolver, child, kind, keyParts, referencedClasses); - addUnboxedKeyParts(field.readUnboxedValueCodec(), true, keyParts, referencedClasses); - addClass(field.readRawType(), referencedClasses); - addClass(child.rawType(), referencedClasses); + void addCreatorField(JsonCreatorFieldInfo field, boolean storesCapability) { + UnboxedValueCodec unboxed = field.unboxedValueCodec(); + if (unboxed != null) { + addCodec(keyParts, occurrence, field.typeRef().getRawType(), unboxed.getClass()); + } + addDirectTerminal(keyParts, occurrence, unboxed, field.typeInfo()); + if (storesCapability && usesSlot(resolver, owner, field.typeInfo(), kind)) { + addSlot(keyParts, occurrence); + } + occurrence++; } - private static void addCreatorFields( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonCreatorFieldInfo[] fields, - ArrayList keyParts, - ArrayList> referencedClasses, - JsonTypeResolver.CapabilityKind kind) { - for (JsonCreatorFieldInfo field : fields) { - addCreatorField(resolver, owner, field, keyParts, referencedClasses, kind); + private static void addDirectTerminal( + ArrayList keyParts, int occurrence, UnboxedValueCodec outer, JsonTypeInfo typeInfo) { + if (!(outer instanceof TransparentUnboxedValueCodec)) { + return; + } + UnboxedValueCodec terminal = typeInfo.unboxedValueCodec(); + if (terminal instanceof DirectUnboxedValueCodec) { + addCodec(keyParts, occurrence, typeInfo.rawType(), terminal.getClass()); } } - private static void addCreatorField( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonCreatorFieldInfo field, - ArrayList keyParts, - ArrayList> referencedClasses, - JsonTypeResolver.CapabilityKind kind) { - JsonTypeInfo child = field.typeInfo(); - keyParts.add("argument"); - keyParts.add(field.name()); - keyParts.add(field.argumentIndex()); - keyParts.add(field.rawType()); - keyParts.add(child.rawType()); - keyParts.add(child.kind()); - keyParts.add(child.nullable()); - keyParts.add(child.rejectsNull()); - keyParts.add(field.materializesNullCarrier()); - keyParts.add(owner != null && resolver.usesReaderSlot(owner, child)); - addCapabilityKeyParts(resolver, child, kind, keyParts, referencedClasses); - addUnboxedKeyParts(field.unboxedValueCodec(), true, keyParts, referencedClasses); - addClass(field.rawType(), referencedClasses); - addClass(child.rawType(), referencedClasses); + private static void addCodec( + ArrayList keyParts, int occurrence, Class logicalClass, Class codecClass) { + keyParts.add(occurrence); + keyParts.add(logicalClass); + keyParts.add(codecClass); } - private static void addAnyKeyParts( + private static void addSlot(ArrayList keyParts, int occurrence) { + keyParts.add(occurrence); + } + + private static boolean usesSlot( JsonTypeResolver resolver, ObjectCodec owner, - ArrayList keyParts, - ArrayList> referencedClasses, + JsonTypeInfo typeInfo, JsonTypeResolver.CapabilityKind kind) { - AnyInfo any = owner.anyInfo(); - boolean active = - any != null - && (JsonTypeResolver.readerKind(kind) - ? any.readField() != null || any.readSetter() != null - : any.writeField() != null || any.writeGetter() != null); - if (!active) { - keyParts.add(null); - return; - } - keyParts.add("any"); - keyParts.add(any.valueRawType()); - keyParts.add(any.writeIndex()); - keyParts.add(any.constructionIndex()); - boolean storesCodec = resolver.storesAnyCodec(owner, any); - keyParts.add(storesCodec); - keyParts.add( - storesCodec - && (JsonTypeResolver.readerKind(kind) - ? resolver.usesReaderSlot(owner, any.valueTypeInfo()) - : resolver.usesWriterSlot(owner, any.valueTypeInfo()))); - if (JsonTypeResolver.readerKind(kind)) { - addMember(any.readField(), keyParts, referencedClasses); - addMember(any.readSetter(), keyParts, referencedClasses); - } else { - addMember(any.writeField(), keyParts, referencedClasses); - addMember(any.writeGetter(), keyParts, referencedClasses); - } - addCapabilityKeyParts(resolver, any.valueTypeInfo(), kind, keyParts, referencedClasses); - addClass(any.valueRawType(), referencedClasses); + return JsonTypeResolver.readerKind(kind) + ? resolver.usesReaderSlot(owner, typeInfo) + : resolver.usesWriterSlot(owner, typeInfo); } - private static void addCapabilityKeyParts( + private static Class keyCodecClass( JsonTypeResolver resolver, + ObjectCodec owner, JsonTypeInfo typeInfo, - JsonTypeResolver.CapabilityKind kind, - ArrayList keyParts, - ArrayList> referencedClasses) { - Object capability = JsonTypeResolver.currentCapability(typeInfo, kind); - Class capabilityClass = - resolver.sharedRegistry().canonicalProtectedBuiltin(typeInfo, capability) - ? null - : logicalCapabilityClass(resolver, typeInfo, capability); - keyParts.add(capabilityClass); - keyParts.add(typeInfo.kind()); - keyParts.add(typeInfo.nullable()); - keyParts.add(typeInfo.rejectsNull()); - keyParts.add(typeInfo.transparentNull()); - addClass(capabilityClass, referencedClasses); - } - - private static Class logicalCapabilityClass( - JsonTypeResolver resolver, JsonTypeInfo typeInfo, Object capability) { + JsonTypeResolver.CapabilityKind kind) { if (resolver.canonicalObjectOwner(typeInfo) != null) { - return ObjectCodec.class; - } - CollectionCodec collection = resolver.collectionCodecOwner(typeInfo); - if (collection != null) { - return collection.getClass(); - } - return capability instanceof ClosedSubtypeCodec - ? ClosedSubtypeCodec.class - : capability.getClass(); - } - - private static void addUnboxedKeyParts( - UnboxedValueCodec codec, - boolean reader, - ArrayList keyParts, - ArrayList> referencedClasses) { - if (codec == null) { - keyParts.add(null); - return; - } - keyParts.add(codec.getClass()); - addClass(codec.getClass(), referencedClasses); - if (codec instanceof DirectUnboxedValueCodec) { - DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) codec; - addMember( - reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), - keyParts, - referencedClasses); - return; - } - TransparentUnboxedValueCodec transparent = (TransparentUnboxedValueCodec) codec; - JsonTypeInfo terminal = transparent.valueTypeInfo(); - keyParts.add(terminal.rawType()); - keyParts.add(terminal.kind()); - addClass(terminal.rawType(), referencedClasses); - UnboxedValueCodec terminalCodec = terminal.unboxedValueCodec(); - if (terminalCodec instanceof DirectUnboxedValueCodec) { - DirectUnboxedValueCodec direct = (DirectUnboxedValueCodec) terminalCodec; - addMember( - reader ? direct.readCarrierMethod() : direct.writeCarrierMethod(), - keyParts, - referencedClasses); - } else { - keyParts.add(null); - } - Method[] methods = reader ? transparent.constructMethods() : transparent.extractMethods(); - keyParts.add(methods.length); - for (Method method : methods) { - addMember(method, keyParts, referencedClasses); - } - if (reader) { - int[] boxes = transparent.constructBoxBytes(); - keyParts.add(boxes.length); - for (int box : boxes) { - keyParts.add(box); - } + return null; } - } - - private static Member accessorMember(JsonFieldAccessor accessor) { - if (accessor == null) { + if ((kind == JsonTypeResolver.CapabilityKind.UTF8_WRITER + && resolver.exactUtf8WriterCollection(typeInfo) != null) + || (kind == JsonTypeResolver.CapabilityKind.UTF8_READER + && resolver.exactUtf8Collection(typeInfo) != null)) { return null; } - return accessor.getter() != null ? accessor.getter() : accessor.field(); - } - - private static void addMember( - Member member, ArrayList keyParts, ArrayList> referencedClasses) { - MemberDescriptor descriptor = MemberDescriptor.of(member); - keyParts.add(descriptor); - if (member != null) { - addClass(member.getDeclaringClass(), referencedClasses); + JsonSharedRegistry registry = resolver.sharedRegistry(); + // Hosted classes always store ordinary registered codecs through the role interface. Native + // runtime must reconstruct the same key from stable metadata without replaying hosted loader + // or module visibility. + if (registry.hostedCodegen() || registry.nativeGeneratedClasses()) { + return null; } - } - - private static void addClass(Class type, ArrayList> referencedClasses) { - if (type != null) { - referencedClasses.add(type); + Class codecClass = typeInfo.registeredCodecClass(); + if (codecClass == null) { + return null; } + return JsonCodegen.isCodecClassSourceAccessible(codecClass) ? codecClass : null; } private static Role role(JsonTypeResolver.CapabilityKind kind) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java index a5b6b16a0e..8988621bff 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java @@ -19,6 +19,7 @@ package org.apache.fory.json.resolver; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -34,34 +35,32 @@ public final class JsonGeneratedClassRegistry { private static Map> pendingClasses = new HashMap<>(); private static Map> pendingCompanions = new HashMap<>(); - private static GeneratedEntry[] generatedEntries = new GeneratedEntry[0]; + private static Map> generatedClasses = Collections.emptyMap(); private static CompanionEntry[] companionEntries = new CompanionEntry[0]; - private static boolean frozen; private JsonGeneratedClassRegistry() {} /** Publishes one hosted configuration's generated classes during Native Image analysis. */ public static synchronized Set> register(JsonSharedRegistry hostedRegistry) { - if (frozen) { + if (pendingClasses == null) { throw new IllegalStateException("Fory JSON generated class registry is frozen"); } GeneratedClasses generated = hostedRegistry.generatedClasses(); LinkedHashSet> added = new LinkedHashSet<>(); mergeClasses(generated.classes(), added); - mergeCompanions(generated.sourceCodecs(), added); - snapshot(); + mergeSourceCodecs(generated.sourceCodecs(), pendingCompanions, added); return added; } /** Finalizes Native runtime lookup and releases hosted mutable state. */ public static synchronized void freeze() { - if (frozen) { + if (pendingClasses == null) { return; } - snapshot(); + generatedClasses = pendingClasses; + snapshotCompanions(); pendingClasses = null; pendingCompanions = null; - frozen = true; } static Class generatedClass(GeneratedCodecKey key) { @@ -69,12 +68,7 @@ static Class generatedClass(GeneratedCodecKey key) { if (pending != null) { return pending.get(key); } - for (GeneratedEntry entry : generatedEntries) { - if (entry.key.equals(key)) { - return entry.generatedClass; - } - } - return null; + return generatedClasses.get(key); } static GeneratedJsonCodec sourceCodec(CompanionKey key) { @@ -104,11 +98,6 @@ private static void mergeClasses(Map> source, Set> source, Set> added) { - mergeSourceCodecs(source, pendingCompanions, added); - } - static void mergeSourceCodecs( Map> source, Map> target, @@ -125,14 +114,9 @@ static void mergeSourceCodecs( } } - private static void snapshot() { - generatedEntries = new GeneratedEntry[pendingClasses.size()]; - int index = 0; - for (Map.Entry> entry : pendingClasses.entrySet()) { - generatedEntries[index++] = new GeneratedEntry(entry.getKey(), entry.getValue()); - } + private static void snapshotCompanions() { companionEntries = new CompanionEntry[pendingCompanions.size()]; - index = 0; + int index = 0; for (Map.Entry> entry : pendingCompanions.entrySet()) { companionEntries[index++] = new CompanionEntry(entry.getKey(), entry.getValue()); } @@ -141,12 +125,10 @@ private static void snapshot() { static final class CompanionKey { private final TypeRef type; private final Class mixinType; - private final int hash; CompanionKey(TypeRef type, Class mixinType) { this.type = type; this.mixinType = mixinType; - hash = type.hashCode() * 31 + System.identityHashCode(mixinType); } @Override @@ -163,17 +145,7 @@ public boolean equals(Object other) { @Override public int hashCode() { - return hash; - } - } - - private static final class GeneratedEntry { - private final GeneratedCodecKey key; - private final Class generatedClass; - - private GeneratedEntry(GeneratedCodecKey key, Class generatedClass) { - this.key = key; - this.generatedClass = generatedClass; + return type.hashCode() * 31 + 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 57130ea2d4..e46d149c30 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 @@ -80,7 +80,6 @@ import java.util.Set; import java.util.TimeZone; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -91,6 +90,7 @@ import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.apache.fory.annotation.Internal; import org.apache.fory.codegen.CodeGenerator; @@ -289,20 +289,16 @@ GeneratedClasses generatedClasses() { } Map> classes = completedClasses(generatedClassFutures); Map> sourceCodecs = - immutableSnapshot(generatedCodecCapabilities); + generatedCodecCapabilities.isEmpty() + ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap<>(generatedCodecCapabilities)); return new GeneratedClasses(classes, sourceCodecs); } - private static Map immutableSnapshot(Map values) { - return values.isEmpty() - ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(values)); - } - - private static Map> completedClasses( - Map>> futures) { - Map> classes = new HashMap<>(futures.size()); - for (Map.Entry>> entry : futures.entrySet()) { + 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( @@ -338,65 +334,36 @@ Map> sourceCodecs() { } CompletableFuture> stringWriterClass( - JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.STRING_WRITER); - return generatedClassFuture( - generatedClassFutures, key, () -> codegen.compileStringWriter(key, owner, resolver)); + GeneratedCodecKey key, ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture(key, () -> codegen.compileStringWriter(key, owner, resolver)); } CompletableFuture> utf8WriterClass( - JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_WRITER); - return generatedClassFuture( - generatedClassFutures, key, () -> codegen.compileUtf8Writer(key, owner, resolver)); + GeneratedCodecKey key, ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture(key, () -> codegen.compileUtf8Writer(key, owner, resolver)); } CompletableFuture> latin1ReaderClass( - JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.LATIN1_READER); - return generatedClassFuture( - generatedClassFutures, key, () -> codegen.compileLatin1Reader(key, owner, resolver)); + GeneratedCodecKey key, ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture(key, () -> codegen.compileLatin1Reader(key, owner, resolver)); } CompletableFuture> utf16ReaderClass( - JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF16_READER); - return generatedClassFuture( - generatedClassFutures, key, () -> codegen.compileUtf16Reader(key, owner, resolver)); + GeneratedCodecKey key, ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture(key, () -> codegen.compileUtf16Reader(key, owner, resolver)); } CompletableFuture> utf8ReaderClass( - JsonTypeInfo typeInfo, ObjectCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedObjectKey(typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_READER); - return generatedClassFuture( - generatedClassFutures, key, () -> codegen.compileUtf8Reader(key, owner, resolver)); - } - - CompletableFuture> utf8CollectionWriterClass( - JsonTypeInfo typeInfo, CollectionCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedCollectionKey( - typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_WRITER); - return generatedClassFuture( - generatedClassFutures, - key, - () -> codegen.compileUtf8CollectionWriter(key, typeInfo.typeRef(), owner)); - } - - CompletableFuture> utf8CollectionReaderClass( - JsonTypeInfo typeInfo, CollectionCodec owner, JsonTypeResolver resolver) { - GeneratedCodecKey key = - resolver.generatedCollectionKey( - typeInfo, owner, JsonTypeResolver.CapabilityKind.UTF8_READER); - return generatedClassFuture( - generatedClassFutures, - key, - () -> codegen.compileUtf8CollectionReader(key, typeInfo.typeRef(), owner)); + GeneratedCodecKey key, ObjectCodec owner, JsonTypeResolver resolver) { + return generatedClassFuture(key, () -> codegen.compileUtf8Reader(key, owner, resolver)); + } + + CompletableFuture> utf8CollectionWriterClass(GeneratedCodecKey key) { + return generatedClassFuture(key, () -> codegen.compileUtf8CollectionWriter(key)); + } + + CompletableFuture> utf8CollectionReaderClass(GeneratedCodecKey key) { + return generatedClassFuture(key, () -> codegen.compileUtf8CollectionReader(key)); } boolean generatedCapabilitiesEnabled() { @@ -408,32 +375,30 @@ boolean hostedCodegen() { } boolean nativeGeneratedClasses() { - return nativeCodegenEnabled && codegen == null; + return nativeCodegenEnabled; } Class nativeGeneratedClass(GeneratedCodecKey key) { return nativeCodegenEnabled ? JsonGeneratedClassRegistry.generatedClass(key) : null; } - private CompletableFuture> generatedClassFuture( - ConcurrentHashMap>> classes, - K key, - Callable> compiler) { - CompletableFuture> existing = classes.get(key); + private CompletableFuture> generatedClassFuture( + GeneratedCodecKey key, Supplier> compiler) { + CompletableFuture> existing = generatedClassFutures.get(key); if (existing != null) { return existing; } CompletableFuture> candidate = new CompletableFuture<>(); - existing = classes.putIfAbsent(key, candidate); + existing = generatedClassFutures.putIfAbsent(key, candidate); if (existing != null) { return existing; } Runnable task = () -> { try { - candidate.complete(compiler.call()); + candidate.complete(compiler.get()); } catch (Throwable failure) { - classes.remove(key, candidate); + generatedClassFutures.remove(key, candidate); candidate.completeExceptionally(failure); } }; @@ -448,7 +413,7 @@ private CompletableFuture> generatedClassFuture( try { service.execute(task); } catch (RuntimeException | Error failure) { - classes.remove(key, candidate); + generatedClassFutures.remove(key, candidate); candidate.completeExceptionally(failure); throw failure; } @@ -924,10 +889,11 @@ private static String generatedMixinCodecBinaryName(Class mixinType, Class public JsonValueCodec createCodec( Class rawType, TypeRef typeRef, JsonTypeResolver localResolver) { - return createCodec(rawType, typeRef, localResolver, null, false); + ResolvedCodec resolved = resolveCodec(rawType, typeRef, localResolver, null, false); + return resolved == null ? null : resolved.codec; } - JsonValueCodec createCodec( + ResolvedCodec resolveCodec( Class rawType, TypeRef typeRef, JsonTypeResolver localResolver, @@ -935,11 +901,13 @@ JsonValueCodec createCodec( boolean runtimeType) { JsonValueCodec customCodec = customCodec(rawType); if (customCodec != null) { - return customCodec; + return new ResolvedCodec(customCodec, null); } FactoryBinding exactFactory = customCodecs.getFactory(rawType); if (exactFactory != null) { - return createExactCodec(rawType, typeRef, exactFactory, localResolver, runtimeType); + return new ResolvedCodec( + createExactCodec(rawType, typeRef, exactFactory, localResolver, runtimeType), + exactFactory.key()); } if (childFactory != null) { // A parent-derived subtype is only the default model. Exact application registration above @@ -950,7 +918,7 @@ JsonValueCodec createCodec( "Closed JSON subtype factory did not create the exact ObjectCodec for " + rawType.getName()); } - return childCodec; + return new ResolvedCodec(childCodec, childFactory.factoryKey()); } if (typeRef.getTypeExtMeta() != null && (rawType == OptionalInt.class @@ -960,27 +928,27 @@ JsonValueCodec createCodec( throw new ForyJsonException("Nullable Optional has ambiguous JSON null: " + typeRef); } if (rawType == OptionalInt.class) { - return ScalarCodecs.OptionalIntCodec.NON_NULL; + return new ResolvedCodec(ScalarCodecs.OptionalIntCodec.NON_NULL, null); } if (rawType == OptionalLong.class) { - return ScalarCodecs.OptionalLongCodec.NON_NULL; + return new ResolvedCodec(ScalarCodecs.OptionalLongCodec.NON_NULL, null); } - return ScalarCodecs.OptionalDoubleCodec.NON_NULL; + return new ResolvedCodec(ScalarCodecs.OptionalDoubleCodec.NON_NULL, null); } boolean semanticToken = typeRef.getTypeExtMeta() != null && typeRef.getTypeExtMeta().typeId() != Types.UNKNOWN; if (semanticToken) { // The exact JVM carrier codec cannot erase an explicit semantic type. The installed module // owns that representation even when the semantic value uses a primitive carrier. - JsonValueCodec codec = createModuleCodec(typeRef, localResolver, runtimeType); - if (codec == null) { + ResolvedCodec resolved = createModuleCodec(typeRef, localResolver, runtimeType); + if (resolved == null) { throw new ForyJsonException("No installed JSON module owns semantic type " + typeRef); } - return codec; + return resolved; } JsonValueCodec codec = exactCodecs.get(rawType); if (codec != null) { - return codec; + return new ResolvedCodec(codec, null); } if (rawType == Class.class) { // JSON strings must not be treated as class-loading authority by the default codecs. @@ -994,58 +962,60 @@ JsonValueCodec createCodec( throw new ForyJsonException("Unsupported JSON type " + rawType); } if (rawType.isEnum()) { - return new ScalarCodecs.EnumCodec(rawType); + return new ResolvedCodec(new ScalarCodecs.EnumCodec(rawType), null); } if (rawType.isArray()) { - return ArrayCodec.create(rawType, typeRef, localResolver); + return new ResolvedCodec(ArrayCodec.create(rawType, typeRef, localResolver), null); } if (rawType == Optional.class) { - return new ScalarCodecs.OptionalCodec(typeRef, localResolver); + return new ResolvedCodec(new ScalarCodecs.OptionalCodec(typeRef, localResolver), null); } if (rawType == AtomicReference.class) { JsonTypeInfo contentInfo = localResolver.getTypeInfo(CodecUtils.elementTypeRef(typeRef)); - return ScalarCodecs.AtomicReferenceCodec.create(typeRef, contentInfo); + return new ResolvedCodec( + ScalarCodecs.AtomicReferenceCodec.create(typeRef, contentInfo), null); } if (rawType == AtomicReferenceArray.class) { JsonTypeInfo elementInfo = localResolver.getTypeInfo(CodecUtils.elementTypeRef(typeRef)); - return ScalarCodecs.AtomicReferenceArrayCodec.create(elementInfo); + return new ResolvedCodec(ScalarCodecs.AtomicReferenceArrayCodec.create(elementInfo), null); } if (Calendar.class.isAssignableFrom(rawType)) { - return ScalarCodecs.CalendarCodec.INSTANCE; + return new ResolvedCodec(ScalarCodecs.CalendarCodec.INSTANCE, null); } if (Date.class.isAssignableFrom(rawType)) { - return ScalarCodecs.DateCodec.INSTANCE; + return new ResolvedCodec(ScalarCodecs.DateCodec.INSTANCE, null); } if (ZoneId.class.isAssignableFrom(rawType)) { - return ScalarCodecs.ZoneIdCodec.INSTANCE; + return new ResolvedCodec(ScalarCodecs.ZoneIdCodec.INSTANCE, null); } if (ByteBuffer.class.isAssignableFrom(rawType)) { - return ScalarCodecs.ByteBufferCodec.INSTANCE; + return new ResolvedCodec(ScalarCodecs.ByteBufferCodec.INSTANCE, null); } if (File.class.isAssignableFrom(rawType)) { - return ScalarCodecs.FileCodec.INSTANCE; + return new ResolvedCodec(ScalarCodecs.FileCodec.INSTANCE, null); } if (Path.class.isAssignableFrom(rawType)) { - return ScalarCodecs.PathCodec.INSTANCE; + return new ResolvedCodec(ScalarCodecs.PathCodec.INSTANCE, null); } - codec = createModuleCodec(typeRef, localResolver, runtimeType); - if (codec != null) { - return codec; + ResolvedCodec resolved = createModuleCodec(typeRef, localResolver, runtimeType); + if (resolved != null) { + return resolved; } if (Number.class.isAssignableFrom(rawType) || CharSequence.class.isAssignableFrom(rawType)) { throw new ForyJsonException("Unsupported JSON type " + rawType); } if (Collection.class.isAssignableFrom(rawType)) { - return CollectionCodec.create(rawType, typeRef, localResolver); + return new ResolvedCodec(CollectionCodec.create(rawType, typeRef, localResolver), null); } if (Map.class.isAssignableFrom(rawType)) { - return MapCodec.create(rawType, typeRef, localResolver); + return new ResolvedCodec(MapCodec.create(rawType, typeRef, localResolver), null); } return null; } JsonValueCodec createRuntimeCodec(Class rawType, JsonTypeResolver localResolver) { - return createCodec(rawType, TypeRef.of(rawType), localResolver, null, true); + ResolvedCodec resolved = resolveCodec(rawType, TypeRef.of(rawType), localResolver, null, true); + return resolved == null ? null : resolved.codec; } private JsonValueCodec createExactCodec( @@ -1076,7 +1046,7 @@ Class runtimeCodecTarget(Class runtimeType) { return binding.target; } - private JsonValueCodec createModuleCodec( + private ResolvedCodec createModuleCodec( TypeRef typeRef, JsonTypeResolver localResolver, boolean runtimeType) { JsonValueCodec selected = null; UnsupportedJsonTypeException unsupported = null; @@ -1108,7 +1078,7 @@ private JsonValueCodec createModuleCodec( } if (claims.size() == 1) { if (selected != null) { - return selected; + return new ResolvedCodec(selected, claims.get(0)); } throw unsupported; } @@ -1117,6 +1087,24 @@ private JsonValueCodec createModuleCodec( "Conflicting JSON codec factories for " + typeRef.getType() + ": " + claims); } + static final class ResolvedCodec { + private final JsonValueCodec codec; + private final String factoryKey; + + private ResolvedCodec(JsonValueCodec codec, String factoryKey) { + this.codec = codec; + this.factoryKey = factoryKey; + } + + JsonValueCodec codec() { + return codec; + } + + String factoryKey() { + return factoryKey; + } + } + private static final class ExactFactoryBinding { private final Class target; private final FactoryBinding binding; @@ -1216,13 +1204,6 @@ Class mixinType(Class targetType) { return overlay == null ? null : overlay.mixinType(); } - boolean canonicalProtectedBuiltin(JsonTypeInfo typeInfo, Object capability) { - Class rawType = typeInfo.rawType(); - return CodecRegistry.isProtectedBuiltinType(rawType) - && (exactCodecs.get(rawType) == capability - || ArrayCodec.isCanonicalProtectedCodec(rawType, capability)); - } - /** Adds exact pair context to a cold effective-schema validation failure. */ @Internal public ForyJsonException mixinSchemaFailure(Class targetType, ForyJsonException failure) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java index 12f9b28981..1792c9bd30 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java @@ -58,6 +58,9 @@ public final class JsonTypeInfo { private final boolean rejectsNull; private final boolean transparentNull; private final UnboxedValueCodec unboxedValueCodec; + private final String objectFactoryKey; + // Only exact registry or factory selection can vary independently of target/Mixin metadata. + private final Class registeredCodecClass; private StringWriterCodec stringWriter; private Utf8WriterCodec utf8Writer; private Latin1ReaderCodec latin1Reader; @@ -66,7 +69,7 @@ public final class JsonTypeInfo { private final boolean annotationCodec; JsonTypeInfo(TypeRef typeRef, JsonFieldKind kind, JsonValueCodec codec) { - this(typeRef, kind, codec, false); + this(typeRef, kind, codec, false, null, null); } JsonTypeInfo( @@ -74,6 +77,16 @@ public final class JsonTypeInfo { JsonFieldKind kind, JsonValueCodec codec, boolean annotationCodec) { + this(typeRef, kind, codec, annotationCodec, null, null); + } + + JsonTypeInfo( + TypeRef typeRef, + JsonFieldKind kind, + JsonValueCodec codec, + boolean annotationCodec, + String objectFactoryKey, + Class registeredCodecClass) { this.typeRef = typeRef; this.rawType = typeRef.getRawType(); this.kind = kind; @@ -84,6 +97,8 @@ public final class JsonTypeInfo { metadata != null && !metadata.nullable() && !metadata.nullableWrapper() && !transparentNull; unboxedValueCodec = codec instanceof UnboxedValueCodec ? (UnboxedValueCodec) codec : null; this.annotationCodec = annotationCodec; + this.objectFactoryKey = objectFactoryKey; + this.registeredCodecClass = registeredCodecClass; stringWriter = codec; utf8Writer = codec; latin1Reader = codec; @@ -183,4 +198,14 @@ void setUtf8Reader(Utf8ReaderCodec utf8Reader) { public boolean usesAnnotationCodec() { return annotationCodec; } + + String objectFactoryKey() { + return objectFactoryKey; + } + + /** Returns the exact application-registered codec implementation, or {@code null}. */ + @Internal + public Class registeredCodecClass() { + return registeredCodecClass; + } } 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 e3339e446b..eb61f801d3 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 @@ -52,6 +52,7 @@ import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.CompositeJsonCodec; +import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonObjectModel; import org.apache.fory.json.codec.JsonSubTypesInfo; @@ -64,6 +65,7 @@ import org.apache.fory.json.codec.ObjectCodec.AnyInfo; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.StringWriterCodec; +import org.apache.fory.json.codec.TransparentUnboxedValueCodec; import org.apache.fory.json.codec.UnboxedValueCodec; import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; @@ -191,17 +193,18 @@ public ObjectCodec getUnwrappedObjectCodec(Class rawType) { if (customTypeInfo(rawType, rawType) != null || sharedRegistry.subTypesInfo(rawType) != null) { return null; } - JsonValueCodec selected = sharedRegistry.createCodec(rawType, ownerType, this); + JsonSharedRegistry.ResolvedCodec selected = + sharedRegistry.resolveCodec(rawType, ownerType, this, null, false); if (selected != null) { - if (!(selected instanceof ObjectCodec)) { + if (!(selected.codec() instanceof ObjectCodec)) { return null; } // A language object model still produces the standard ObjectCodec; only its constructor and // accessors differ. Publish that shell without resolving it so JsonUnwrappedInfo remains the // sole owner of iterative flattened-graph resolution and cycle detection. - ObjectCodec codec = (ObjectCodec) selected; + ObjectCodec codec = (ObjectCodec) selected.codec(); objectCodecs.put(key, codec); - typeInfo = newTypeInfo(rawType, rawType, codec); + typeInfo = newRegisteredTypeInfo(ownerType, codec, selected.factoryKey()); publishTypeInfo(key, typeInfo); registerTypeInfoOwner(typeInfo, codec); return codec; @@ -247,6 +250,11 @@ ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { return null; } + String objectFactoryKey(ObjectCodec owner) { + JsonTypeInfo typeInfo = canonicalObjectTypeInfos.get(owner); + return typeInfo == null ? null : typeInfo.objectFactoryKey(); + } + /** Returns an exact declared ArrayList-backed UTF-8 collection owner, or {@code null}. */ @Internal public CollectionCodec exactUtf8Collection(JsonTypeInfo typeInfo) { @@ -271,10 +279,6 @@ private CollectionCodec exactUtf8CollectionOwner(JsonTypeInfo typeInfo) { return owner; } - CollectionCodec collectionCodecOwner(JsonTypeInfo typeInfo) { - return collectionCodecs.get(typeInfo); - } - /** Returns an exact declared UTF-8 collection writer owner, or {@code null}. */ @Internal public CollectionCodec exactUtf8WriterCollection(JsonTypeInfo typeInfo) { @@ -671,7 +675,7 @@ private JsonTypeInfo customTypeInfo(TypeRef declaredType, Class rawType) { JsonValueCodec codec = sharedRegistry.customCodec(rawType); if (codec != null) { sharedRegistry.checkCustomSecure(rawType); - return newTypeInfo(declaredType, JsonFieldKind.OBJECT, codec, false); + return newRegisteredTypeInfo(declaredType, codec, null); } JsonCodecDeclaration declaration = sharedRegistry.codecDeclaration(rawType); if (declaration != null) { @@ -1287,7 +1291,7 @@ private StringWriterCodec newStringWriter( (StringWriterCodec[]) new StringWriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (usesWriteCodec(field)) { + if (storesWriteCapability(owner, field, false)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.STRING_WRITER); } @@ -1319,7 +1323,7 @@ private Utf8WriterCodec newUtf8Writer( (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (usesUtf8WriteCodec(field)) { + if (storesWriteCapability(owner, field, true)) { JsonTypeInfo typeInfo = field.writeTypeInfo(); codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_WRITER); } @@ -1348,7 +1352,7 @@ private StringWriterCodec newUnwrappedStringWriter( (StringWriterCodec[]) new StringWriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (usesWriteCodec(field)) { + if (storesWriteCapability(owner, field, false)) { JsonTypeInfo child = field.writeTypeInfo(); codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.STRING_WRITER); } @@ -1377,7 +1381,7 @@ private Utf8WriterCodec newUnwrappedUtf8Writer( (Utf8WriterCodec[]) new Utf8WriterCodec[fields.length]; for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - if (usesUtf8WriteCodec(field)) { + if (storesWriteCapability(owner, field, true)) { JsonTypeInfo child = field.writeTypeInfo(); codecs[i] = resolvedCapability(child, capabilities, CapabilityKind.UTF8_WRITER); } @@ -1448,9 +1452,7 @@ private Latin1ReaderCodec newLatin1Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (usesReadCodec(field)) { - codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); - } else if (readNestedType(field) != null && field.readRawType() != owner.type()) { + if (storesReadCapability(owner, field)) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.LATIN1_READER); } } @@ -1517,9 +1519,7 @@ private Utf16ReaderCodec newUtf16Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (usesReadCodec(field)) { - codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); - } else if (readNestedType(field) != null && field.readRawType() != owner.type()) { + if (storesReadCapability(owner, field)) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF16_READER); } } @@ -1586,9 +1586,7 @@ private Utf8ReaderCodec newUtf8Reader( for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; JsonTypeInfo typeInfo = field.readTypeInfo(); - if (usesReadCodec(field)) { - codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); - } else if (readNestedType(field) != null && field.readRawType() != owner.type()) { + if (storesReadCapability(owner, field)) { codecs[i] = resolvedCapability(typeInfo, capabilities, CapabilityKind.UTF8_READER); } } @@ -1745,20 +1743,11 @@ enum CapabilityKind { UTF8_READER } - GeneratedCodecKey generatedObjectKey( - JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { - return GeneratedCodecKeyBuilder.object(this, typeInfo, owner, kind); - } - - GeneratedCodecKey generatedCollectionKey( - JsonTypeInfo typeInfo, CollectionCodec owner, CapabilityKind kind) { - return GeneratedCodecKeyBuilder.collection(typeInfo, owner, kind); - } - /** Returns the nested object type inlined by generated readers, or {@code null}. */ @Internal public Class readNestedType(JsonFieldInfo field) { - if (!field.readsUnboxedValue() + if (field.readAccessorType() == field.readRawType() + && !field.readsUnboxedValue() && field.readKind() == JsonFieldKind.OBJECT && field.readRawType() != Object.class && canonicalObjectCodec(field.readTypeInfo()) != null) { @@ -1773,6 +1762,11 @@ public boolean usesWriteCodec(JsonFieldInfo field) { if (field.writesUnboxedValue() && field.writeKind() == JsonFieldKind.ENUM) { return true; } + // A resolved logical type belongs to the codec instance, not the generated class. Keep the + // class on the erased accessor type and inject the selected capability through its base API. + if (field.writeTypeDiffersFromDeclaration() && !field.writesRawString()) { + return true; + } switch (field.writeKind()) { case ARRAY: case MAP: @@ -1785,7 +1779,7 @@ public boolean usesWriteCodec(JsonFieldInfo field) { } } - /** Returns whether a generated UTF-8 writer stores a field codec. */ + /** Returns whether a generated UTF-8 writer invokes a resolved field capability. */ @Internal public boolean usesUtf8WriteCodec(JsonFieldInfo field) { return usesWriteCodec(field) @@ -1793,6 +1787,16 @@ public boolean usesUtf8WriteCodec(JsonFieldInfo field) { && exactUtf8WriterCollection(field.writeTypeInfo()) != null; } + /** Returns whether a generated writer stores this field's resolved capability. */ + @Internal + public boolean storesWriteCapability( + ObjectCodec owner, JsonFieldInfo field, boolean utf8Writer) { + boolean usesCodec = utf8Writer ? usesUtf8WriteCodec(field) : usesWriteCodec(field); + return usesCodec + && (canonicalObjectCodec(field.writeTypeInfo()) == null + || field.writeTypeInfo().rawType() != owner.type()); + } + /** Returns whether a generated reader stores a field codec. */ @Internal public boolean usesReadCodec(JsonFieldInfo field) { @@ -1806,17 +1810,14 @@ public boolean usesReadCodec(JsonFieldInfo field) { return false; } if (rawType.isPrimitive()) { - return !((rawType == boolean.class && kind == JsonFieldKind.BOOLEAN) - || (rawType == byte.class && kind == JsonFieldKind.BYTE) - || (rawType == short.class && kind == JsonFieldKind.SHORT) - || (rawType == int.class && kind == JsonFieldKind.INT) - || (rawType == long.class && kind == JsonFieldKind.LONG) - || (rawType == float.class && kind == JsonFieldKind.FLOAT) - || (rawType == double.class && kind == JsonFieldKind.DOUBLE) - || (rawType == char.class && kind == JsonFieldKind.CHAR)); + return !kind.matchesPrimitive(rawType); } return true; } + // The matching writer rule keeps resolved logical types out of generated-class identity. + if (field.readTypeDiffersFromDeclaration()) { + return true; + } switch (field.readKind()) { case ENUM: case ARRAY: @@ -1831,15 +1832,47 @@ public boolean usesReadCodec(JsonFieldInfo field) { } } + /** Returns whether a generated reader stores a resolved capability for this field. */ + @Internal + public boolean storesReadCapability(ObjectCodec owner, JsonFieldInfo field) { + if (usesReadCodec(field)) { + return true; + } + Class nestedType = readNestedType(field); + return nestedType != null && nestedType != owner.type(); + } + + /** Returns whether a generated reader stores this creator argument's capability. */ + @Internal + public boolean storesReadCapability(JsonCreatorFieldInfo field) { + UnboxedValueCodec unboxed = field.unboxedValueCodec(); + if (unboxed instanceof DirectUnboxedValueCodec) { + return false; + } + if (!(unboxed instanceof TransparentUnboxedValueCodec)) { + return !field.readsDirectPrimitive(); + } + JsonTypeInfo terminal = ((TransparentUnboxedValueCodec) unboxed).valueTypeInfo(); + if (terminal.unboxedValueCodec() instanceof DirectUnboxedValueCodec) { + return false; + } + if (terminal.rawType() == String.class && terminal.kind() == JsonFieldKind.STRING) { + return false; + } + return !terminal.kind().matchesPrimitive(terminal.rawType()); + } + /** Returns whether the standard string collection writer is fully inlined. */ @Internal public static boolean writesStringCollectionDirectly(JsonFieldInfo field) { - return field.writeElementRawType() == String.class + return !field.writeTypeDiffersFromDeclaration() + && field.writeElementRawType() == String.class && field.writeTypeInfo().stringWriter().getClass() == CollectionCodec.StringCollectionCodec.class; } - private ArrayList capabilityChildren(ObjectCodec owner, CapabilityKind kind) { + private ArrayList capabilityChildren( + ObjectCodec owner, CapabilityKind kind, GeneratedCodecKeyBuilder keyBuilder) { ArrayList children = new ArrayList<>(); AnyInfo any = owner.anyInfo(); boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; @@ -1848,66 +1881,86 @@ private ArrayList capabilityChildren(ObjectCodec owner, Capabil owner.unwrappedInfo() == null ? owner.writeFields() : unwrappedWriteFields(owner); for (int i = 0; i < fields.length; i++) { JsonFieldInfo field = fields[i]; - boolean usesCodec = - kind == CapabilityKind.UTF8_WRITER ? usesUtf8WriteCodec(field) : usesWriteCodec(field); - if (usesCodec - && (field.writeRawType() != owner.type() - || canonicalObjectOwner(field.writeTypeInfo()) == null)) { + boolean storesCapability = + storesWriteCapability(owner, field, kind == CapabilityKind.UTF8_WRITER); + if (keyBuilder != null) { + keyBuilder.addField(field, storesCapability); + } + if (storesCapability) { children.add(field.writeTypeInfo()); } } - if (any != null - && (any.writeField() != null || any.writeGetter() != null) - && storesAnyCodec(owner, any)) { + boolean storesAny = + any != null + && (any.writeField() != null || any.writeGetter() != null) + && storesAnyCodec(owner, any); + if (keyBuilder != null) { + keyBuilder.addAny(storesAny); + } + if (storesAny) { children.add(any.valueTypeInfo()); } return children; } - if (owner.unwrappedInfo() != null) { - JsonCreatorInfo creator = owner.creatorInfo(); - if (creator == null) { - JsonFieldInfo[] fields = owner.readFields(); - for (int i = 0; i < fields.length; i++) { - addReadDependency(children, owner, fields[i]); - } - } else { - JsonCreatorFieldInfo[] fields = creator.fields(); - for (int i = 0; i < fields.length; i++) { - children.add(fields[i].typeInfo()); + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + JsonFieldInfo[] fields = owner.readFields(); + for (int i = 0; i < fields.length; i++) { + addReadDependency(children, owner, fields[i], keyBuilder); + } + } else { + JsonCreatorFieldInfo[] fields = creator.fields(); + for (int i = 0; i < fields.length; i++) { + JsonCreatorFieldInfo field = fields[i]; + if (keyBuilder != null) { + keyBuilder.addCreatorField(field, storesReadCapability(field)); } + children.add(field.typeInfo()); } + } + if (owner.unwrappedInfo() != null) { JsonUnwrappedInfo.ReadRoute[] routes = owner.unwrappedInfo().readRoutes(); for (int i = 0; i < routes.length; i++) { JsonUnwrappedInfo.ReadRoute route = routes[i]; if (route.field() == null) { + if (keyBuilder != null) { + keyBuilder.addCreatorField( + route.creatorField(), storesReadCapability(route.creatorField())); + } children.add(route.creatorField().typeInfo()); } else { - addReadDependency(children, owner, route.field()); + addReadDependency(children, owner, route.field(), keyBuilder); } } - } else if (owner.creatorInfo() == null) { - JsonFieldInfo[] fields = owner.readFields(); - for (int i = 0; i < fields.length; i++) { - addReadDependency(children, owner, fields[i]); - } - } else { - JsonCreatorFieldInfo[] fields = owner.creatorInfo().fields(); - for (int i = 0; i < fields.length; i++) { - children.add(fields[i].typeInfo()); - } } - if (any != null - && (any.readField() != null || any.readSetter() != null) - && storesAnyCodec(owner, any)) { + boolean storesAny = + any != null + && (any.readField() != null || any.readSetter() != null) + && storesAnyCodec(owner, any); + if (keyBuilder != null) { + keyBuilder.addAny(storesAny); + } + if (storesAny) { children.add(any.valueTypeInfo()); } return children; } + private ArrayList capabilityChildren( + ObjectCodec owner, CapabilityKind kind) { + return capabilityChildren(owner, kind, null); + } + private void addReadDependency( - ArrayList children, ObjectCodec owner, JsonFieldInfo field) { - if (usesReadCodec(field) - || readNestedType(field) != null && field.readRawType() != owner.type()) { + ArrayList children, + ObjectCodec owner, + JsonFieldInfo field, + GeneratedCodecKeyBuilder keyBuilder) { + boolean storesCapability = storesReadCapability(owner, field); + if (keyBuilder != null) { + keyBuilder.addField(field, storesCapability); + } + if (storesCapability) { children.add(field.readTypeInfo()); } } @@ -2019,40 +2072,6 @@ private boolean reachesReader( return false; } - private boolean canCompile(JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { - if (owner.fixedInstance()) { - return false; - } - GeneratedCodecKey key = generatedObjectKey(typeInfo, owner, kind); - if (sharedRegistry.nativeGeneratedClass(key) != null) { - return true; - } - if (sharedRegistry.nativeGeneratedClasses()) { - return false; - } - return codegen != null - && (kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER - ? codegen.canCompileWriter(key, owner) - : codegen.canCompileReader(key, owner)); - } - - private boolean canCompileCollection(JsonTypeInfo typeInfo, CapabilityKind kind) { - CollectionCodec owner = exactUtf8CollectionOwner(typeInfo); - GeneratedCodecKey key = generatedCollectionKey(typeInfo, owner, kind); - if (sharedRegistry.nativeGeneratedClass(key) != null) { - return true; - } - if (sharedRegistry.nativeGeneratedClasses()) { - return false; - } - return codegen != null; - } - - private Class nativeObjectClass( - JsonTypeInfo typeInfo, ObjectCodec owner, CapabilityKind kind) { - return sharedRegistry.nativeGeneratedClass(generatedObjectKey(typeInfo, owner, kind)); - } - static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { switch (kind) { case STRING_WRITER: @@ -2076,40 +2095,29 @@ private CompletableFuture> generatedClass(CapabilityNode node, Capabili } if (node.collectionOwner != null) { if (kind == CapabilityKind.UTF8_WRITER) { - return sharedRegistry.utf8CollectionWriterClass(node.typeInfo, node.collectionOwner, this); + return sharedRegistry.utf8CollectionWriterClass(node.generatedKey); } if (kind == CapabilityKind.UTF8_READER) { - return sharedRegistry.utf8CollectionReaderClass(node.typeInfo, node.collectionOwner, this); + return sharedRegistry.utf8CollectionReaderClass(node.generatedKey); } throw new IllegalStateException("Unsupported generated JSON collection capability " + kind); } switch (kind) { case STRING_WRITER: - return sharedRegistry.stringWriterClass(node.typeInfo, node.objectOwner, this); + return sharedRegistry.stringWriterClass(node.generatedKey, node.objectOwner, this); case UTF8_WRITER: - return sharedRegistry.utf8WriterClass(node.typeInfo, node.objectOwner, this); + return sharedRegistry.utf8WriterClass(node.generatedKey, node.objectOwner, this); case LATIN1_READER: - return sharedRegistry.latin1ReaderClass(node.typeInfo, node.objectOwner, this); + return sharedRegistry.latin1ReaderClass(node.generatedKey, node.objectOwner, this); case UTF16_READER: - return sharedRegistry.utf16ReaderClass(node.typeInfo, node.objectOwner, this); + return sharedRegistry.utf16ReaderClass(node.generatedKey, node.objectOwner, this); case UTF8_READER: - return sharedRegistry.utf8ReaderClass(node.typeInfo, node.objectOwner, this); + return sharedRegistry.utf8ReaderClass(node.generatedKey, node.objectOwner, this); default: throw new IllegalStateException("Unknown JSON capability kind " + kind); } } - 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) { - return sharedRegistry.nativeGeneratedClass( - generatedCollectionKey(node.typeInfo, node.collectionOwner, kind)); - } - return nativeObjectClass(node.typeInfo, node.objectOwner, kind); - } - private Object newCapability( CapabilityNode node, Class generatedClass, @@ -2341,7 +2349,6 @@ private void requestGraph(CapabilityGraph graph) { return; } if (sharedRegistry.nativeGeneratedClasses()) { - graph.loadNativeClasses(); graph.publish(); return; } @@ -2438,12 +2445,24 @@ private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo, boolea if (existing != null) { return existing.complete || slotEdge; } - if (!canCompile(typeInfo, owner, kind)) { + GeneratedCodecKeyBuilder keyBuilder = + GeneratedCodecKeyBuilder.object(JsonTypeResolver.this, typeInfo, owner, kind); + ArrayList children = capabilityChildren(owner, kind, keyBuilder); + GeneratedCodecKey generatedKey = keyBuilder.build(); + Class generatedClass = sharedRegistry.nativeGeneratedClass(generatedKey); + if (sharedRegistry.nativeGeneratedClasses()) { + if (generatedClass == null) { + return false; + } + } else if (codegen == null + || (kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER + ? !codegen.canCompileWriter(generatedKey, owner) + : !codegen.canCompileReader(generatedKey, owner))) { return false; } - CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); + CapabilityNode node = new CapabilityNode(typeInfo, owner, initial, generatedKey); + node.generatedClass = generatedClass; nodes.put(typeInfo, node); - ArrayList children = capabilityChildren(owner, kind); for (int i = 0; i < children.size(); i++) { JsonTypeInfo child = children.get(i); boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; @@ -2467,10 +2486,16 @@ private boolean addCollection(CollectionCodec owner, JsonTypeInfo typeInfo) { return existing.complete; } JsonTypeInfo element = declaredCollectionElement(typeInfo); - if (element == null || !canCompileCollection(typeInfo, kind)) { + if (element == null) { + return false; + } + GeneratedCodecKey generatedKey = GeneratedCodecKeyBuilder.collection(typeInfo, owner, kind); + Class generatedClass = sharedRegistry.nativeGeneratedClass(generatedKey); + if (sharedRegistry.nativeGeneratedClasses() ? generatedClass == null : codegen == null) { return false; } - CapabilityNode node = new CapabilityNode(typeInfo, owner, initial); + CapabilityNode node = new CapabilityNode(typeInfo, owner, initial, generatedKey); + node.generatedClass = generatedClass; nodes.put(typeInfo, node); if (!addDependency(element)) { return false; @@ -2487,34 +2512,12 @@ 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); } 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 ForyJsonException( - "Missing generated Fory JSON class for exact type " + node.typeInfo.type()); - } - } - } - } - private void publish() { requireJITLock(); IdentityMap capabilities = new IdentityMap<>(); @@ -2563,25 +2566,36 @@ private final class CapabilityNode { private final ObjectCodec objectOwner; private final CollectionCodec collectionOwner; private final ClosedSubtypeCodec subtypeOwner; + private final GeneratedCodecKey generatedKey; private final Object initial; private boolean complete; private CompletableFuture> classFuture; private Class generatedClass; private Object instance; - private CapabilityNode(JsonTypeInfo typeInfo, ObjectCodec owner, Object initial) { + private CapabilityNode( + JsonTypeInfo typeInfo, + ObjectCodec owner, + Object initial, + GeneratedCodecKey generatedKey) { this.typeInfo = typeInfo; objectOwner = owner; collectionOwner = null; subtypeOwner = null; + this.generatedKey = generatedKey; this.initial = initial; } - private CapabilityNode(JsonTypeInfo typeInfo, CollectionCodec owner, Object initial) { + private CapabilityNode( + JsonTypeInfo typeInfo, + CollectionCodec owner, + Object initial, + GeneratedCodecKey generatedKey) { this.typeInfo = typeInfo; objectOwner = null; collectionOwner = owner; subtypeOwner = null; + this.generatedKey = generatedKey; this.initial = initial; } @@ -2590,6 +2604,7 @@ private CapabilityNode(JsonTypeInfo typeInfo, ClosedSubtypeCodec owner, Object i objectOwner = null; collectionOwner = null; subtypeOwner = owner; + generatedKey = null; this.initial = initial; } @@ -2667,11 +2682,12 @@ private JsonTypeInfo buildTypeInfo(Class rawType, TypeRef typeRef, Object private JsonTypeInfo buildTypeInfo( Class rawType, TypeRef typeRef, Object key, JsonCodecFactory childFactory) { sharedRegistry.checkSecure(rawType); - JsonValueCodec codec = - sharedRegistry.createCodec(rawType, typeRef, this, childFactory, false); - if (codec == null) { + JsonSharedRegistry.ResolvedCodec resolved = + sharedRegistry.resolveCodec(rawType, typeRef, this, childFactory, false); + if (resolved == null) { return buildObjectTypeInfo(typeRef, key); } + JsonValueCodec codec = resolved.codec(); JsonTypeInfo recursiveTypeInfo = typeInfos.get(key); if (recursiveTypeInfo != null) { return recursiveTypeInfo; @@ -2679,7 +2695,7 @@ private JsonTypeInfo buildTypeInfo( if (codec instanceof ObjectCodec) { boolean bindingOwner = enterObjectBinding(typeRef); try { - JsonTypeInfo typeInfo = newTypeInfo(typeRef, codec); + JsonTypeInfo typeInfo = newRegisteredTypeInfo(typeRef, codec, resolved.factoryKey()); objectCodecs.put(key, (ObjectCodec) codec); publishTypeInfo(key, typeInfo); registerTypeInfoOwner(typeInfo, codec); @@ -2689,7 +2705,10 @@ private JsonTypeInfo buildTypeInfo( exitObjectBinding(typeRef, bindingOwner); } } - JsonTypeInfo typeInfo = newTypeInfo(typeRef, codec); + JsonTypeInfo typeInfo = + resolved.factoryKey() == null + ? newTypeInfo(typeRef, codec) + : newRegisteredTypeInfo(typeRef, codec, null); publishTypeInfo(key, typeInfo); registerTypeInfoOwner(typeInfo, codec); resolveCodecTypes(codec, typeRef); @@ -2775,6 +2794,17 @@ private JsonTypeInfo newTypeInfo(TypeRef typeRef, JsonValueCodec codec) { return new JsonTypeInfo(typeRef, sharedRegistry.kind(typeRef.getRawType()), bindCodec(codec)); } + private JsonTypeInfo newRegisteredTypeInfo( + TypeRef typeRef, JsonValueCodec codec, String objectFactoryKey) { + return new JsonTypeInfo( + typeRef, + sharedRegistry.kind(typeRef.getRawType()), + bindCodec(codec), + false, + objectFactoryKey, + codec instanceof ObjectCodec ? null : codec.getClass()); + } + private JsonTypeInfo newTypeInfo( TypeRef typeRef, JsonFieldKind kind, JsonValueCodec codec, boolean annotationCodec) { return new JsonTypeInfo(typeRef, kind, bindCodec(codec), annotationCodec); 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 6607bf3cdd..40b5947b97 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 @@ -115,7 +115,6 @@ final class ForyJsonGraalVMFeature implements Feature { private final Set> processedObjectModels = Collections.newSetFromMap(new IdentityHashMap<>()); private final ArrayList hostedConfigurations = new ArrayList<>(); - private boolean defaultConfigurationAdded; @Override public String getDescription() { @@ -157,7 +156,8 @@ public void duringAnalysis(DuringAnalysisAccess access) { } if (type == ForyJson.class) { registerBuiltInTypes(access); - addDefaultConfiguration(); + hostedConfigurations.add( + new HostedConfiguration(ForyJson.builder().buildConfig())); changed = true; } } @@ -259,14 +259,6 @@ private boolean registerProvider(DuringAnalysisAccess access, Class providerC return changed; } - private void addDefaultConfiguration() { - if (defaultConfigurationAdded) { - return; - } - defaultConfigurationAdded = true; - hostedConfigurations.add(new HostedConfiguration(ForyJson.builder().build().config())); - } - private boolean addFactoryRoot( DuringAnalysisAccess access, HostedConfiguration configuration, Class type) { boolean changed = configuration.factoryModels.add(type); @@ -348,6 +340,7 @@ private boolean generateConfigurations(DuringAnalysisAccess access) { } ArrayList> models = new ArrayList<>(selectedModels); models.sort(Comparator.comparing(Class::getName)); + boolean generated = false; for (Class model : models) { // A raw generic Class is not a schema. Hosted capabilities are generated only when a // concrete TypeRef occurrence is reached from a selected non-generic root; eagerly @@ -369,12 +362,14 @@ private boolean generateConfigurations(DuringAnalysisAccess access) { for (ObjectCodec objectModel : objectModels) { registerObjectModel(access, objectModel); } - Set> generatedClasses = - JsonGeneratedClassRegistry.register(configuration.registry); + generated = true; + changed = true; + } + if (generated) { + Set> generatedClasses = JsonGeneratedClassRegistry.register(configuration.registry); for (Class generatedClass : generatedClasses) { registerGeneratedClass(generatedClass); } - changed = true; } } return changed; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index 5dff34b30e..4f756fec23 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -637,7 +637,7 @@ public void semanticBindingsRemainOwners() throws Exception { controlled.json.fromJson( "{\"value\":\"raw\"}".getBytes(StandardCharsets.UTF_8), GenericAsyncBox.class); - assertEquals(controlled.executor.submittedTasks(), 10); + assertEquals(controlled.executor.submittedTasks(), 5); resolver.lockJIT(); try { JsonTypeInfo raw = resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class); @@ -648,9 +648,10 @@ public void semanticBindingsRemainOwners() throws Exception { resolver.unlockJIT(); } controlled.executor.runAll(); - assertNotSame( - resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class).utf8Reader(), - parameterized.utf8Reader()); + Object rawReader = + resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class).utf8Reader(); + assertNotSame(rawReader, parameterized.utf8Reader()); + assertSame(rawReader.getClass(), parameterized.utf8Reader().getClass()); JsonValueCodec codec = nullCodec(); CodecRegistry codecs = new CodecRegistry(); @@ -669,7 +670,7 @@ public void semanticBindingsRemainOwners() throws Exception { } @Test - public void sourceShapeIgnoresPublicationOrder() { + public void generatedFieldsIgnorePublicationOrder() { ForyJson parentFirstJson = ForyJson.builder().withAsyncCompilation(false).build(); JsonTypeResolver parentFirstResolver = currentTypeResolver(parentFirstJson); ObjectCodec parentFirstOwner = diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java index 4a03671442..0975ee72d2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java @@ -44,13 +44,14 @@ import java.util.List; import java.util.UUID; import org.apache.fory.json.codec.JsonValueCodec; +import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.resolver.CodecRegistry; import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.reflect.TypeRef; import org.testng.annotations.Test; public class JsonCodecRegistrationTest { - private static final Class[] PROTECTED_TYPES = { + private static final Class[] DEDICATED_TYPES = { boolean.class, Boolean.class, byte.class, @@ -93,10 +94,9 @@ public class JsonCodecRegistrationTest { @Test @SuppressWarnings({"rawtypes", "unchecked"}) - public void protectedBuiltinRegistrationsRejected() { + public void dedicatedTypeRegistrationsRejected() { JsonCodecFactory factory = (type, resolver, runtimeType) -> null; - for (Class type : PROTECTED_TYPES) { - assertTrue(CodecRegistry.isProtectedBuiltinType(type), type.getTypeName()); + for (Class type : DEDICATED_TYPES) { assertThrows( IllegalArgumentException.class, () -> ForyJson.builder().registerCodec((Class) type, nullCodec())); @@ -107,7 +107,7 @@ public void protectedBuiltinRegistrationsRejected() { } @Test - public void factoryHandledBuiltinRejectedBeforeMutation() { + public void factoryHandledDedicatedTypeRejected() { CodecRegistry registry = new CodecRegistry(); JsonCodecFactory factory = new JsonCodecFactory() { @@ -128,7 +128,7 @@ public List> handledRuntimeClasses() { } @Test - public void moduleExactBuiltinRejected() { + public void moduleExactDedicatedTypeRejected() { assertThrows( IllegalArgumentException.class, () -> @@ -147,10 +147,19 @@ public void applicationTypeRegistrationAllowed() { @Test public void otherBuiltinRegistrationAllowed() { CodecRegistry registry = new CodecRegistry(); - assertFalse(CodecRegistry.isProtectedBuiltinType(File.class)); registry.register(File.class, nullCodec()); assertTrue(registry.contains(File.class)); } + @Test + public void objectCodecRegistrationRejected() { + ForyJson source = ForyJson.builder().build(); + ObjectCodec codec = + JsonTestSupport.currentTypeResolver(source).getObjectCodec(ApplicationValue.class); + assertThrows( + IllegalArgumentException.class, + () -> ForyJson.builder().registerCodec(ApplicationValue.class, codec)); + } + public static final class ApplicationValue {} } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java index fff9d1ad05..1cbaad1ac1 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java @@ -23,7 +23,6 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertNotSame; -import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; @@ -49,18 +48,18 @@ import javax.security.auth.Subject; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; -import org.apache.fory.codegen.CodeGenerator; import org.apache.fory.json.annotation.JsonAnyGetter; import org.apache.fory.json.annotation.JsonAnySetter; import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonType; +import org.apache.fory.json.annotation.JsonUnwrapped; import org.apache.fory.json.codec.AbstractJsonValueCodec; import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.JsonObjectModel; import org.apache.fory.json.codec.JsonValueCodec; import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.TransparentUnboxedValueCodec; -import org.apache.fory.json.codec.UnboxedValueCodec; +import org.apache.fory.json.codegen.GeneratedCodecKey; import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.data.PublicFields; import org.apache.fory.json.reader.JsonReader; @@ -119,7 +118,7 @@ public void outerNullabilityReusesCollectionClasses() { } @Test - public void injectedComponentMetadataReusesParentClass() { + public void injectedMetadataReusesParentClass() { JsonTypeResolver resolver = resolver(); TypeRef nonNullArray = TypeRef.of( @@ -132,6 +131,111 @@ public void injectedComponentMetadataReusesParentClass() { assertSame(nonNull.utf8Reader().getClass(), nullable.utf8Reader().getClass()); } + @Test + public void genericScalarInputsReuseClass() { + ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); + JsonTypeInfo strings = resolver.getTypeInfo(new TypeRef>() {}); + JsonTypeInfo integers = resolver.getTypeInfo(new TypeRef>() {}); + JsonTypeInfo bytes = resolver.getTypeInfo(new TypeRef>() {}); + JsonTypeInfo enums = resolver.getTypeInfo(new TypeRef>() {}); + assertObjectClasses(strings, integers); + assertObjectClasses(strings, bytes); + assertObjectClasses(strings, enums); + + GenericModel stringValue = new GenericModel<>(); + stringValue.value = "value"; + GenericModel intValue = new GenericModel<>(); + intValue.value = 7; + GenericModel byteValue = new GenericModel<>(); + byteValue.value = (byte) 3; + GenericModel enumValue = new GenericModel<>(); + enumValue.value = FirstEnum.VALUE; + TypeRef> stringType = new TypeRef>() {}; + TypeRef> intType = new TypeRef>() {}; + TypeRef> byteType = new TypeRef>() {}; + TypeRef> enumType = new TypeRef>() {}; + assertEquals(json.toJson(stringValue, stringType), "{\"value\":\"value\"}"); + assertEquals(json.toJson(intValue, intType), "{\"value\":7}"); + assertEquals(json.toJson(byteValue, byteType), "{\"value\":3}"); + assertEquals(json.toJson(enumValue, enumType), "{\"value\":\"VALUE\"}"); + assertEquals(json.fromJson(json.toJson(stringValue, stringType), stringType).value, "value"); + assertEquals(json.fromJson(json.toJson(intValue, intType), intType).value, 7); + assertEquals(json.fromJson(json.toJson(byteValue, byteType), byteType).value, (byte) 3); + assertSame( + json.fromJson(json.toJson(enumValue, enumType), enumType).value, FirstEnum.VALUE); + } + + @Test + public void equivalentGenericInputsReuseClass() { + ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); + JsonTypeInfo first = resolver.getTypeInfo(new TypeRef>() {}); + JsonTypeInfo second = resolver.getTypeInfo(new TypeRef>() {}); + assertObjectClasses(first, second); + + GenericModel firstValue = new GenericModel<>(); + firstValue.value = new Child(); + firstValue.value.value = "first"; + GenericModel secondValue = new GenericModel<>(); + secondValue.value = new OtherChild(); + secondValue.value.value = 2; + TypeRef> firstType = new TypeRef>() {}; + TypeRef> secondType = new TypeRef>() {}; + assertEquals(json.toJson(firstValue, firstType), "{\"value\":{\"value\":\"first\"}}"); + assertEquals(json.toJson(secondValue, secondType), "{\"value\":{\"value\":2}}"); + assertEquals(json.fromJson(json.toJson(firstValue, firstType), firstType).value.value, "first"); + assertEquals(json.fromJson(json.toJson(secondValue, secondType), secondType).value.value, 2); + } + + @Test + public void genericCollectionInputsReuseClass() { + ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); + TypeRef> stringType = new TypeRef>() {}; + TypeRef> childType = new TypeRef>() {}; + assertObjectClasses(resolver.getTypeInfo(stringType), resolver.getTypeInfo(childType)); + + GenericCollection strings = new GenericCollection<>(); + strings.values = Collections.singletonList("value"); + GenericCollection children = new GenericCollection<>(); + Child child = new Child(); + child.value = "child"; + children.values = Collections.singletonList(child); + assertEquals( + json.fromJson(json.toJson(strings, stringType), stringType).values.get(0), "value"); + assertEquals( + json.fromJson(json.toJson(children, childType), childType).values.get(0).value, "child"); + } + + @Test + public void genericEnumInputsReuseClass() { + ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); + JsonTypeInfo first = resolver.getTypeInfo(new TypeRef>() {}); + JsonTypeInfo second = resolver.getTypeInfo(new TypeRef>() {}); + assertObjectClasses(first, second); + + GenericModel firstValue = new GenericModel<>(); + firstValue.value = FirstEnum.VALUE; + GenericModel secondValue = new GenericModel<>(); + secondValue.value = SecondEnum.VALUE; + TypeRef> firstType = new TypeRef>() {}; + TypeRef> secondType = new TypeRef>() {}; + assertSame(json.fromJson(json.toJson(firstValue, firstType), firstType).value, FirstEnum.VALUE); + assertSame( + json.fromJson(json.toJson(secondValue, secondType), secondType).value, SecondEnum.VALUE); + } + + @Test + public void genericRegisteredCodecUsesBaseCapability() { + ForyJson json = parentJson(new ChildCodecA()); + JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); + JsonTypeInfo first = resolver.getTypeInfo(new TypeRef>() {}); + JsonTypeInfo second = resolver.getTypeInfo(new TypeRef>() {}); + assertObjectClasses(first, second); + } + @Test public void fixedSubtypeIsGeneratedLeaf() { ForyJson json = @@ -198,6 +302,24 @@ public void directCodecClassVersionsParent() { assertDifferentObjectClasses(first, different); } + @Test + public void hiddenCodecClassesReuseParent() { + JsonTypeInfo first = parentType(new HiddenChildCodecA()); + JsonTypeInfo second = parentType(new HiddenChildCodecB()); + + assertObjectClasses(first, second); + } + + @Test + public void objectFactoryVersionsClass() throws Exception { + assertDifferentObjectClasses(factoryModelType(true), factoryModelType(false)); + } + + @Test + public void unwrappedFactoryVersionsParent() throws Exception { + assertDifferentObjectClasses(unwrappedFactoryType(true), unwrappedFactoryType(false)); + } + @Test public void directCodecStateStaysInstanceOwned() { ForyJson first = parentJson(new StatefulChildCodec("first:")); @@ -217,7 +339,39 @@ public void directCodecStateStaysInstanceOwned() { } @Test - public void unrelatedRegistrationDoesNotVersionParent() { + public void transparentTerminalStaysInstanceOwned() throws Exception { + ForyJson first = transparentJson(new TerminalCodecA()); + ForyJson second = transparentJson(new TerminalCodecB()); + JsonTypeInfo firstType = transparentType(first); + JsonTypeInfo secondType = transparentType(second); + assertObjectClasses(firstType, secondType); + + TransparentModel value = new TransparentModel(); + value.setValue(new LocalTerminal("value")); + assertEquals(first.toJson(value), "{\"value\":\"a:value\"}"); + assertEquals(second.toJson(value), "{\"value\":\"b:value\"}"); + assertEquals( + ((LocalTerminal) + first.fromJson("{\"value\":\"a:value\"}", TransparentModel.class).getValue()) + .value, + "value"); + assertEquals( + ((LocalTerminal) + second.fromJson("{\"value\":\"b:value\"}", TransparentModel.class).getValue()) + .value, + "value"); + } + + @Test + public void transparentDirectTerminalVersionsClass() throws Exception { + JsonTypeInfo first = transparentType(transparentJson(new DirectTerminalCodecA())); + JsonTypeInfo second = transparentType(transparentJson(new DirectTerminalCodecB())); + + assertDifferentObjectClasses(first, second); + } + + @Test + public void unrelatedRegistrationReusesParent() { JsonTypeInfo first = parentType(new ChildCodecA()); ForyJson json = parentJson(new ChildCodecA(), true); assertObjectClasses(first, parentType(json)); @@ -239,20 +393,26 @@ public void configuredLoaderDoesNotVersionClass() { } @Test - public void rawWriteNullVersionsReaders() { + public void writeNullVersionsWriters() { ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); ForyJson second = ForyJson.builder().writeNullFields(true).withAsyncCompilation(false).build(); - assertDifferentObjectClasses( - JsonTestSupport.currentTypeResolver(first).getTypeInfo(Model.class, Model.class), - JsonTestSupport.currentTypeResolver(second).getTypeInfo(Model.class, Model.class)); + JsonTypeInfo firstType = + JsonTestSupport.currentTypeResolver(first).getTypeInfo(Model.class, Model.class); + JsonTypeInfo secondType = + JsonTestSupport.currentTypeResolver(second).getTypeInfo(Model.class, Model.class); + assertNotSame(firstType.stringWriter().getClass(), secondType.stringWriter().getClass()); + assertNotSame(firstType.utf8Writer().getClass(), secondType.utf8Writer().getClass()); + assertSame(firstType.latin1Reader().getClass(), secondType.latin1Reader().getClass()); + assertSame(firstType.utf16Reader().getClass(), secondType.utf16Reader().getClass()); + assertSame(firstType.utf8Reader().getClass(), secondType.utf8Reader().getClass()); } @Test - public void inactiveAnyDirectionReusesClass() { + public void anyCodecUsesCapabilityInterface() { JsonTypeInfo getterA = anyType(GetterAny.class, new ChildCodecA()); JsonTypeInfo getterB = anyType(GetterAny.class, new ChildCodecB()); - assertNotSame(getterA.stringWriter().getClass(), getterB.stringWriter().getClass()); - assertNotSame(getterA.utf8Writer().getClass(), getterB.utf8Writer().getClass()); + assertSame(getterA.stringWriter().getClass(), getterB.stringWriter().getClass()); + assertSame(getterA.utf8Writer().getClass(), getterB.utf8Writer().getClass()); assertSame(getterA.latin1Reader().getClass(), getterB.latin1Reader().getClass()); assertSame(getterA.utf16Reader().getClass(), getterB.utf16Reader().getClass()); assertSame(getterA.utf8Reader().getClass(), getterB.utf8Reader().getClass()); @@ -261,55 +421,20 @@ public void inactiveAnyDirectionReusesClass() { JsonTypeInfo setterB = anyType(SetterAny.class, new ChildCodecB()); assertSame(setterA.stringWriter().getClass(), setterB.stringWriter().getClass()); assertSame(setterA.utf8Writer().getClass(), setterB.utf8Writer().getClass()); - assertNotSame(setterA.latin1Reader().getClass(), setterB.latin1Reader().getClass()); - assertNotSame(setterA.utf16Reader().getClass(), setterB.utf16Reader().getClass()); - assertNotSame(setterA.utf8Reader().getClass(), setterB.utf8Reader().getClass()); + assertSame(setterA.latin1Reader().getClass(), setterB.latin1Reader().getClass()); + assertSame(setterA.utf16Reader().getClass(), setterB.utf16Reader().getClass()); + assertSame(setterA.utf8Reader().getClass(), setterB.utf8Reader().getClass()); } @Test - public void terminalDirectMethodsChangeKey() throws Exception { - JsonTypeInfo first = directTerminal(new VariableDirectCodec(false)); - JsonTypeInfo second = directTerminal(new VariableDirectCodec(true)); - TerminalTransparentCodec firstCodec = new TerminalTransparentCodec(first); - TerminalTransparentCodec secondCodec = new TerminalTransparentCodec(second); - - assertNotEquals(unboxedKeyParts(firstCodec, false), unboxedKeyParts(secondCodec, false)); - assertNotEquals(unboxedKeyParts(firstCodec, true), unboxedKeyParts(secondCodec, true)); - } - - @Test - @SuppressWarnings({"rawtypes", "unchecked"}) - public void hostedSiblingCodecUsesInterface() throws Exception { - String packageName = "org.apache.fory.json.sibling"; - Path targetOutput = Files.createTempDirectory("fory-json-sibling-target"); - compileSource( - targetOutput, - packageName, - "SiblingModel", - "public final class SiblingModel { public " + Child.class.getCanonicalName() + " child; }"); - Path codecOutput = Files.createTempDirectory("fory-json-sibling-codec"); - compileSource( - codecOutput, - packageName, - "SiblingChildCodec", - "public final class SiblingChildCodec extends " - + ChildCodecA.class.getCanonicalName() - + " {}"); - try (URLClassLoader targetLoader = - new URLClassLoader( - new URL[] {targetOutput.toUri().toURL()}, getClass().getClassLoader()); - URLClassLoader codecLoader = - new URLClassLoader( - new URL[] {codecOutput.toUri().toURL()}, getClass().getClassLoader())) { - Class target = Class.forName(packageName + ".SiblingModel", true, targetLoader); - Class codecType = Class.forName(packageName + ".SiblingChildCodec", true, codecLoader); - JsonValueCodec codec = - (JsonValueCodec) codecType.getDeclaredConstructor().newInstance(); - ForyJson configured = parentJson(codec); - JsonTypeResolver resolver = hostedResolver(configured); - List> models = resolver.generateHostedCodecs(target); - assertTrue(models.stream().anyMatch(model -> model.type() == target)); - } + public void hostedCodecUsesInterface() throws Exception { + JsonTypeResolver first = hostedResolver(parentJson(new ChildCodecA())); + JsonTypeResolver second = hostedResolver(parentJson(new ChildCodecB())); + first.generateHostedCodecs(Parent.class); + second.generateHostedCodecs(Parent.class); + assertObjectClasses( + first.getTypeInfo(Parent.class, Parent.class), + second.getTypeInfo(Parent.class, Parent.class)); } @Test @@ -340,16 +465,19 @@ public void hostedConcealedTypeUsesInterpretedRole() throws Exception { } @Test - public void hostedBootstrapPackageNeedsOwner() throws Exception { - Method method = - JsonCodegen.class.getDeclaredMethod("hostedDefinitionOwner", Class.class, String.class); - method.setAccessible(true); - assertNull(method.invoke(null, Subject.class, CodeGenerator.getPackage(Subject.class))); + public void hostedBootstrapUsesInterpretedRole() throws Exception { + JsonTypeResolver resolver = + hostedResolver( + ForyJson.builder() + .registerCodec(java.security.Principal.class, JsonTestSupport.nullCodec()) + .build()); + resolver.generateHostedCodecs(Subject.class); + assertInterpretedObject(resolver.getTypeInfo(Subject.class, Subject.class)); } @Test @SuppressWarnings({"rawtypes", "unchecked"}) - public void hostedTransparentTerminalUsesInterpretedRole() throws Exception { + public void hostedTerminalUsesInterpretedRole() throws Exception { String packageName = "org.apache.fory.json.terminal"; Path terminalOutput = Files.createTempDirectory("fory-json-terminal"); compileSource( @@ -441,6 +569,12 @@ public void sameNamedLoaderClassesDoNotCollide() throws Exception { Class firstClass = shadowClass(PublicFields.class, bytes); Class secondClass = shadowClass(PublicFields.class, bytes); assertNotSame(firstClass, secondClass); + GeneratedCodecKey firstKey = + GeneratedCodecKey.object(firstClass, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + GeneratedCodecKey secondKey = + GeneratedCodecKey.object(secondClass, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + assertEquals(firstKey.hashCode(), secondKey.hashCode()); + assertNotEquals(firstKey, secondKey); JsonTypeInfo first = loaderType(firstClass); JsonTypeInfo second = loaderType(secondClass); @@ -473,12 +607,90 @@ private static JsonTypeInfo parentType(ForyJson json) { return JsonTestSupport.currentTypeResolver(json).getTypeInfo(Parent.class, Parent.class); } + private static ForyJson transparentJson(JsonValueCodec terminalCodec) + throws Exception { + JsonObjectModel model = transparentModel(); + return ForyJson.builder() + .registerCodec(LocalTerminal.class, terminalCodec) + .registerCodec( + SiblingValue.class, + (type, resolver, runtimeType) -> + new SiblingTransparentCodec( + resolver.getTypeInfo(LocalTerminal.class, LocalTerminal.class))) + .registerCodec( + TransparentModel.class, + (type, resolver, runtimeType) -> resolver.createObjectCodec(type, model)) + .withAsyncCompilation(false) + .build(); + } + + private static JsonTypeInfo transparentType(ForyJson json) { + return JsonTestSupport.currentTypeResolver(json) + .getTypeInfo(TransparentModel.class, TransparentModel.class); + } + + private static Method publicMethod(Class owner, String name, Class... parameterTypes) { + try { + return owner.getMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + private static JsonTypeInfo collectionType(JsonValueCodec codec) { ForyJson json = ForyJson.builder().registerCodec(Child.class, codec).withAsyncCompilation(false).build(); return JsonTestSupport.currentTypeResolver(json).getTypeInfo(new TypeRef>() {}); } + private static JsonTypeInfo factoryModelType(boolean first) throws Exception { + JsonObjectModel model = factoryModel(first); + JsonCodecFactory factory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return resolver.createObjectCodec(type, model); + } + + @Override + public String factoryKey() { + return first ? "factory-model-first" : "factory-model-second"; + } + }; + ForyJson json = + ForyJson.builder() + .registerCodec(FactoryModel.class, factory) + .withAsyncCompilation(false) + .build(); + return JsonTestSupport.currentTypeResolver(json) + .getTypeInfo(FactoryModel.class, FactoryModel.class); + } + + private static JsonTypeInfo unwrappedFactoryType(boolean first) throws Exception { + JsonObjectModel model = factoryModel(first); + JsonCodecFactory factory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return resolver.createObjectCodec(type, model); + } + + @Override + public String factoryKey() { + return first ? "unwrapped-first" : "unwrapped-second"; + } + }; + ForyJson json = + ForyJson.builder() + .registerCodec(FactoryModel.class, factory) + .withAsyncCompilation(false) + .build(); + return JsonTestSupport.currentTypeResolver(json) + .getTypeInfo(UnwrappedFactoryModel.class, UnwrappedFactoryModel.class); + } + @SuppressWarnings({"rawtypes", "unchecked"}) private static JsonTypeInfo anyType(Class type, JsonValueCodec codec) { ForyJson json = parentJson(codec); @@ -493,36 +705,6 @@ private static JsonTypeResolver hostedResolver(ForyJson json) throws Exception { return new JsonTypeResolver(constructor.newInstance(json.config(), null, true)); } - private static JsonTypeInfo directTerminal(VariableDirectCodec codec) { - JsonCodecFactory factory = - (type, resolver, runtimeType) -> - type.getRawType() == int.class - && type.getTypeExtMeta() != null - && type.getTypeExtMeta().typeId() == Types.UINT32 - ? codec - : null; - ForyJson json = - ForyJson.builder().withModule(context -> context.registerCodecFactory(factory)).build(); - return JsonTestSupport.currentTypeResolver(json) - .getTypeInfo(TypeRef.of(int.class, TypeExtMeta.of(Types.UINT32, false, false))); - } - - private static List unboxedKeyParts(UnboxedValueCodec codec, boolean reader) - throws Exception { - Method method = - Class.forName("org.apache.fory.json.resolver.GeneratedCodecKeyBuilder") - .getDeclaredMethod( - "addUnboxedKeyParts", - UnboxedValueCodec.class, - boolean.class, - ArrayList.class, - ArrayList.class); - method.setAccessible(true); - ArrayList keyParts = new ArrayList<>(); - method.invoke(null, codec, reader, keyParts, new ArrayList>()); - return keyParts; - } - @SuppressWarnings({"rawtypes", "unchecked"}) private static JsonTypeInfo loaderType(Class type) { ForyJson json = @@ -608,6 +790,26 @@ private static JsonObjectModel transparentModel() throws Exception { new TypeRef[] {logicalType}); } + private static JsonObjectModel factoryModel(boolean first) throws Exception { + String name = first ? "first" : "second"; + Class valueType = first ? String.class : long.class; + String getter = first ? "getFirst" : "getSecond"; + String setter = first ? "setFirst" : "setSecond"; + return new JsonObjectModel( + FactoryModel.class.getConstructor(), + null, + new String[0], + new Method[0], + new Method[0], + new int[0], + new boolean[0], + new TypeRef[0], + new String[] {name}, + new Method[] {FactoryModel.class.getMethod(getter)}, + new Method[] {FactoryModel.class.getMethod(setter, valueType)}, + new TypeRef[] {TypeRef.of(valueType)}); + } + private static TypeExtMeta ordinary(boolean nullable) { return TypeExtMeta.of(Types.UNKNOWN, nullable, false, false, false); } @@ -666,6 +868,62 @@ public static final class Parent { public Parent() {} } + public static final class GenericModel { + public T value; + + public GenericModel() {} + } + + public static final class GenericPair { + public T first; + public U second; + + public GenericPair() {} + } + + public static final class GenericCollection { + public List values; + + public GenericCollection() {} + } + + public enum FirstEnum { + VALUE + } + + public enum SecondEnum { + VALUE + } + + public static final class FactoryModel { + private String first; + private long second; + + public FactoryModel() {} + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + public long getSecond() { + return second; + } + + public void setSecond(long second) { + this.second = second; + } + } + + public static final class UnwrappedFactoryModel { + @JsonUnwrapped public FactoryModel value; + + public UnwrappedFactoryModel() {} + } + public static final class GetterAny { private final Map values = new LinkedHashMap<>(); @@ -690,8 +948,22 @@ public static final class Child { public Child() {} } + public static final class OtherChild { + public long value; + + public OtherChild() {} + } + public interface SiblingCarrier {} + public static final class LocalTerminal implements SiblingCarrier { + final String value; + + LocalTerminal(String value) { + this.value = value; + } + } + public static final class SiblingValue {} public static final class TransparentModel { @@ -743,204 +1015,182 @@ private static Child child(String text) { public static final class ChildCodecB extends ChildCodecA {} - public static final class StatefulChildCodec extends ChildCodecA { + private static final class HiddenChildCodecA extends ChildCodecA {} + + private static final class HiddenChildCodecB extends ChildCodecA {} + + public abstract static class TerminalCodec implements JsonValueCodec { private final String prefix; - public StatefulChildCodec(String prefix) { + TerminalCodec(String prefix) { this.prefix = prefix; } @Override - public void writeString(StringJsonWriter writer, Child value) { + public void writeString(StringJsonWriter writer, LocalTerminal value) { writer.writeString(prefix + value.value); } @Override - public void writeUtf8(Utf8JsonWriter writer, Child value) { + public void writeUtf8(Utf8JsonWriter writer, LocalTerminal value) { writer.writeString(prefix + value.value); } @Override - public Child readLatin1(Latin1JsonReader reader) { - return childWithoutPrefix(reader.readString()); + public LocalTerminal readLatin1(Latin1JsonReader reader) { + return terminal(reader.readString()); } @Override - public Child readUtf16(Utf16JsonReader reader) { - return childWithoutPrefix(reader.readString()); + public LocalTerminal readUtf16(Utf16JsonReader reader) { + return terminal(reader.readString()); } @Override - public Child readUtf8(Utf8JsonReader reader) { - return childWithoutPrefix(reader.readString()); + public LocalTerminal readUtf8(Utf8JsonReader reader) { + return terminal(reader.readString()); } - private Child childWithoutPrefix(String text) { - Child value = new Child(); - value.value = text.substring(prefix.length()); - return value; + private LocalTerminal terminal(String value) { + return new LocalTerminal(value.substring(prefix.length())); } } - public static class VariableDirectCodec extends AbstractJsonValueCodec - implements DirectUnboxedValueCodec { - private final boolean alternate; - - public VariableDirectCodec(boolean alternate) { - this.alternate = alternate; + public static final class TerminalCodecA extends TerminalCodec { + TerminalCodecA() { + super("a:"); } + } - @Override - public void write(JsonWriter writer, Integer value) { - writer.writeInt(value); + public static final class TerminalCodecB extends TerminalCodec { + TerminalCodecB() { + super("b:"); } + } - @Override - public Integer read(JsonReader reader) { - return reader.readInt(); + public abstract static class DirectTerminalCodec extends TerminalCodec + implements DirectUnboxedValueCodec { + DirectTerminalCodec(String prefix) { + super(prefix); } @Override public Class carrierType() { - return int.class; + return LocalTerminal.class; } @Override public Object readLatin1Carrier(Latin1JsonReader reader) { - return reader.readInt(); + return readLatin1(reader); } @Override public Object readUtf16Carrier(Utf16JsonReader reader) { - return reader.readInt(); + return readUtf16(reader); } @Override public Object readUtf8Carrier(Utf8JsonReader reader) { - return reader.readInt(); + return readUtf8(reader); } @Override public void writeStringCarrier(StringJsonWriter writer, Object carrier) { - writer.writeInt((Integer) carrier); + writeString(writer, (LocalTerminal) carrier); } @Override public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { - writer.writeInt((Integer) carrier); + writeUtf8(writer, (LocalTerminal) carrier); + } + } + + public static final class DirectTerminalCodecA extends DirectTerminalCodec { + DirectTerminalCodecA() { + super("a:"); } @Override public Method readCarrierMethod() { - return method(alternate ? "readSecond" : "readFirst", JsonReader.class); + return publicMethod(DirectTerminalCodecA.class, "read", JsonReader.class); } @Override public Method writeCarrierMethod() { - return method(alternate ? "writeSecond" : "writeFirst", JsonWriter.class, int.class); - } - - public static int readFirst(JsonReader reader) { - return reader.readInt(); - } - - public static int readSecond(JsonReader reader) { - return reader.readInt(); - } - - public static void writeFirst(JsonWriter writer, int value) { - writer.writeInt(value); + return publicMethod( + DirectTerminalCodecA.class, "write", JsonWriter.class, LocalTerminal.class); } - public static void writeSecond(JsonWriter writer, int value) { - writer.writeInt(value); + public static LocalTerminal read(JsonReader reader) { + return new LocalTerminal(reader.readString().substring(2)); } - private static Method method(String name, Class... parameters) { - try { - return VariableDirectCodec.class.getMethod(name, parameters); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); - } + public static void write(JsonWriter writer, LocalTerminal value) { + writer.writeString("a:" + value.value); } } - public static final class TerminalTransparentCodec extends AbstractJsonValueCodec - implements TransparentUnboxedValueCodec { - private final JsonTypeInfo valueTypeInfo; - - public TerminalTransparentCodec(JsonTypeInfo valueTypeInfo) { - this.valueTypeInfo = valueTypeInfo; + public static final class DirectTerminalCodecB extends DirectTerminalCodec { + DirectTerminalCodecB() { + super("b:"); } @Override - public JsonTypeInfo valueTypeInfo() { - return valueTypeInfo; - } - - @Override - public Object constructCarrier(JsonReader reader, Object value) { - return value; + public Method readCarrierMethod() { + return publicMethod(DirectTerminalCodecB.class, "read", JsonReader.class); } @Override - public Object extractValue(Object carrier) { - return carrier; + public Method writeCarrierMethod() { + return publicMethod( + DirectTerminalCodecB.class, "write", JsonWriter.class, LocalTerminal.class); } - @Override - public Method[] constructMethods() { - return new Method[0]; + public static LocalTerminal read(JsonReader reader) { + return new LocalTerminal(reader.readString().substring(2)); } - @Override - public int[] constructBoxBytes() { - return new int[0]; - } - - @Override - public Method[] extractMethods() { - return new Method[0]; + public static void write(JsonWriter writer, LocalTerminal value) { + writer.writeString("b:" + value.value); } + } - @Override - public void write(JsonWriter writer, Integer value) { - writer.writeInt(value); - } + public static final class StatefulChildCodec extends ChildCodecA { + private final String prefix; - @Override - public Integer read(JsonReader reader) { - return reader.readInt(); + public StatefulChildCodec(String prefix) { + this.prefix = prefix; } @Override - public Class carrierType() { - return int.class; + public void writeString(StringJsonWriter writer, Child value) { + writer.writeString(prefix + value.value); } @Override - public Object readLatin1Carrier(Latin1JsonReader reader) { - return reader.readInt(); + public void writeUtf8(Utf8JsonWriter writer, Child value) { + writer.writeString(prefix + value.value); } @Override - public Object readUtf16Carrier(Utf16JsonReader reader) { - return reader.readInt(); + public Child readLatin1(Latin1JsonReader reader) { + return childWithoutPrefix(reader.readString()); } @Override - public Object readUtf8Carrier(Utf8JsonReader reader) { - return reader.readInt(); + public Child readUtf16(Utf16JsonReader reader) { + return childWithoutPrefix(reader.readString()); } @Override - public void writeStringCarrier(StringJsonWriter writer, Object carrier) { - writer.writeInt((Integer) carrier); + public Child readUtf8(Utf8JsonReader reader) { + return childWithoutPrefix(reader.readString()); } - @Override - public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { - writer.writeInt((Integer) carrier); + private Child childWithoutPrefix(String text) { + Child value = new Child(); + value.value = text.substring(prefix.length()); + return value; } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index c49fa15999..bdc0f6cb84 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -182,7 +182,7 @@ public void readGeneratedCollectionFields(boolean codegen) { } @Test(dataProvider = "enableCodegen") - public void sameConfigUsesSameClass(boolean codegen) throws Exception { + public void sameConfigUsesSameClass(boolean codegen) { ForyJson first = newJson(codegen); ForyJson second = newJson(codegen); ForyJson writeNullFields = newJsonBuilder(codegen).writeNullFields(true).build(); diff --git a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt index 9e9ac56988..fef1c9e10e 100644 --- a/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt +++ b/kotlin/fory-json-kotlin/src/test/kotlin/org/apache/fory/json/kotlin/KotlinRuntimeTestSupport.kt @@ -141,12 +141,13 @@ private fun compileStates(json: ForyJson): Map { codegen.javaClass.getDeclaredMethod( "compiler", GeneratedCodecKey::class.java, + Class::class.java, String::class.java, String::class.java, ) compiler.isAccessible = true return futures.keys - .map { compiler.invoke(codegen, it, "", "") } + .map { key -> compiler.invoke(codegen, key, key.targetClass(), "", "") } .map { ReflectionUtils.getObjectFieldValue(it, "codeGenerator") } .distinct() .flatMap { From 6eb37590c97bf004caf3381904f4d7d220380b86 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 23 Aug 2026 02:06:39 +0800 Subject: [PATCH 10/11] refactor(json): finalize generated codec key ownership --- .agents/languages/java.md | 5 +- docs/json/custom-codecs.md | 4 + docs/json/graalvm.md | 5 +- .../org/apache/fory/graalvm/kotlin/Main.java | 2 +- integration_tests/graalvm_tests/README.md | 5 +- .../apache/fory/graalvm/ForyJsonExample.java | 22 +- .../apache/fory/json/JsonCodecFactory.java | 8 +- .../json/codec/DirectUnboxedValueCodec.java | 5 +- .../codec/TransparentUnboxedValueCodec.java | 6 +- .../fory/json/codegen/GeneratedCodecKey.java | 113 ++++- .../apache/fory/json/codegen/JsonCodegen.java | 140 ++++-- .../fory/json/codegen/JsonReaderCodegen.java | 14 +- .../fory/json/codegen/JsonWriterCodegen.java | 2 +- .../json/codegen/StringWriterCodegen.java | 2 +- .../fory/json/codegen/Utf8WriterCodegen.java | 2 +- .../fory/json/meta/JsonCreatorFieldInfo.java | 5 - .../apache/fory/json/meta/JsonFieldInfo.java | 30 -- .../apache/fory/json/meta/JsonFieldKind.java | 29 +- .../fory/json/resolver/CodecRegistry.java | 6 +- .../resolver/GeneratedCodecKeyBuilder.java | 249 +++++------ .../resolver/JsonGeneratedClassRegistry.java | 2 + .../fory/json/resolver/JsonTypeInfo.java | 23 +- .../fory/json/resolver/JsonTypeResolver.java | 209 ++------- .../fory/json/JsonAsyncCompilationTest.java | 4 +- .../fory/json/JsonCodecRegistrationTest.java | 31 ++ .../org/apache/fory/json/JsonCreatorTest.java | 9 +- .../json/JsonGeneratedCapabilityKeyTest.java | 422 +++++++++--------- .../org/apache/fory/json/JsonScalarTest.java | 25 +- .../org/apache/fory/json/JsonTestSupport.java | 42 ++ .../apache/fory/json/JsonTypeCheckerTest.java | 4 +- 30 files changed, 720 insertions(+), 705 deletions(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index e2e80eff26..f4848db70a 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -90,8 +90,9 @@ Load this file when changing anything under `java/` or when Java drives a cross- `@JsonCodec`, `@JsonFormat`, and semantic metadata remain separate from exact registry mutation and are fixed by the target class or effective Mixin. - Fory JSON `ObjectCodec` instances are resolver-owned and must not be registered directly. A - language module that supplies a custom object model must use a `JsonCodecFactory`, whose stable - factory key participates in the generated object-class identity. + language module that supplies a custom object model must use a `JsonCodecFactory`. A configurable + factory's stable key must cover every option that can change its created codec class, object + model, or generated operations. - Do not add normal-JVM process-global caches keyed by user classes, generated classes, serializer classes, classloaders, or class-bound method handles. Prefer per-runtime state, immutable shared metadata, or build-time-only template data. The only exception is Fory JSON's generated-role diff --git a/docs/json/custom-codecs.md b/docs/json/custom-codecs.md index fbb94d4f5f..5a8fd9ca5b 100644 --- a/docs/json/custom-codecs.md +++ b/docs/json/custom-codecs.md @@ -104,6 +104,10 @@ ForyJson json = .build(); ``` +A configurable factory must override `factoryKey()` with a deterministic value covering every +option that can change the created codec class, object model, or generated operations. The default +factory class name is sufficient only for a configuration-free factory. + `runtimeType` is `true` only when the factory is selecting a codec for the actual class of a value during a dynamic write. Declared roots and composite child types receive `false`. A composite codec that needs this distinction after construction must retain the flag for its later `resolveTypes` diff --git a/docs/json/graalvm.md b/docs/json/graalvm.md index a0e65c83f4..6cba20f3cb 100644 --- a/docs/json/graalvm.md +++ b/docs/json/graalvm.md @@ -110,6 +110,7 @@ Default-configuration codecs remain available when providers are present, and ev provider adds codecs for its configuration. A codegen-enabled runtime uses an interpreted codec whenever no matching generated codec is available. Reflection metadata remains available in either case. + `withCodegen(false)` explicitly selects interpreted codecs and does not request generated-codec lookup. Asynchronous compilation is disabled in a native executable. @@ -138,8 +139,8 @@ construction. A Kotlin-enabled runtime configuration with no matching generated prepared interpreted codec. An exact generic Kotlin root is available only when its complete binding is reached through a -property, constructor argument, container/map child, or closed subtype of a provider-selected -concrete root. Keep using `jsonTypeRef()` at the direct root call; no public root registry or +property, constructor argument, container/map child, or closed subtype of a reachable concrete +root. Keep using `jsonTypeRef()` at the direct root call; no public root registry or reflection configuration is needed. ## Mixins diff --git a/integration_tests/graalvm_kotlin_tests/src/main/java/org/apache/fory/graalvm/kotlin/Main.java b/integration_tests/graalvm_kotlin_tests/src/main/java/org/apache/fory/graalvm/kotlin/Main.java index b27280ac97..344e31e5d6 100644 --- a/integration_tests/graalvm_kotlin_tests/src/main/java/org/apache/fory/graalvm/kotlin/Main.java +++ b/integration_tests/graalvm_kotlin_tests/src/main/java/org/apache/fory/graalvm/kotlin/Main.java @@ -25,7 +25,7 @@ import org.apache.fory.json.annotation.ForyJsonProvider; import org.apache.fory.json.kotlin.ForyJsonKotlin; -/** Native Image acceptance application for provider-selected Kotlin JSON capabilities. */ +/** Native Image acceptance application for provider-added Kotlin JSON capabilities. */ public final class Main { private Main() {} diff --git a/integration_tests/graalvm_tests/README.md b/integration_tests/graalvm_tests/README.md index 5462709cd6..4f15e19f2f 100644 --- a/integration_tests/graalvm_tests/README.md +++ b/integration_tests/graalvm_tests/README.md @@ -2,9 +2,8 @@ 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 hosted access metadata for unprovided configurations in one -native image. +`JsonMixin` target/source mappings, default and provider-added generated codecs, exact-key fallback +to interpreted codecs, and hosted access metadata for unmatched configurations in one native image. ## Test 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 7b202f46de..c428168616 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 @@ -119,9 +119,10 @@ private static void testHostedCodegenConfigurations() { ForyJson interpretedJson = newInterpretedJson(); exerciseCodegenConfiguration(DEFAULT_JSON, true, true); exerciseCodegenConfiguration(providerJson, true, true); - exerciseCodegenConfiguration(interpretedJson, false, false); + exerciseCodegenConfiguration(interpretedJson, false, true); + testRegisteredCodec(providerJson); testEmptyMixin(providerJson, true, true); - testEmptyMixin(interpretedJson, false, false); + testEmptyMixin(interpretedJson, false, true); testInterpretedMetadata(interpretedJson); testPrimitiveProperties(interpretedJson); testIndependentChildCodegen(); @@ -239,6 +240,16 @@ private static void exerciseCodegenConfiguration( .equals("probe")); } + private static void testRegisteredCodec(ForyJson json) { + CodegenProbeCodec.expect(RegisteredCodecModel.class, true); + RegisteredCodecModel value = new RegisteredCodecModel(); + value.probe = new CodegenProbeValue("registered"); + String encoded = json.toJson(value); + Preconditions.checkArgument(encoded.equals("{\"probe\":\"registered\"}")); + Preconditions.checkArgument( + json.fromJson(encoded, RegisteredCodecModel.class).probe.value.equals("registered")); + } + private static void testClosedPackage() { ClosedJsonRecord value = new ClosedJsonRecord(17, "closed"); ForyJson interpreted = ForyJson.builder().build(); @@ -704,6 +715,13 @@ public CodegenProbeChild(String name) { } } + @JsonType + public static final class RegisteredCodecModel { + public CodegenProbeValue probe; + + public RegisteredCodecModel() {} + } + public static final class EmptyMixinTarget { @JsonCodec(CodegenProbeCodec.class) private CodegenProbeValue probe; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java index 234ddef9e6..39a1e69c96 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonCodecFactory.java @@ -37,7 +37,13 @@ public interface JsonCodecFactory { */ JsonValueCodec create(TypeRef type, JsonTypeResolver resolver, boolean runtimeType); - /** Returns the deterministic semantic identity of this factory configuration. */ + /** + * Returns the deterministic semantic identity of this factory configuration. + * + *

A configurable factory must override this method and include every option that can change + * the created codec class, object model, or generated operations. The default class name is + * sufficient only for a configuration-free factory. + */ default String factoryKey() { return getClass().getName(); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java index 4f23740666..0d6fbaae99 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/DirectUnboxedValueCodec.java @@ -25,8 +25,9 @@ /** * Exact parent-carrier operations for a semantic leaf which is not transparent to its carrier. * - *

The generated operations must depend only on the codec implementation class and logical - * serialized class. Resolver-local instance state must not select different methods. + *

Instances supplied by direct exact registration are keyed by implementation class and must + * therefore expose the same generated operations. A factory which varies these operations must + * represent that difference in {@code JsonCodecFactory.factoryKey()}. */ @Internal public interface DirectUnboxedValueCodec extends UnboxedValueCodec { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java index a4551c784d..d233a46725 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/TransparentUnboxedValueCodec.java @@ -27,9 +27,9 @@ /** * Exact terminal conversion for a logical value transparent to one underlying JSON type. * - *

The terminal type and every generated operation, including terminal direct operations and - * graph charges, must depend only on the codec implementation class and logical serialized class. - * Resolver-local instance state must not change them. + *

Instances supplied by direct exact registration are keyed by implementation class and must + * therefore expose the same terminal type, generated operations, and graph charges. A factory which + * varies them must represent that difference in {@code JsonCodecFactory.factoryKey()}. */ @Internal public interface TransparentUnboxedValueCodec extends UnboxedValueCodec { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java index 8a5c6c6830..310e2589e1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/GeneratedCodecKey.java @@ -19,11 +19,18 @@ package org.apache.fory.json.codegen; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.IdentityHashMap; import java.util.Objects; import org.apache.fory.annotation.Internal; +import org.apache.fory.reflect.TypeRef; /** Exact, source-independent identity of one generated JSON capability class. */ @Internal @@ -51,33 +58,47 @@ public String classSuffix() { private final Class targetClass; private final Role role; + private final TypeRef rootBinding; private final Object[] keyParts; private final int hash; - private GeneratedCodecKey(Class targetClass, Role role, Object[] keyParts) { + private GeneratedCodecKey( + Class targetClass, Role role, TypeRef rootBinding, Object[] keyParts) { this.targetClass = Objects.requireNonNull(targetClass); this.role = Objects.requireNonNull(role); + this.rootBinding = rootBinding; this.keyParts = keyParts.clone(); // Hosted keys are reconstructed at Native runtime. Class names keep hashes stable across that // boundary; equals still uses Class identity so same-named loader classes remain distinct. hash = - (targetClass.getName().hashCode() * 31 + role.ordinal()) * 31 + valuesHash(this.keyParts); + ((targetClass.getName().hashCode() * 31 + role.ordinal()) * 31 + + rootBindingHash(rootBinding)) + * 31 + + valuesHash(this.keyParts); } - public static GeneratedCodecKey object(Class targetClass, Role role, Object[] keyParts) { + public static GeneratedCodecKey object( + Class targetClass, TypeRef rootBinding, Role role, Object[] keyParts) { if (collectionRole(role)) { throw new IllegalArgumentException("Collection role requires a collection key"); } - return new GeneratedCodecKey(targetClass, role, keyParts); + return new GeneratedCodecKey(targetClass, role, rootBinding, keyParts); } public static GeneratedCodecKey collection( - Class collectionClass, Class elementClass, Role role, boolean stringElements) { + Class collectionClass, + TypeRef rootBinding, + Class elementClass, + Role role, + boolean stringElements) { if (!collectionRole(role)) { throw new IllegalArgumentException("Object role requires an object key"); } return new GeneratedCodecKey( - collectionClass, role, new Object[] {elementClass, stringElements}); + collectionClass, + role, + Objects.requireNonNull(rootBinding), + new Object[] {Objects.requireNonNull(elementClass), stringElements}); } public Class targetClass() { @@ -106,12 +127,9 @@ public Class anchorClass() { if (preferred.getClassLoader() != null) { return preferred; } - if (targetClass.getClassLoader() != null) { - return targetClass; - } - for (Object keyPart : keyParts) { - if (keyPart instanceof Class && ((Class) keyPart).getClassLoader() != null) { - return (Class) keyPart; + for (Class referencedClass : referencedClasses()) { + if (referencedClass.getClassLoader() != null) { + return referencedClass; } } return preferred; @@ -122,6 +140,7 @@ public Class[] referencedClasses() { ArrayList> classes = new ArrayList<>(); IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); addClass(targetClass, classes, seen); + addTypeRefClasses(rootBinding, classes, seen); for (Object keyPart : keyParts) { if (keyPart instanceof Class) { addClass((Class) keyPart, classes, seen); @@ -141,6 +160,7 @@ public boolean equals(Object other) { GeneratedCodecKey that = (GeneratedCodecKey) other; return targetClass == that.targetClass && role == that.role + && Objects.equals(rootBinding, that.rootBinding) && Arrays.equals(keyParts, that.keyParts); } @@ -185,4 +205,73 @@ private static int valuesHash(Object[] values) { } return hash; } + + private static int rootBindingHash(TypeRef typeRef) { + if (typeRef == null) { + return 0; + } + int hash = typeRef.getRawType().getName().hashCode(); + if (typeRef.hasTypeExtMeta() || typeRef.getType() instanceof ParameterizedType) { + for (TypeRef argument : typeRef.getTypeArguments()) { + hash = hash * 31 + rootBindingHash(argument); + } + } + return hash; + } + + private static void addTypeRefClasses( + TypeRef typeRef, ArrayList> classes, IdentityHashMap, Boolean> seen) { + if (typeRef == null) { + return; + } + addTypeClasses(typeRef.getType(), classes, seen); + if (typeRef.hasTypeExtMeta()) { + for (TypeRef argument : typeRef.getTypeArguments()) { + addTypeRefClasses(argument, classes, seen); + } + if (typeRef.isArray()) { + addTypeRefClasses(typeRef.getComponentType(), classes, seen); + } + } + } + + private static void addTypeClasses( + Type type, ArrayList> classes, IdentityHashMap, Boolean> seen) { + if (type == null) { + return; + } + if (type instanceof Class) { + addClass((Class) type, classes, seen); + return; + } + if (type instanceof ParameterizedType) { + ParameterizedType parameterized = (ParameterizedType) type; + addTypeClasses(parameterized.getOwnerType(), classes, seen); + addTypeClasses(parameterized.getRawType(), classes, seen); + for (Type argument : parameterized.getActualTypeArguments()) { + addTypeClasses(argument, classes, seen); + } + return; + } + if (type instanceof GenericArrayType) { + addTypeClasses(((GenericArrayType) type).getGenericComponentType(), classes, seen); + return; + } + if (type instanceof WildcardType) { + WildcardType wildcard = (WildcardType) type; + for (Type bound : wildcard.getUpperBounds()) { + addTypeClasses(bound, classes, seen); + } + for (Type bound : wildcard.getLowerBounds()) { + addTypeClasses(bound, classes, seen); + } + return; + } + if (type instanceof TypeVariable) { + GenericDeclaration declaration = ((TypeVariable) type).getGenericDeclaration(); + if (declaration instanceof Class) { + addClass((Class) declaration, classes, seen); + } + } + } } 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 b6cc0571f6..1c3dde20b2 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -40,6 +40,7 @@ import org.apache.fory.codegen.JaninoUtils.DirectInvocation; import org.apache.fory.collection.ClassValueCache; import org.apache.fory.json.ForyJsonException; +import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.Latin1ReaderCodec; @@ -55,6 +56,7 @@ import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldAccessor; import org.apache.fory.json.meta.JsonFieldInfo; +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.JdkVersion; @@ -86,7 +88,7 @@ public final class JsonCodegen { private static final int HOT_INLINE_LIMIT = 325; private static final int GENERATED_NAME_PREFIX_CODE_POINTS = 32; private static final AtomicLong GENERATED_CLASS_SUFFIX = new AtomicLong(); - private static volatile ClassValueCache generatedClasses = + private static volatile ClassValueCache generatedClassCache = newGeneratedClassCache(); private final CodeGenerator codeGenerator; @@ -212,7 +214,7 @@ private Class compile( GeneratedCodecKey key, Class sourceOwner, CompilerOperation operation) { String generatedPackage = CodeGenerator.getPackage(sourceOwner); PerClassGeneratedCodecCache perClass = - generatedClasses.get(key.anchorClass(), PerClassGeneratedCodecCache::new); + generatedClassCache.get(key.anchorClass(), PerClassGeneratedCodecCache::new); CacheEntry entry = perClass.entries.computeIfAbsent( key, @@ -277,7 +279,7 @@ private ClassLoader[] canonicalLoaders(GeneratedCodecKey key) { /** Releases the hosted strong cache after Native Image analysis freezes the runtime registry. */ @Internal public static void resetGeneratedClassCache() { - generatedClasses = newGeneratedClassCache(); + generatedClassCache = newGeneratedClassCache(); } private static ClassValueCache newGeneratedClassCache() { @@ -1186,6 +1188,88 @@ private boolean canCompileAnyRead(AnyInfo any, boolean creator) { return isGeneratedClassVisible(any.valueRawType()); } + @Internal + public static Class readNestedType(JsonFieldInfo field, JsonTypeResolver resolver) { + if (!field.readsUnboxedValue() + && field.readKind() == JsonFieldKind.OBJECT + && field.readRawType() != Object.class + && resolver.canonicalObjectCodec(field.readTypeInfo()) != null) { + return field.readRawType(); + } + return null; + } + + @Internal + public static boolean usesWriteCodec(JsonFieldInfo field) { + if (field.writesUnboxedValue() && field.writeKind() == JsonFieldKind.ENUM) { + return true; + } + switch (field.writeKind()) { + case ARRAY: + case MAP: + case OBJECT: + return true; + case COLLECTION: + return !writesStringCollectionDirectly(field); + default: + return false; + } + } + + @Internal + public static boolean usesUtf8WriteCodec(JsonFieldInfo field, JsonTypeResolver resolver) { + return usesWriteCodec(field) + || field.writeKind() == JsonFieldKind.COLLECTION + && resolver.exactUtf8WriterCollection(field.writeTypeInfo()) != null; + } + + static boolean writesStringCollectionDirectly(JsonFieldInfo field) { + return field.writeElementRawType() == String.class + && field.writeTypeInfo().stringWriter().getClass() + == CollectionCodec.StringCollectionCodec.class; + } + + @Internal + public static boolean usesReadCodec(JsonFieldInfo field, JsonTypeResolver resolver) { + if (field.readsUnboxedValue()) { + if (field.readDirectUnboxedValueCodec() != null) { + return false; + } + Class rawType = field.readTypeInfo().rawType(); + JsonFieldKind kind = field.readKind(); + if (rawType == String.class && kind == JsonFieldKind.STRING) { + return false; + } + if (rawType.isPrimitive()) { + return !((rawType == boolean.class && kind == JsonFieldKind.BOOLEAN) + || (rawType == byte.class && kind == JsonFieldKind.BYTE) + || (rawType == short.class && kind == JsonFieldKind.SHORT) + || (rawType == int.class && kind == JsonFieldKind.INT) + || (rawType == long.class && kind == JsonFieldKind.LONG) + || (rawType == float.class && kind == JsonFieldKind.FLOAT) + || (rawType == double.class && kind == JsonFieldKind.DOUBLE) + || (rawType == char.class && kind == JsonFieldKind.CHAR)); + } + return true; + } + switch (field.readKind()) { + case ENUM: + case ARRAY: + case COLLECTION: + case MAP: + return true; + case OBJECT: + return !usesReadObjectCodec(field, resolver); + default: + return false; + } + } + + private static boolean usesReadObjectCodec(JsonFieldInfo field, JsonTypeResolver resolver) { + return field.readRawType() != Object.class + && resolver.canonicalObjectCodec(field.readTypeInfo()) != null; + } + Class stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (typeInfo.usesAnnotationCodec()) { return StringWriterCodec.class; @@ -1194,7 +1278,10 @@ Class stringWriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) return StringWriterCodec.class; } Object codec = typeInfo.stringWriter(); - return codecFieldType(typeInfo, codec.getClass(), StringWriterCodec.class); + Class type = codec.getClass(); + return isPublicSourceType(type) && isGeneratedClassVisible(type) + ? type + : StringWriterCodec.class; } Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1208,7 +1295,8 @@ Class utf8WriterFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { return Utf8WriterCodec.class; } Object codec = typeInfo.utf8Writer(); - return codecFieldType(typeInfo, codec.getClass(), Utf8WriterCodec.class); + Class type = codec.getClass(); + return isPublicSourceType(type) && isGeneratedClassVisible(type) ? type : Utf8WriterCodec.class; } Class latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1218,8 +1306,10 @@ Class latin1ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) if (resolver.canonicalObjectCodec(typeInfo) != null) { return Latin1ReaderCodec.class; } - return codecFieldType( - typeInfo, typeInfo.latin1Reader().getClass(), Latin1ReaderCodec.class); + Class type = typeInfo.latin1Reader().getClass(); + return isPublicSourceType(type) && isGeneratedClassVisible(type) + ? type + : Latin1ReaderCodec.class; } Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1229,8 +1319,10 @@ Class utf16ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf16ReaderCodec.class; } - return codecFieldType( - typeInfo, typeInfo.utf16Reader().getClass(), Utf16ReaderCodec.class); + Class type = typeInfo.utf16Reader().getClass(); + return isPublicSourceType(type) && isGeneratedClassVisible(type) + ? type + : Utf16ReaderCodec.class; } Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { @@ -1243,20 +1335,8 @@ Class utf8ReaderFieldType(JsonTypeInfo typeInfo, JsonTypeResolver resolver) { if (resolver.canonicalObjectCodec(typeInfo) != null) { return Utf8ReaderCodec.class; } - return codecFieldType(typeInfo, typeInfo.utf8Reader().getClass(), Utf8ReaderCodec.class); - } - - private Class codecFieldType( - JsonTypeInfo typeInfo, Class codecClass, Class capabilityType) { - // Native-hosted object classes must be reusable by the same exact key at image runtime, where - // registered codec instances are reconstructed independently. Keep ordinary registered codecs - // behind the stable role interface; direct unboxed operations are handled separately. - if (hostedCodegen && typeInfo.registeredCodecClass() != null) { - return capabilityType; - } - return isCodecClassSourceAccessible(codecClass) && isGeneratedClassVisible(codecClass) - ? codecClass - : capabilityType; + Class type = typeInfo.utf8Reader().getClass(); + return isPublicSourceType(type) && isGeneratedClassVisible(type) ? type : Utf8ReaderCodec.class; } @Internal @@ -1272,7 +1352,7 @@ public static boolean storesSelfReader(ObjectCodec owner, JsonTypeResolver re JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); if (unwrapped != null) { for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { - if (route.field() != null && resolver.readNestedType(route.field()) == owner.type()) { + if (route.field() != null && readNestedType(route.field(), resolver) == owner.type()) { return true; } } @@ -1293,7 +1373,7 @@ static boolean storesSelfReader( return false; } for (JsonFieldInfo property : properties) { - if (resolver.readNestedType(property) == type) { + if (readNestedType(property, resolver) == type) { return true; } } @@ -1311,7 +1391,7 @@ private boolean canCompileWrite(JsonFieldInfo property) { if (field != null && !canCompileField(field)) { return false; } - Class rawType = property.writeAccessorType(); + Class rawType = property.writeRawType(); if (rawType != null && !rawType.isPrimitive() && !isGeneratedClassVisible(rawType)) { return false; } @@ -1333,7 +1413,7 @@ private boolean canCompileRead(JsonFieldInfo property) { if (property.readSetter() == null && property.readField() == null) { return false; } - Class rawType = property.readAccessorType(); + Class rawType = property.readRawType(); if (rawType != null && !rawType.isPrimitive() && !isGeneratedClassVisible(rawType)) { return false; } @@ -1452,12 +1532,6 @@ private boolean isDefinitionModuleVisible(Class type) throws ReflectiveOperat .invoke(typeModule, packageName, ownerModule); } - /** Returns whether a codec implementation can be named in generated Java source. */ - @Internal - public static boolean isCodecClassSourceAccessible(Class codecType) { - return isPublicSourceType(codecType); - } - private boolean isVisible(Class type) { if (type.isPrimitive()) { return true; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 980c3c9c4e..1f150c290a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -141,7 +141,7 @@ abstract Expression readEnumField( abstract Reference readerRef(); final Class readNestedType(JsonFieldInfo property) { - return resolver.readNestedType(property); + return JsonCodegen.readNestedType(property, resolver); } String genReaderCode( @@ -173,7 +173,7 @@ String genReaderCode( if (usesReadInfo(properties[i])) { ctx.addField(JsonFieldInfo.class, "rp" + i); } - if (resolver.usesReadCodec(properties[i])) { + if (JsonCodegen.usesReadCodec(properties[i], resolver)) { addValueReaderField(ctx, properties[i], "r" + i); } if (storesReadObjectCodec(type, properties[i])) { @@ -373,7 +373,7 @@ String genUnwrappedReaderCode( if (usesReadInfo(field)) { ctx.addField(JsonFieldInfo.class, "rp" + id); } - if (resolver.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, resolver)) { addValueReaderField(ctx, field, "r" + id); } if (storesReadObjectCodec(type, field)) { @@ -525,7 +525,7 @@ private void addReaderFields(CodegenContext ctx, Class type, JsonFieldInfo[] if (usesReadInfo(properties[i])) { ctx.addField(JsonFieldInfo.class, "rp" + i); } - if (resolver.usesReadCodec(properties[i])) { + if (JsonCodegen.usesReadCodec(properties[i], resolver)) { addValueReaderField(ctx, properties[i], "r" + i); } if (storesReadObjectCodec(type, properties[i])) { @@ -2041,7 +2041,7 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr hashes, new Expression.Invoke(property, "nameHash", TypeRef.of(long.class)).inline(), id)); - if (resolver.usesReadCodec(properties[i])) { + if (JsonCodegen.usesReadCodec(properties[i], resolver)) { if (usesReaderSlot(properties[i].readTypeInfo())) { expressions.add( new Expression.Assign( @@ -2177,7 +2177,7 @@ private void addUnwrappedReaderAssignment( new Expression.Assign( new Reference("this.rp" + id, TypeRef.of(JsonFieldInfo.class)), property)); } - if (resolver.usesReadCodec(field)) { + if (JsonCodegen.usesReadCodec(field, resolver)) { if (usesReaderSlot(field.readTypeInfo())) { expressions.add( new Expression.Assign( @@ -4501,7 +4501,7 @@ final Expression not(Expression expression) { } final boolean usesReadCodec(JsonFieldInfo property) { - return resolver.usesReadCodec(property); + return JsonCodegen.usesReadCodec(property, resolver); } final boolean usesReadInfo(JsonFieldInfo property) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index fe1bf4802d..92a10d76b0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -161,7 +161,7 @@ abstract Expression utf16EnumFieldValue( private boolean usesWriteCodec(JsonFieldInfo property) { return property.writeKind() == JsonFieldKind.COLLECTION ? !writesStringCollectionDirectly(property) - : resolver.usesWriteCodec(property); + : JsonCodegen.usesWriteCodec(property); } static Reference fieldRef(String name, Class type) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java index 0713a37178..b898fc8473 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/StringWriterCodegen.java @@ -84,7 +84,7 @@ int splitMemberThreshold() { @Override boolean writesStringCollectionDirectly(JsonFieldInfo property) { - return JsonTypeResolver.writesStringCollectionDirectly(property); + return JsonCodegen.writesStringCollectionDirectly(property); } @Override diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java index c590f9ec41..7b0b8ba1a4 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/Utf8WriterCodegen.java @@ -101,7 +101,7 @@ int splitMemberThreshold() { @Override boolean writesStringCollectionDirectly(JsonFieldInfo property) { - return JsonTypeResolver.writesStringCollectionDirectly(property) + return JsonCodegen.writesStringCollectionDirectly(property) && resolver.exactUtf8WriterCollection(property.writeTypeInfo()) == null; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java index 061b9df3e4..afc567a143 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonCreatorFieldInfo.java @@ -229,11 +229,6 @@ public DirectUnboxedValueCodec directUnboxedValueCodec() { : null; } - /** Returns whether this argument uses its primitive reader operation directly. */ - public boolean readsDirectPrimitive() { - return typeInfo.kind().matchesPrimitive(rawType); - } - /** Throws the cold failure used by interpreted and generated readers. */ public Object rejectNullRead() { throw new ForyJsonException("JSON creator property " + name + " is not nullable"); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index d85a121f6b..7ecbb8127f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -450,21 +450,6 @@ public Class writeRawType() { return writeRawType; } - /** Returns the erased Java type exposed by the write field or getter. */ - @Internal - public Class writeAccessorType() { - return writeRawType(writeField, writeGetter); - } - - /** Returns whether the resolved write type differs from the Java member declaration. */ - @Internal - public boolean writeTypeDiffersFromDeclaration() { - Type declaredType = writeType(writeField, writeGetter); - return declaredType != null - && writeTypeRef != null - && !declaredType.equals(writeTypeRef.getType()); - } - private static Class writeRawType(Field field, Method getter) { return getter == null ? fieldRawType(field) : getter.getReturnType(); } @@ -514,21 +499,6 @@ public Class readRawType() { return readRawType; } - /** Returns the erased Java type accepted by the read field or setter. */ - @Internal - public Class readAccessorType() { - return readRawType(readField, readSetter); - } - - /** Returns whether the resolved read type differs from the Java member declaration. */ - @Internal - public boolean readTypeDiffersFromDeclaration() { - Type declaredType = readType(readField, readSetter); - return declaredType != null - && readTypeRef != null - && !declaredType.equals(readTypeRef.getType()); - } - private static Class readRawType(Field field, Method setter) { return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java index 32560559f6..d0a9d3f8a9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldKind.java @@ -19,8 +19,6 @@ package org.apache.fory.json.meta; -import org.apache.fory.annotation.Internal; - /** * Semantic field families used to select interpreted and generated JSON operations. * @@ -41,30 +39,5 @@ public enum JsonFieldKind { ARRAY, COLLECTION, MAP, - OBJECT; - - /** Returns whether this kind uses the dedicated operation for {@code type}. */ - @Internal - public boolean matchesPrimitive(Class type) { - switch (this) { - case BOOLEAN: - return type == boolean.class; - case BYTE: - return type == byte.class; - case SHORT: - return type == short.class; - case INT: - return type == int.class; - case LONG: - return type == long.class; - case FLOAT: - return type == float.class; - case DOUBLE: - return type == double.class; - case CHAR: - return type == char.class; - default: - return false; - } - } + OBJECT } 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 2a2f6a26f1..aaaaf47eb8 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 @@ -214,8 +214,9 @@ private static FactoryBinding create(Class target, JsonCodecFactory factory) } List> declared = Preconditions.checkNotNull(factory.handledRuntimeClasses()); ArrayList> handled = new ArrayList<>(declared.size()); + // Registration identity is Class identity; same-named classes from different loaders are + // distinct runtime branches and must remain representable by one factory. IdentityHashMap, Boolean> identities = new IdentityHashMap<>(); - HashSet names = new HashSet<>(); for (Class runtimeType : declared) { Preconditions.checkNotNull(runtimeType); checkRegistrationType(runtimeType); @@ -223,8 +224,7 @@ private static FactoryBinding create(Class target, JsonCodecFactory factory) throw new IllegalArgumentException( runtimeType.getName() + " is not a subtype of " + target.getName()); } - if (identities.put(runtimeType, Boolean.TRUE) != null - || !names.add(runtimeType.getName())) { + if (identities.put(runtimeType, Boolean.TRUE) != null) { throw new IllegalArgumentException( "Duplicate handled runtime class " + runtimeType.getName()); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java index 509ad4a1b3..b1853db494 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/GeneratedCodecKeyBuilder.java @@ -21,65 +21,74 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.IdentityHashMap; import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; -import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.JsonUnwrappedInfo; import org.apache.fory.json.codec.ObjectCodec; import org.apache.fory.json.codec.ObjectCodec.AnyInfo; -import org.apache.fory.json.codec.TransparentUnboxedValueCodec; -import org.apache.fory.json.codec.UnboxedValueCodec; import org.apache.fory.json.codegen.GeneratedCodecKey; import org.apache.fory.json.codegen.GeneratedCodecKey.Role; -import org.apache.fory.json.codegen.JsonCodegen; import org.apache.fory.json.meta.JsonCreatorFieldInfo; +import org.apache.fory.json.meta.JsonCreatorInfo; import org.apache.fory.json.meta.JsonFieldInfo; +import org.apache.fory.reflect.TypeRef; -/** Builds generated-codec keys from configuration and direct codec inputs. */ +/** + * Collects the model-local inputs of one generated JSON class. + * + *

Occurrence scanning is deliberately mechanical and one level deep. Keep generator storage, + * visibility, and source-equivalence decisions in the Writer/Reader generators; conservative key + * splits are safer than duplicating those decisions here. + */ final class GeneratedCodecKeyBuilder { + private enum Part { + TARGET_FACTORY, + TARGET_MIXIN, + UNWRAPPED_FACTORY, + UNWRAPPED_MIXIN, + EXACT_CODEC, + FACTORY, + MIXIN, + CYCLE_SLOT + } + private final JsonTypeResolver resolver; private final ObjectCodec owner; private final JsonTypeResolver.CapabilityKind kind; + private final TypeRef rootBinding; private final ArrayList keyParts; private int occurrence; private GeneratedCodecKeyBuilder( JsonTypeResolver resolver, + JsonTypeInfo typeInfo, ObjectCodec owner, - JsonTypeResolver.CapabilityKind kind, - ArrayList keyParts) { + JsonTypeResolver.CapabilityKind kind) { this.resolver = resolver; this.owner = owner; this.kind = kind; - this.keyParts = keyParts; - } - - static GeneratedCodecKeyBuilder object( - JsonTypeResolver resolver, - JsonTypeInfo typeInfo, - ObjectCodec owner, - JsonTypeResolver.CapabilityKind kind) { + rootBinding = owner.type().getTypeParameters().length == 0 ? null : typeInfo.typeRef(); + keyParts = new ArrayList<>(); JsonSharedRegistry registry = resolver.sharedRegistry(); - ArrayList keyParts = new ArrayList<>(); if (!JsonTypeResolver.readerKind(kind)) { keyParts.add(registry.writeNullFields()); } keyParts.add(registry.propertyDiscoveryEnabled()); keyParts.add(registry.propertyNamingStrategy()); - String factoryKey = typeInfo.objectFactoryKey(); - if (factoryKey != null) { - keyParts.add(factoryKey); - } + addModelInputs(typeInfo, owner.unwrappedInfo(), registry); + addOccurrences(); + } - JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); - addMixins(registry, owner, unwrapped, keyParts); - addUnwrappedFactoryKeys(resolver, unwrapped, keyParts); - return new GeneratedCodecKeyBuilder(resolver, owner, kind, keyParts); + static GeneratedCodecKeyBuilder object( + JsonTypeResolver resolver, + JsonTypeInfo typeInfo, + ObjectCodec owner, + JsonTypeResolver.CapabilityKind kind) { + return new GeneratedCodecKeyBuilder(resolver, typeInfo, owner, kind); } GeneratedCodecKey build() { - return GeneratedCodecKey.object(owner.type(), role(kind), keyParts.toArray()); + return GeneratedCodecKey.object(owner.type(), rootBinding, role(kind), keyParts.toArray()); } static GeneratedCodecKey collection( @@ -89,6 +98,7 @@ static GeneratedCodecKey collection( CodecUtils.rawType(CodecUtils.elementType(typeInfo.type()), Object.class); return GeneratedCodecKey.collection( rawType, + typeInfo.typeRef(), elementType, kind == JsonTypeResolver.CapabilityKind.UTF8_WRITER ? Role.UTF8_COLLECTION_WRITER @@ -96,157 +106,100 @@ static GeneratedCodecKey collection( owner instanceof CollectionCodec.StringCollectionCodec); } - private static void addMixins( - JsonSharedRegistry registry, - ObjectCodec owner, - JsonUnwrappedInfo unwrapped, - ArrayList keyParts) { - IdentityHashMap, Boolean> seen = new IdentityHashMap<>(); - Class ownerMixin = registry.mixinType(owner.type()); - addMixin(ownerMixin, seen, keyParts); - if (unwrapped != null) { - for (JsonUnwrappedInfo.Group group : unwrapped.groups()) { - Class childType = group.childCodec().type(); - addMixin(registry.mixinType(childType), seen, keyParts); + void addCycleSlots(boolean[] slots) { + for (int i = 0; i < slots.length; i++) { + if (slots[i]) { + keyParts.add(Part.CYCLE_SLOT); + keyParts.add(i); } } } - private static void addMixin( - Class mixin, - IdentityHashMap, Boolean> seen, - ArrayList keyParts) { - if (mixin != null && seen.put(mixin, Boolean.TRUE) == null) { - keyParts.add(mixin); - } - } - - private static void addUnwrappedFactoryKeys( - JsonTypeResolver resolver, - JsonUnwrappedInfo unwrapped, - ArrayList keyParts) { + private void addModelInputs( + JsonTypeInfo typeInfo, JsonUnwrappedInfo unwrapped, JsonSharedRegistry registry) { + add(Part.TARGET_FACTORY, typeInfo.factoryKey()); + add(Part.TARGET_MIXIN, registry.mixinType(owner.type())); if (unwrapped == null) { return; } JsonUnwrappedInfo.Group[] groups = unwrapped.groups(); for (int i = 0; i < groups.length; i++) { - String factoryKey = resolver.objectFactoryKey(groups[i].childCodec()); - if (factoryKey != null) { - keyParts.add(i); - keyParts.add(factoryKey); - } + ObjectCodec child = groups[i].childCodec(); + add(Part.UNWRAPPED_FACTORY, i, resolver.factoryKey(child)); + add(Part.UNWRAPPED_MIXIN, i, registry.mixinType(child.type())); } } - void addAny(boolean storesCapability) { + private void addOccurrences() { AnyInfo any = owner.anyInfo(); - if (any == null || !storesCapability) { + if (!JsonTypeResolver.readerKind(kind)) { + JsonFieldInfo[] fields = + owner.unwrappedInfo() == null ? owner.writeFields() : owner.unwrappedInfo().writeFields(); + for (JsonFieldInfo field : fields) { + addRegistration(field.writeTypeInfo()); + } + if (any != null && (any.writeField() != null || any.writeGetter() != null)) { + addRegistration(any.valueTypeInfo()); + } return; } - boolean slot = - JsonTypeResolver.readerKind(kind) - ? resolver.usesReaderSlot(owner, any.valueTypeInfo()) - : resolver.usesWriterSlot(owner, any.valueTypeInfo()); - if (slot) { - addSlot(keyParts, occurrence); - } - } - void addField(JsonFieldInfo field, boolean storesCapability) { - boolean reader = JsonTypeResolver.readerKind(kind); - JsonTypeInfo typeInfo = reader ? field.readTypeInfo() : field.writeTypeInfo(); - UnboxedValueCodec unboxed = - reader ? field.readUnboxedValueCodec() : field.writeUnboxedValueCodec(); - boolean typeDiffers = - reader ? field.readTypeDiffersFromDeclaration() : field.writeTypeDiffersFromDeclaration(); - Class codecClass = - unboxed != null - ? unboxed.getClass() - : storesCapability && !typeDiffers - ? keyCodecClass(resolver, owner, typeInfo, kind) - : null; - if (codecClass != null) { - Class logicalClass = - CodecUtils.rawType(reader ? field.readType() : field.writeType(), Object.class); - addCodec(keyParts, occurrence, logicalClass, codecClass); + JsonCreatorInfo creator = owner.creatorInfo(); + if (creator == null) { + for (JsonFieldInfo field : owner.readFields()) { + addRegistration(field.readTypeInfo()); + } + } else { + for (JsonCreatorFieldInfo field : creator.fields()) { + addRegistration(field.typeInfo()); + } } - addDirectTerminal(keyParts, occurrence, unboxed, typeInfo); - if (storesCapability && usesSlot(resolver, owner, typeInfo, kind)) { - addSlot(keyParts, occurrence); + JsonUnwrappedInfo unwrapped = owner.unwrappedInfo(); + if (unwrapped != null) { + for (JsonUnwrappedInfo.ReadRoute route : unwrapped.readRoutes()) { + addRegistration( + route.field() == null ? route.creatorField().typeInfo() : route.field().readTypeInfo()); + } + } + if (any != null && (any.readField() != null || any.readSetter() != null)) { + addRegistration(any.valueTypeInfo()); } - occurrence++; } - void addCreatorField(JsonCreatorFieldInfo field, boolean storesCapability) { - UnboxedValueCodec unboxed = field.unboxedValueCodec(); - if (unboxed != null) { - addCodec(keyParts, occurrence, field.typeRef().getRawType(), unboxed.getClass()); + private void addRegistration(JsonTypeInfo typeInfo) { + JsonSharedRegistry registry = resolver.sharedRegistry(); + String factoryKey = typeInfo.factoryKey(); + if (factoryKey != null) { + keyParts.add(Part.FACTORY); + keyParts.add(occurrence); + keyParts.add(factoryKey); + } else if (typeInfo.exactCodecClass() != null) { + keyParts.add(Part.EXACT_CODEC); + keyParts.add(occurrence); + keyParts.add(typeInfo.exactCodecClass()); } - addDirectTerminal(keyParts, occurrence, unboxed, field.typeInfo()); - if (storesCapability && usesSlot(resolver, owner, field.typeInfo(), kind)) { - addSlot(keyParts, occurrence); + Class mixinType = registry.mixinType(typeInfo.rawType()); + if (mixinType != null) { + keyParts.add(Part.MIXIN); + keyParts.add(occurrence); + keyParts.add(mixinType); } occurrence++; } - private static void addDirectTerminal( - ArrayList keyParts, int occurrence, UnboxedValueCodec outer, JsonTypeInfo typeInfo) { - if (!(outer instanceof TransparentUnboxedValueCodec)) { - return; - } - UnboxedValueCodec terminal = typeInfo.unboxedValueCodec(); - if (terminal instanceof DirectUnboxedValueCodec) { - addCodec(keyParts, occurrence, typeInfo.rawType(), terminal.getClass()); + private void add(Part part, Object value) { + if (value != null) { + keyParts.add(part); + keyParts.add(value); } } - private static void addCodec( - ArrayList keyParts, int occurrence, Class logicalClass, Class codecClass) { - keyParts.add(occurrence); - keyParts.add(logicalClass); - keyParts.add(codecClass); - } - - private static void addSlot(ArrayList keyParts, int occurrence) { - keyParts.add(occurrence); - } - - private static boolean usesSlot( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonTypeInfo typeInfo, - JsonTypeResolver.CapabilityKind kind) { - return JsonTypeResolver.readerKind(kind) - ? resolver.usesReaderSlot(owner, typeInfo) - : resolver.usesWriterSlot(owner, typeInfo); - } - - private static Class keyCodecClass( - JsonTypeResolver resolver, - ObjectCodec owner, - JsonTypeInfo typeInfo, - JsonTypeResolver.CapabilityKind kind) { - if (resolver.canonicalObjectOwner(typeInfo) != null) { - return null; - } - if ((kind == JsonTypeResolver.CapabilityKind.UTF8_WRITER - && resolver.exactUtf8WriterCollection(typeInfo) != null) - || (kind == JsonTypeResolver.CapabilityKind.UTF8_READER - && resolver.exactUtf8Collection(typeInfo) != null)) { - return null; - } - JsonSharedRegistry registry = resolver.sharedRegistry(); - // Hosted classes always store ordinary registered codecs through the role interface. Native - // runtime must reconstruct the same key from stable metadata without replaying hosted loader - // or module visibility. - if (registry.hostedCodegen() || registry.nativeGeneratedClasses()) { - return null; - } - Class codecClass = typeInfo.registeredCodecClass(); - if (codecClass == null) { - return null; + private void add(Part part, int index, Object value) { + if (value != null) { + keyParts.add(part); + keyParts.add(index); + keyParts.add(value); } - return JsonCodegen.isCodecClassSourceAccessible(codecClass) ? codecClass : null; } private static Role role(JsonTypeResolver.CapabilityKind kind) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java index 8988621bff..0249b60640 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistry.java @@ -115,6 +115,8 @@ static void mergeSourceCodecs( } private static void snapshotCompanions() { + // Companion keys retain TypeRef and Mixin identity hashes. Freeze them as entries so Native + // runtime lookup uses equality instead of a hosted HashMap bucket computed before image start. companionEntries = new CompanionEntry[pendingCompanions.size()]; int index = 0; for (Map.Entry> entry : pendingCompanions.entrySet()) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java index 1792c9bd30..8a5ab7129d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/resolver/JsonTypeInfo.java @@ -58,9 +58,8 @@ public final class JsonTypeInfo { private final boolean rejectsNull; private final boolean transparentNull; private final UnboxedValueCodec unboxedValueCodec; - private final String objectFactoryKey; - // Only exact registry or factory selection can vary independently of target/Mixin metadata. - private final Class registeredCodecClass; + private final String factoryKey; + private final Class exactCodecClass; private StringWriterCodec stringWriter; private Utf8WriterCodec utf8Writer; private Latin1ReaderCodec latin1Reader; @@ -85,8 +84,8 @@ public final class JsonTypeInfo { JsonFieldKind kind, JsonValueCodec codec, boolean annotationCodec, - String objectFactoryKey, - Class registeredCodecClass) { + String factoryKey, + Class exactCodecClass) { this.typeRef = typeRef; this.rawType = typeRef.getRawType(); this.kind = kind; @@ -97,8 +96,8 @@ public final class JsonTypeInfo { metadata != null && !metadata.nullable() && !metadata.nullableWrapper() && !transparentNull; unboxedValueCodec = codec instanceof UnboxedValueCodec ? (UnboxedValueCodec) codec : null; this.annotationCodec = annotationCodec; - this.objectFactoryKey = objectFactoryKey; - this.registeredCodecClass = registeredCodecClass; + this.factoryKey = factoryKey; + this.exactCodecClass = exactCodecClass; stringWriter = codec; utf8Writer = codec; latin1Reader = codec; @@ -199,13 +198,11 @@ public boolean usesAnnotationCodec() { return annotationCodec; } - String objectFactoryKey() { - return objectFactoryKey; + String factoryKey() { + return factoryKey; } - /** Returns the exact application-registered codec implementation, or {@code null}. */ - @Internal - public Class registeredCodecClass() { - return registeredCodecClass; + Class exactCodecClass() { + return exactCodecClass; } } 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 eb61f801d3..d9e2c55e19 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 @@ -52,7 +52,6 @@ import org.apache.fory.json.codec.CodecUtils; import org.apache.fory.json.codec.CollectionCodec; import org.apache.fory.json.codec.CompositeJsonCodec; -import org.apache.fory.json.codec.DirectUnboxedValueCodec; import org.apache.fory.json.codec.GeneratedJsonCodec; import org.apache.fory.json.codec.JsonObjectModel; import org.apache.fory.json.codec.JsonSubTypesInfo; @@ -65,7 +64,6 @@ import org.apache.fory.json.codec.ObjectCodec.AnyInfo; import org.apache.fory.json.codec.ScalarCodecs; import org.apache.fory.json.codec.StringWriterCodec; -import org.apache.fory.json.codec.TransparentUnboxedValueCodec; import org.apache.fory.json.codec.UnboxedValueCodec; import org.apache.fory.json.codec.Utf16ReaderCodec; import org.apache.fory.json.codec.Utf8ReaderCodec; @@ -237,7 +235,7 @@ public ObjectCodec canonicalObjectCodec(JsonTypeInfo typeInfo) { } } - ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { + private ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { ObjectCodec owner = objectCodecs.get(metadataKey(typeInfo)); if (owner != null && canonicalObjectTypeInfos.get(owner) == typeInfo) { return owner; @@ -250,9 +248,9 @@ ObjectCodec canonicalObjectOwner(JsonTypeInfo typeInfo) { return null; } - String objectFactoryKey(ObjectCodec owner) { + String factoryKey(ObjectCodec owner) { JsonTypeInfo typeInfo = canonicalObjectTypeInfos.get(owner); - return typeInfo == null ? null : typeInfo.objectFactoryKey(); + return typeInfo == null ? null : typeInfo.factoryKey(); } /** Returns an exact declared ArrayList-backed UTF-8 collection owner, or {@code null}. */ @@ -1731,7 +1729,7 @@ private static JsonTypeInfo[] unwrappedReadTypeInfos(ObjectCodec owner) { return children; } - boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { + private boolean storesAnyCodec(ObjectCodec owner, AnyInfo any) { return canonicalObjectCodec(any.valueTypeInfo()) == null || any.valueRawType() != owner.type(); } @@ -1743,136 +1741,26 @@ enum CapabilityKind { UTF8_READER } - /** Returns the nested object type inlined by generated readers, or {@code null}. */ - @Internal - public Class readNestedType(JsonFieldInfo field) { - if (field.readAccessorType() == field.readRawType() - && !field.readsUnboxedValue() - && field.readKind() == JsonFieldKind.OBJECT - && field.readRawType() != Object.class - && canonicalObjectCodec(field.readTypeInfo()) != null) { - return field.readRawType(); - } - return null; - } - - /** Returns whether a generated string writer stores a field codec. */ - @Internal - public boolean usesWriteCodec(JsonFieldInfo field) { - if (field.writesUnboxedValue() && field.writeKind() == JsonFieldKind.ENUM) { - return true; - } - // A resolved logical type belongs to the codec instance, not the generated class. Keep the - // class on the erased accessor type and inject the selected capability through its base API. - if (field.writeTypeDiffersFromDeclaration() && !field.writesRawString()) { - return true; - } - switch (field.writeKind()) { - case ARRAY: - case MAP: - case OBJECT: - return true; - case COLLECTION: - return !writesStringCollectionDirectly(field); - default: - return false; - } - } - - /** Returns whether a generated UTF-8 writer invokes a resolved field capability. */ - @Internal - public boolean usesUtf8WriteCodec(JsonFieldInfo field) { - return usesWriteCodec(field) - || field.writeKind() == JsonFieldKind.COLLECTION - && exactUtf8WriterCollection(field.writeTypeInfo()) != null; - } - - /** Returns whether a generated writer stores this field's resolved capability. */ - @Internal - public boolean storesWriteCapability( + private boolean storesWriteCapability( ObjectCodec owner, JsonFieldInfo field, boolean utf8Writer) { - boolean usesCodec = utf8Writer ? usesUtf8WriteCodec(field) : usesWriteCodec(field); + boolean usesCodec = + utf8Writer + ? JsonCodegen.usesUtf8WriteCodec(field, this) + : JsonCodegen.usesWriteCodec(field); return usesCodec - && (canonicalObjectCodec(field.writeTypeInfo()) == null - || field.writeTypeInfo().rawType() != owner.type()); - } - - /** Returns whether a generated reader stores a field codec. */ - @Internal - public boolean usesReadCodec(JsonFieldInfo field) { - if (field.readsUnboxedValue()) { - if (field.readDirectUnboxedValueCodec() != null) { - return false; - } - Class rawType = field.readTypeInfo().rawType(); - JsonFieldKind kind = field.readKind(); - if (rawType == String.class && kind == JsonFieldKind.STRING) { - return false; - } - if (rawType.isPrimitive()) { - return !kind.matchesPrimitive(rawType); - } - return true; - } - // The matching writer rule keeps resolved logical types out of generated-class identity. - if (field.readTypeDiffersFromDeclaration()) { - return true; - } - switch (field.readKind()) { - case ENUM: - case ARRAY: - case COLLECTION: - case MAP: - return true; - case OBJECT: - return !(field.readRawType() != Object.class - && canonicalObjectCodec(field.readTypeInfo()) != null); - default: - return false; - } + && (field.writeRawType() != owner.type() + || canonicalObjectOwner(field.writeTypeInfo()) == null); } - /** Returns whether a generated reader stores a resolved capability for this field. */ - @Internal - public boolean storesReadCapability(ObjectCodec owner, JsonFieldInfo field) { - if (usesReadCodec(field)) { + private boolean storesReadCapability(ObjectCodec owner, JsonFieldInfo field) { + if (JsonCodegen.usesReadCodec(field, this)) { return true; } - Class nestedType = readNestedType(field); + Class nestedType = JsonCodegen.readNestedType(field, this); return nestedType != null && nestedType != owner.type(); } - /** Returns whether a generated reader stores this creator argument's capability. */ - @Internal - public boolean storesReadCapability(JsonCreatorFieldInfo field) { - UnboxedValueCodec unboxed = field.unboxedValueCodec(); - if (unboxed instanceof DirectUnboxedValueCodec) { - return false; - } - if (!(unboxed instanceof TransparentUnboxedValueCodec)) { - return !field.readsDirectPrimitive(); - } - JsonTypeInfo terminal = ((TransparentUnboxedValueCodec) unboxed).valueTypeInfo(); - if (terminal.unboxedValueCodec() instanceof DirectUnboxedValueCodec) { - return false; - } - if (terminal.rawType() == String.class && terminal.kind() == JsonFieldKind.STRING) { - return false; - } - return !terminal.kind().matchesPrimitive(terminal.rawType()); - } - - /** Returns whether the standard string collection writer is fully inlined. */ - @Internal - public static boolean writesStringCollectionDirectly(JsonFieldInfo field) { - return !field.writeTypeDiffersFromDeclaration() - && field.writeElementRawType() == String.class - && field.writeTypeInfo().stringWriter().getClass() - == CollectionCodec.StringCollectionCodec.class; - } - - private ArrayList capabilityChildren( - ObjectCodec owner, CapabilityKind kind, GeneratedCodecKeyBuilder keyBuilder) { + private ArrayList capabilityChildren(ObjectCodec owner, CapabilityKind kind) { ArrayList children = new ArrayList<>(); AnyInfo any = owner.anyInfo(); boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; @@ -1883,9 +1771,6 @@ private ArrayList capabilityChildren( JsonFieldInfo field = fields[i]; boolean storesCapability = storesWriteCapability(owner, field, kind == CapabilityKind.UTF8_WRITER); - if (keyBuilder != null) { - keyBuilder.addField(field, storesCapability); - } if (storesCapability) { children.add(field.writeTypeInfo()); } @@ -1894,9 +1779,6 @@ private ArrayList capabilityChildren( any != null && (any.writeField() != null || any.writeGetter() != null) && storesAnyCodec(owner, any); - if (keyBuilder != null) { - keyBuilder.addAny(storesAny); - } if (storesAny) { children.add(any.valueTypeInfo()); } @@ -1906,15 +1788,12 @@ private ArrayList capabilityChildren( if (creator == null) { JsonFieldInfo[] fields = owner.readFields(); for (int i = 0; i < fields.length; i++) { - addReadDependency(children, owner, fields[i], keyBuilder); + addReadDependency(children, owner, fields[i]); } } else { JsonCreatorFieldInfo[] fields = creator.fields(); for (int i = 0; i < fields.length; i++) { JsonCreatorFieldInfo field = fields[i]; - if (keyBuilder != null) { - keyBuilder.addCreatorField(field, storesReadCapability(field)); - } children.add(field.typeInfo()); } } @@ -1923,13 +1802,9 @@ private ArrayList capabilityChildren( for (int i = 0; i < routes.length; i++) { JsonUnwrappedInfo.ReadRoute route = routes[i]; if (route.field() == null) { - if (keyBuilder != null) { - keyBuilder.addCreatorField( - route.creatorField(), storesReadCapability(route.creatorField())); - } children.add(route.creatorField().typeInfo()); } else { - addReadDependency(children, owner, route.field(), keyBuilder); + addReadDependency(children, owner, route.field()); } } } @@ -1937,29 +1812,15 @@ private ArrayList capabilityChildren( any != null && (any.readField() != null || any.readSetter() != null) && storesAnyCodec(owner, any); - if (keyBuilder != null) { - keyBuilder.addAny(storesAny); - } if (storesAny) { children.add(any.valueTypeInfo()); } return children; } - private ArrayList capabilityChildren( - ObjectCodec owner, CapabilityKind kind) { - return capabilityChildren(owner, kind, null); - } - private void addReadDependency( - ArrayList children, - ObjectCodec owner, - JsonFieldInfo field, - GeneratedCodecKeyBuilder keyBuilder) { + ArrayList children, ObjectCodec owner, JsonFieldInfo field) { boolean storesCapability = storesReadCapability(owner, field); - if (keyBuilder != null) { - keyBuilder.addField(field, storesCapability); - } if (storesCapability) { children.add(field.readTypeInfo()); } @@ -2072,7 +1933,7 @@ private boolean reachesReader( return false; } - static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { + private static Object currentCapability(JsonTypeInfo typeInfo, CapabilityKind kind) { switch (kind) { case STRING_WRITER: return typeInfo.stringWriter(); @@ -2447,7 +2308,14 @@ private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo, boolea } GeneratedCodecKeyBuilder keyBuilder = GeneratedCodecKeyBuilder.object(JsonTypeResolver.this, typeInfo, owner, kind); - ArrayList children = capabilityChildren(owner, kind, keyBuilder); + ArrayList children = capabilityChildren(owner, kind); + boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; + boolean[] childSlots = new boolean[children.size()]; + for (int i = 0; i < children.size(); i++) { + JsonTypeInfo child = children.get(i); + childSlots[i] = writer ? usesWriterSlot(owner, child) : usesReaderSlot(owner, child); + } + keyBuilder.addCycleSlots(childSlots); GeneratedCodecKey generatedKey = keyBuilder.build(); Class generatedClass = sharedRegistry.nativeGeneratedClass(generatedKey); if (sharedRegistry.nativeGeneratedClasses()) { @@ -2464,10 +2332,7 @@ private boolean addObject(ObjectCodec rawOwner, JsonTypeInfo typeInfo, boolea node.generatedClass = generatedClass; nodes.put(typeInfo, node); for (int i = 0; i < children.size(); i++) { - JsonTypeInfo child = children.get(i); - boolean writer = kind == CapabilityKind.STRING_WRITER || kind == CapabilityKind.UTF8_WRITER; - boolean childSlot = writer ? usesWriterSlot(owner, child) : usesReaderSlot(owner, child); - if (!addDependency(child, childSlot)) { + if (!addDependency(children.get(i), childSlots[i])) { return false; } } @@ -2708,7 +2573,7 @@ private JsonTypeInfo buildTypeInfo( JsonTypeInfo typeInfo = resolved.factoryKey() == null ? newTypeInfo(typeRef, codec) - : newRegisteredTypeInfo(typeRef, codec, null); + : newRegisteredTypeInfo(typeRef, codec, resolved.factoryKey()); publishTypeInfo(key, typeInfo); registerTypeInfoOwner(typeInfo, codec); resolveCodecTypes(codec, typeRef); @@ -2794,20 +2659,20 @@ private JsonTypeInfo newTypeInfo(TypeRef typeRef, JsonValueCodec codec) { return new JsonTypeInfo(typeRef, sharedRegistry.kind(typeRef.getRawType()), bindCodec(codec)); } + private JsonTypeInfo newTypeInfo( + TypeRef typeRef, JsonFieldKind kind, JsonValueCodec codec, boolean annotationCodec) { + return new JsonTypeInfo(typeRef, kind, bindCodec(codec), annotationCodec); + } + private JsonTypeInfo newRegisteredTypeInfo( - TypeRef typeRef, JsonValueCodec codec, String objectFactoryKey) { + TypeRef typeRef, JsonValueCodec codec, String factoryKey) { return new JsonTypeInfo( typeRef, sharedRegistry.kind(typeRef.getRawType()), bindCodec(codec), false, - objectFactoryKey, - codec instanceof ObjectCodec ? null : codec.getClass()); - } - - private JsonTypeInfo newTypeInfo( - TypeRef typeRef, JsonFieldKind kind, JsonValueCodec codec, boolean annotationCodec) { - return new JsonTypeInfo(typeRef, kind, bindCodec(codec), annotationCodec); + factoryKey, + factoryKey == null && !(codec instanceof ObjectCodec) ? codec.getClass() : null); } private void registerTypeInfoOwner(JsonTypeInfo typeInfo, JsonValueCodec initialCodec) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java index 4f756fec23..65e923db9f 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAsyncCompilationTest.java @@ -637,7 +637,7 @@ public void semanticBindingsRemainOwners() throws Exception { controlled.json.fromJson( "{\"value\":\"raw\"}".getBytes(StandardCharsets.UTF_8), GenericAsyncBox.class); - assertEquals(controlled.executor.submittedTasks(), 5); + assertEquals(controlled.executor.submittedTasks(), 10); resolver.lockJIT(); try { JsonTypeInfo raw = resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class); @@ -651,7 +651,7 @@ public void semanticBindingsRemainOwners() throws Exception { Object rawReader = resolver.getTypeInfo(GenericAsyncBox.class, GenericAsyncBox.class).utf8Reader(); assertNotSame(rawReader, parameterized.utf8Reader()); - assertSame(rawReader.getClass(), parameterized.utf8Reader().getClass()); + assertNotSame(rawReader.getClass(), parameterized.utf8Reader().getClass()); JsonValueCodec codec = nullCodec(); CodecRegistry codecs = new CodecRegistry(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java index 0975ee72d2..2d8929cdfd 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCodecRegistrationTest.java @@ -20,7 +20,9 @@ package org.apache.fory.json; import static org.apache.fory.json.JsonTestSupport.nullCodec; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotSame; import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; @@ -40,6 +42,7 @@ import java.time.YearMonth; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -127,6 +130,34 @@ public List> handledRuntimeClasses() { assertFalse(registry.contains(Object.class)); } + @Test + public void sameNamedHandledClassesAllowed() throws Exception { + Class first = JsonTestSupport.shadowClass(ApplicationValue.class); + Class second = JsonTestSupport.shadowClass(ApplicationValue.class); + assertNotSame(first, second); + + CodecRegistry registry = new CodecRegistry(); + JsonCodecFactory factory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return null; + } + + @Override + public List> handledRuntimeClasses() { + return Arrays.asList(first, second); + } + }; + registry.registerFactory(Object.class, factory); + + List> handled = registry.getFactory(Object.class).handledRuntimeClasses(); + assertEquals(handled.size(), 2); + assertTrue(handled.contains(first)); + assertTrue(handled.contains(second)); + } + @Test public void moduleExactDedicatedTypeRejected() { assertThrows( diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java index e0e88aaf4d..6d2f16f845 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonCreatorTest.java @@ -271,13 +271,12 @@ public void primitiveNullRejected() { public void creatorPrimitiveNullRejected() { ForyJson json = newJson(); assertThrows( - ForyJsonException.class, - () -> json.fromJson("{\"id\":null}", CustomPrimitiveCreator.class)); + ForyJsonException.class, () -> json.fromJson("{\"id\":null}", PrimitiveCreator.class)); assertThrows( ForyJsonException.class, () -> json.fromJson( - "{\"id\":null}".getBytes(StandardCharsets.UTF_8), CustomPrimitiveCreator.class)); + "{\"id\":null}".getBytes(StandardCharsets.UTF_8), PrimitiveCreator.class)); } @Test @@ -404,11 +403,11 @@ public PrefixCreator(int abcOne, int abcTwo) { } } - public static final class CustomPrimitiveCreator { + public static final class PrimitiveCreator { public final int id; @JsonCreator({"id"}) - public CustomPrimitiveCreator(int id) { + public PrimitiveCreator(int id) { this.id = id; } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java index 1cbaad1ac1..9caf6b1bd0 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCapabilityKeyTest.java @@ -26,9 +26,7 @@ import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; @@ -50,6 +48,8 @@ import javax.tools.ToolProvider; 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.JsonMixin; import org.apache.fory.json.annotation.JsonSubTypes; import org.apache.fory.json.annotation.JsonType; import org.apache.fory.json.annotation.JsonUnwrapped; @@ -97,28 +97,28 @@ public void outerNullabilityReusesObjectClasses() { } @Test - public void outerNullabilityReusesCollectionClasses() { + public void collectionBindingVersionsClasses() { JsonTypeResolver resolver = resolver(); TypeRef element = TypeRef.of(String.class); JsonTypeInfo raw = resolver.getTypeInfo(listType(null, element)); JsonTypeInfo nonNull = resolver.getTypeInfo(listType(ordinary(false), element)); JsonTypeInfo nullable = resolver.getTypeInfo(listType(ordinary(true), element)); - assertSame(raw.utf8Writer().getClass(), nonNull.utf8Writer().getClass()); - assertSame(raw.utf8Writer().getClass(), nullable.utf8Writer().getClass()); - assertSame(raw.utf8Reader().getClass(), nonNull.utf8Reader().getClass()); - assertSame(raw.utf8Reader().getClass(), nullable.utf8Reader().getClass()); + assertNotSame(raw.utf8Writer().getClass(), nonNull.utf8Writer().getClass()); + assertNotSame(raw.utf8Writer().getClass(), nullable.utf8Writer().getClass()); + assertNotSame(raw.utf8Reader().getClass(), nonNull.utf8Reader().getClass()); + assertNotSame(raw.utf8Reader().getClass(), nullable.utf8Reader().getClass()); JsonTypeInfo nonNullElement = resolver.getTypeInfo(listType(null, TypeRef.of(String.class, ordinary(false)))); JsonTypeInfo nullableElement = resolver.getTypeInfo(listType(null, TypeRef.of(String.class, ordinary(true)))); - assertSame(nonNullElement.utf8Writer().getClass(), nullableElement.utf8Writer().getClass()); - assertSame(nonNullElement.utf8Reader().getClass(), nullableElement.utf8Reader().getClass()); + assertNotSame(nonNullElement.utf8Writer().getClass(), nullableElement.utf8Writer().getClass()); + assertNotSame(nonNullElement.utf8Reader().getClass(), nullableElement.utf8Reader().getClass()); } @Test - public void injectedMetadataReusesParentClass() { + public void nestedBindingVersionsParentClass() { JsonTypeResolver resolver = resolver(); TypeRef nonNullArray = TypeRef.of( @@ -127,21 +127,20 @@ public void injectedMetadataReusesParentClass() { TypeRef.of(String[].class, ordinary(false), null, TypeRef.of(String.class, ordinary(true))); JsonTypeInfo nonNull = resolver.getTypeInfo(boxType(nonNullArray)); JsonTypeInfo nullable = resolver.getTypeInfo(boxType(nullableArray)); - assertSame(nonNull.utf8Writer().getClass(), nullable.utf8Writer().getClass()); - assertSame(nonNull.utf8Reader().getClass(), nullable.utf8Reader().getClass()); + assertDifferentObjectClasses(nonNull, nullable); } @Test - public void genericScalarInputsReuseClass() { + public void genericBindingsVersionClass() { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); JsonTypeInfo strings = resolver.getTypeInfo(new TypeRef>() {}); JsonTypeInfo integers = resolver.getTypeInfo(new TypeRef>() {}); JsonTypeInfo bytes = resolver.getTypeInfo(new TypeRef>() {}); JsonTypeInfo enums = resolver.getTypeInfo(new TypeRef>() {}); - assertObjectClasses(strings, integers); - assertObjectClasses(strings, bytes); - assertObjectClasses(strings, enums); + assertDifferentObjectClasses(strings, integers); + assertDifferentObjectClasses(strings, bytes); + assertDifferentObjectClasses(strings, enums); GenericModel stringValue = new GenericModel<>(); stringValue.value = "value"; @@ -162,78 +161,20 @@ public void genericScalarInputsReuseClass() { assertEquals(json.fromJson(json.toJson(stringValue, stringType), stringType).value, "value"); assertEquals(json.fromJson(json.toJson(intValue, intType), intType).value, 7); assertEquals(json.fromJson(json.toJson(byteValue, byteType), byteType).value, (byte) 3); - assertSame( - json.fromJson(json.toJson(enumValue, enumType), enumType).value, FirstEnum.VALUE); + assertSame(json.fromJson(json.toJson(enumValue, enumType), enumType).value, FirstEnum.VALUE); } @Test - public void equivalentGenericInputsReuseClass() { - ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); - JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); - JsonTypeInfo first = resolver.getTypeInfo(new TypeRef>() {}); - JsonTypeInfo second = resolver.getTypeInfo(new TypeRef>() {}); - assertObjectClasses(first, second); - - GenericModel firstValue = new GenericModel<>(); - firstValue.value = new Child(); - firstValue.value.value = "first"; - GenericModel secondValue = new GenericModel<>(); - secondValue.value = new OtherChild(); - secondValue.value.value = 2; - TypeRef> firstType = new TypeRef>() {}; - TypeRef> secondType = new TypeRef>() {}; - assertEquals(json.toJson(firstValue, firstType), "{\"value\":{\"value\":\"first\"}}"); - assertEquals(json.toJson(secondValue, secondType), "{\"value\":{\"value\":2}}"); - assertEquals(json.fromJson(json.toJson(firstValue, firstType), firstType).value.value, "first"); - assertEquals(json.fromJson(json.toJson(secondValue, secondType), secondType).value.value, 2); - } - - @Test - public void genericCollectionInputsReuseClass() { - ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); - JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); - TypeRef> stringType = new TypeRef>() {}; - TypeRef> childType = new TypeRef>() {}; - assertObjectClasses(resolver.getTypeInfo(stringType), resolver.getTypeInfo(childType)); - - GenericCollection strings = new GenericCollection<>(); - strings.values = Collections.singletonList("value"); - GenericCollection children = new GenericCollection<>(); - Child child = new Child(); - child.value = "child"; - children.values = Collections.singletonList(child); - assertEquals( - json.fromJson(json.toJson(strings, stringType), stringType).values.get(0), "value"); - assertEquals( - json.fromJson(json.toJson(children, childType), childType).values.get(0).value, "child"); - } - - @Test - public void genericEnumInputsReuseClass() { - ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); - JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); - JsonTypeInfo first = resolver.getTypeInfo(new TypeRef>() {}); - JsonTypeInfo second = resolver.getTypeInfo(new TypeRef>() {}); - assertObjectClasses(first, second); - - GenericModel firstValue = new GenericModel<>(); - firstValue.value = FirstEnum.VALUE; - GenericModel secondValue = new GenericModel<>(); - secondValue.value = SecondEnum.VALUE; - TypeRef> firstType = new TypeRef>() {}; - TypeRef> secondType = new TypeRef>() {}; - assertSame(json.fromJson(json.toJson(firstValue, firstType), firstType).value, FirstEnum.VALUE); - assertSame( - json.fromJson(json.toJson(secondValue, secondType), secondType).value, SecondEnum.VALUE); - } + public void exactGenericBindingReusesClass() { + TypeRef> type = new TypeRef>() {}; + ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); + ForyJson second = ForyJson.builder().withAsyncCompilation(false).build(); + JsonTypeInfo firstType = JsonTestSupport.currentTypeResolver(first).getTypeInfo(type); + JsonTypeInfo secondType = + JsonTestSupport.currentTypeResolver(second) + .getTypeInfo(new TypeRef>() {}); - @Test - public void genericRegisteredCodecUsesBaseCapability() { - ForyJson json = parentJson(new ChildCodecA()); - JsonTypeResolver resolver = JsonTestSupport.currentTypeResolver(json); - JsonTypeInfo first = resolver.getTypeInfo(new TypeRef>() {}); - JsonTypeInfo second = resolver.getTypeInfo(new TypeRef>() {}); - assertObjectClasses(first, second); + assertObjectClasses(firstType, secondType); } @Test @@ -303,11 +244,39 @@ public void directCodecClassVersionsParent() { } @Test - public void hiddenCodecClassesReuseParent() { + public void hiddenCodecClassesVersionParent() { JsonTypeInfo first = parentType(new HiddenChildCodecA()); JsonTypeInfo second = parentType(new HiddenChildCodecB()); - assertObjectClasses(first, second); + assertDifferentObjectClasses(first, second); + } + + @Test + public void directFactoryVersionsParent() { + assertDifferentObjectClasses( + parentFactoryType("direct-a", new ChildCodecA(), true), + parentFactoryType("direct-b", new ChildCodecB(), true)); + } + + @Test + public void moduleFactoryVersionsParent() { + assertDifferentObjectClasses( + parentFactoryType("module-a", new ChildCodecA(), false), + parentFactoryType("module-b", new ChildCodecB(), false)); + } + + @Test + public void directMixinVersionsParent() { + ForyJson first = ForyJson.builder().withAsyncCompilation(false).build(); + ForyJson second = + ForyJson.builder().registerMixin(ChildCodecMixin.class).withAsyncCompilation(false).build(); + assertDifferentObjectClasses(parentType(first), parentType(second)); + + Parent value = new Parent(); + value.child = new Child(); + value.child.value = "value"; + assertEquals(first.toJson(value), "{\"child\":{\"value\":\"value\"}}"); + assertEquals(second.toJson(value), "{\"child\":\"value\"}"); } @Test @@ -339,12 +308,12 @@ public void directCodecStateStaysInstanceOwned() { } @Test - public void transparentTerminalStaysInstanceOwned() throws Exception { - ForyJson first = transparentJson(new TerminalCodecA()); - ForyJson second = transparentJson(new TerminalCodecB()); + public void transparentDirectFactoryVersionsClass() throws Exception { + ForyJson first = transparentJson(new DirectTerminalCodecA()); + ForyJson second = transparentJson(new DirectTerminalCodecB()); JsonTypeInfo firstType = transparentType(first); JsonTypeInfo secondType = transparentType(second); - assertObjectClasses(firstType, secondType); + assertDifferentObjectClasses(firstType, secondType); TransparentModel value = new TransparentModel(); value.setValue(new LocalTerminal("value")); @@ -362,14 +331,6 @@ public void transparentTerminalStaysInstanceOwned() throws Exception { "value"); } - @Test - public void transparentDirectTerminalVersionsClass() throws Exception { - JsonTypeInfo first = transparentType(transparentJson(new DirectTerminalCodecA())); - JsonTypeInfo second = transparentType(transparentJson(new DirectTerminalCodecB())); - - assertDifferentObjectClasses(first, second); - } - @Test public void unrelatedRegistrationReusesParent() { JsonTypeInfo first = parentType(new ChildCodecA()); @@ -408,11 +369,23 @@ public void writeNullVersionsWriters() { } @Test - public void anyCodecUsesCapabilityInterface() { + public void fieldModeVersionsClasses() { + ForyJson properties = ForyJson.builder().withAsyncCompilation(false).build(); + ForyJson fields = ForyJson.builder().withFieldMode(true).withAsyncCompilation(false).build(); + JsonTypeInfo propertyType = + JsonTestSupport.currentTypeResolver(properties).getTypeInfo(Model.class, Model.class); + JsonTypeInfo fieldType = + JsonTestSupport.currentTypeResolver(fields).getTypeInfo(Model.class, Model.class); + + assertDifferentObjectClasses(propertyType, fieldType); + } + + @Test + public void anyCodecVersionsGeneratedRoles() { JsonTypeInfo getterA = anyType(GetterAny.class, new ChildCodecA()); JsonTypeInfo getterB = anyType(GetterAny.class, new ChildCodecB()); - assertSame(getterA.stringWriter().getClass(), getterB.stringWriter().getClass()); - assertSame(getterA.utf8Writer().getClass(), getterB.utf8Writer().getClass()); + assertNotSame(getterA.stringWriter().getClass(), getterB.stringWriter().getClass()); + assertNotSame(getterA.utf8Writer().getClass(), getterB.utf8Writer().getClass()); assertSame(getterA.latin1Reader().getClass(), getterB.latin1Reader().getClass()); assertSame(getterA.utf16Reader().getClass(), getterB.utf16Reader().getClass()); assertSame(getterA.utf8Reader().getClass(), getterB.utf8Reader().getClass()); @@ -421,20 +394,9 @@ public void anyCodecUsesCapabilityInterface() { JsonTypeInfo setterB = anyType(SetterAny.class, new ChildCodecB()); assertSame(setterA.stringWriter().getClass(), setterB.stringWriter().getClass()); assertSame(setterA.utf8Writer().getClass(), setterB.utf8Writer().getClass()); - assertSame(setterA.latin1Reader().getClass(), setterB.latin1Reader().getClass()); - assertSame(setterA.utf16Reader().getClass(), setterB.utf16Reader().getClass()); - assertSame(setterA.utf8Reader().getClass(), setterB.utf8Reader().getClass()); - } - - @Test - public void hostedCodecUsesInterface() throws Exception { - JsonTypeResolver first = hostedResolver(parentJson(new ChildCodecA())); - JsonTypeResolver second = hostedResolver(parentJson(new ChildCodecB())); - first.generateHostedCodecs(Parent.class); - second.generateHostedCodecs(Parent.class); - assertObjectClasses( - first.getTypeInfo(Parent.class, Parent.class), - second.getTypeInfo(Parent.class, Parent.class)); + assertNotSame(setterA.latin1Reader().getClass(), setterB.latin1Reader().getClass()); + assertNotSame(setterA.utf16Reader().getClass(), setterB.utf16Reader().getClass()); + assertNotSame(setterA.utf8Reader().getClass(), setterB.utf8Reader().getClass()); } @Test @@ -565,22 +527,74 @@ public void collectionClassIgnoresElementCodec() { @Test public void sameNamedLoaderClassesDoNotCollide() throws Exception { - byte[] bytes = classBytes(PublicFields.class); - Class firstClass = shadowClass(PublicFields.class, bytes); - Class secondClass = shadowClass(PublicFields.class, bytes); + Class firstClass = JsonTestSupport.shadowClass(PublicFields.class); + Class secondClass = JsonTestSupport.shadowClass(PublicFields.class); assertNotSame(firstClass, secondClass); GeneratedCodecKey firstKey = - GeneratedCodecKey.object(firstClass, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + GeneratedCodecKey.object( + firstClass, null, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); GeneratedCodecKey secondKey = - GeneratedCodecKey.object(secondClass, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + GeneratedCodecKey.object( + secondClass, null, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); assertEquals(firstKey.hashCode(), secondKey.hashCode()); assertNotEquals(firstKey, secondKey); + TypeRef firstBinding = + TypeRef.ofDeclaredTypeArguments( + GenericModel.class, null, Collections.singletonList(TypeRef.of(firstClass)), null); + TypeRef secondBinding = + TypeRef.ofDeclaredTypeArguments( + GenericModel.class, null, Collections.singletonList(TypeRef.of(secondClass)), null); + GeneratedCodecKey firstGenericKey = + GeneratedCodecKey.object( + GenericModel.class, firstBinding, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + GeneratedCodecKey secondGenericKey = + GeneratedCodecKey.object( + GenericModel.class, secondBinding, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + assertEquals(firstGenericKey.hashCode(), secondGenericKey.hashCode()); + assertNotEquals(firstGenericKey, secondGenericKey); + JsonTypeInfo first = loaderType(firstClass); JsonTypeInfo second = loaderType(secondClass); assertDifferentObjectClasses(first, second); } + @Test + public void equalKeysHaveEqualHash() { + TypeRef strings = + TypeRef.ofSemanticTypeArguments( + GenericModel.class, null, Collections.singletonList(TypeRef.of(String.class)), null); + TypeRef integers = + TypeRef.ofSemanticTypeArguments( + GenericModel.class, null, Collections.singletonList(TypeRef.of(Integer.class)), null); + assertEquals(strings, integers); + + GeneratedCodecKey first = + GeneratedCodecKey.object( + GenericModel.class, strings, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + GeneratedCodecKey second = + GeneratedCodecKey.object( + GenericModel.class, integers, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + } + + @Test + public void rootBindingAnchorsBootstrapTarget() { + TypeRef> binding = new TypeRef>() {}; + GeneratedCodecKey key = + GeneratedCodecKey.object( + Map.Entry.class, binding, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + assertSame(key.anchorClass(), Child.class); + + TypeRef> lowerBound = + new TypeRef>() {}; + GeneratedCodecKey lowerBoundKey = + GeneratedCodecKey.object( + Map.Entry.class, lowerBound, GeneratedCodecKey.Role.STRING_WRITER, new Object[0]); + assertSame(lowerBoundKey.anchorClass(), Child.class); + } + private static JsonTypeResolver resolver() { ForyJson json = ForyJson.builder().withAsyncCompilation(false).build(); return JsonTestSupport.currentTypeResolver(json); @@ -598,11 +612,55 @@ private static ForyJson parentJson(JsonValueCodec codec, boolean unrelate ForyJsonBuilder builder = ForyJson.builder().registerCodec(Child.class, codec).withAsyncCompilation(false); if (unrelated) { - builder.registerCodec(Unrelated.class, JsonTestSupport.nullCodec()); + JsonCodecFactory factory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return type.getRawType() == OtherUnrelated.class ? JsonTestSupport.nullCodec() : null; + } + + @Override + public String factoryKey() { + return "unrelated-module"; + } + }; + builder + .registerCodec(Unrelated.class, JsonTestSupport.nullCodec()) + .registerMixin(UnrelatedMixin.class) + .withModule( + context -> { + context.registerCodec(OtherUnrelated.class, factory); + context.registerCodecFactory(factory); + }); } return builder.build(); } + private static JsonTypeInfo parentFactoryType( + String key, JsonValueCodec codec, boolean exact) { + JsonCodecFactory factory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return type.getRawType() == Child.class ? codec : null; + } + + @Override + public String factoryKey() { + return key; + } + }; + ForyJsonBuilder builder = ForyJson.builder().withAsyncCompilation(false); + if (exact) { + builder.registerCodec(Child.class, factory); + } else { + builder.withModule(context -> context.registerCodecFactory(factory)); + } + return parentType(builder.build()); + } + private static JsonTypeInfo parentType(ForyJson json) { return JsonTestSupport.currentTypeResolver(json).getTypeInfo(Parent.class, Parent.class); } @@ -610,13 +668,23 @@ private static JsonTypeInfo parentType(ForyJson json) { private static ForyJson transparentJson(JsonValueCodec terminalCodec) throws Exception { JsonObjectModel model = transparentModel(); + JsonCodecFactory siblingFactory = + new JsonCodecFactory() { + @Override + public JsonValueCodec create( + TypeRef type, JsonTypeResolver resolver, boolean runtimeType) { + return new SiblingTransparentCodec( + resolver.getTypeInfo(LocalTerminal.class, LocalTerminal.class)); + } + + @Override + public String factoryKey() { + return "sibling-transparent:" + terminalCodec.getClass().getName(); + } + }; return ForyJson.builder() .registerCodec(LocalTerminal.class, terminalCodec) - .registerCodec( - SiblingValue.class, - (type, resolver, runtimeType) -> - new SiblingTransparentCodec( - resolver.getTypeInfo(LocalTerminal.class, LocalTerminal.class))) + .registerCodec(SiblingValue.class, siblingFactory) .registerCodec( TransparentModel.class, (type, resolver, runtimeType) -> resolver.createObjectCodec(type, model)) @@ -715,44 +783,6 @@ private static JsonTypeInfo loaderType(Class type) { return JsonTestSupport.currentTypeResolver(json).getTypeInfo((Class) type, type); } - private static Class shadowClass(Class type, byte[] bytes) throws ClassNotFoundException { - String name = type.getName(); - ClassLoader loader = - new ClassLoader(type.getClassLoader()) { - @Override - protected Class loadClass(String className, boolean resolve) - throws ClassNotFoundException { - synchronized (getClassLoadingLock(className)) { - if (!name.equals(className)) { - return super.loadClass(className, resolve); - } - Class loaded = findLoadedClass(className); - if (loaded == null) { - loaded = defineClass(className, bytes, 0, bytes.length); - } - if (resolve) { - resolveClass(loaded); - } - return loaded; - } - } - }; - return loader.loadClass(name); - } - - private static byte[] classBytes(Class type) throws IOException { - String resource = "/" + type.getName().replace('.', '/') + ".class"; - try (InputStream input = type.getResourceAsStream(resource); - ByteArrayOutputStream output = new ByteArrayOutputStream()) { - byte[] buffer = new byte[1024]; - int read; - while ((read = input.read(buffer)) >= 0) { - output.write(buffer, 0, read); - } - return output.toByteArray(); - } - } - private static void compileSource( Path output, String packageName, String simpleName, String declaration) throws IOException { compileSource(output, packageName, simpleName, declaration, new String[0]); @@ -874,27 +904,10 @@ public static final class GenericModel { public GenericModel() {} } - public static final class GenericPair { - public T first; - public U second; - - public GenericPair() {} - } - - public static final class GenericCollection { - public List values; - - public GenericCollection() {} - } - public enum FirstEnum { VALUE } - public enum SecondEnum { - VALUE - } - public static final class FactoryModel { private String first; private long second; @@ -948,12 +961,6 @@ public static final class Child { public Child() {} } - public static final class OtherChild { - public long value; - - public OtherChild() {} - } - public interface SiblingCarrier {} public static final class LocalTerminal implements SiblingCarrier { @@ -1019,10 +1026,11 @@ private static final class HiddenChildCodecA extends ChildCodecA {} private static final class HiddenChildCodecB extends ChildCodecA {} - public abstract static class TerminalCodec implements JsonValueCodec { + public abstract static class DirectTerminalCodec + implements JsonValueCodec, DirectUnboxedValueCodec { private final String prefix; - TerminalCodec(String prefix) { + DirectTerminalCodec(String prefix) { this.prefix = prefix; } @@ -1051,29 +1059,6 @@ public LocalTerminal readUtf8(Utf8JsonReader reader) { return terminal(reader.readString()); } - private LocalTerminal terminal(String value) { - return new LocalTerminal(value.substring(prefix.length())); - } - } - - public static final class TerminalCodecA extends TerminalCodec { - TerminalCodecA() { - super("a:"); - } - } - - public static final class TerminalCodecB extends TerminalCodec { - TerminalCodecB() { - super("b:"); - } - } - - public abstract static class DirectTerminalCodec extends TerminalCodec - implements DirectUnboxedValueCodec { - DirectTerminalCodec(String prefix) { - super(prefix); - } - @Override public Class carrierType() { return LocalTerminal.class; @@ -1103,6 +1088,10 @@ public void writeStringCarrier(StringJsonWriter writer, Object carrier) { public void writeUtf8Carrier(Utf8JsonWriter writer, Object carrier) { writeUtf8(writer, (LocalTerminal) carrier); } + + private LocalTerminal terminal(String value) { + return new LocalTerminal(value.substring(prefix.length())); + } } public static final class DirectTerminalCodecA extends DirectTerminalCodec { @@ -1291,6 +1280,15 @@ private static Method method(String name, Class... parameterTypes) { public static final class Unrelated {} + public static final class OtherUnrelated {} + + @JsonMixin(target = Child.class) + @JsonCodec(ChildCodecA.class) + public abstract static class ChildCodecMixin {} + + @JsonMixin(target = Unrelated.class) + public abstract static class UnrelatedMixin {} + public static final class Box { public T value; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index d7b2b7b6c1..16dc0449be 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -2199,38 +2199,35 @@ public void generatedFloatingReadersAllowWhitespace(boolean codegen) { } @Test(dataProvider = "enableCodegen") - public void customPrimitiveNull(boolean codegen) { + public void primitiveNull(boolean codegen) { ForyJson json = newJson(codegen); assertThrows(ForyJsonException.class, () -> json.fromJson("null", int.class)); assertThrows( ForyJsonException.class, () -> json.fromJson("null".getBytes(StandardCharsets.UTF_8), int.class)); assertThrows( - ForyJsonException.class, - () -> json.fromJson("{\"value\":null}", CustomPrimitiveField.class)); + ForyJsonException.class, () -> json.fromJson("{\"value\":null}", PrimitiveField.class)); assertThrows( ForyJsonException.class, - () -> json.fromJson("{\"ignored\":\"\u0100\",\"value\":null}", CustomPrimitiveField.class)); + () -> json.fromJson("{\"ignored\":\"\u0100\",\"value\":null}", PrimitiveField.class)); assertThrows( ForyJsonException.class, () -> json.fromJson( - "{\"value\":null}".getBytes(StandardCharsets.UTF_8), CustomPrimitiveField.class)); - assertGeneratedWhenSupported(json, CustomPrimitiveField.class, codegen); + "{\"value\":null}".getBytes(StandardCharsets.UTF_8), PrimitiveField.class)); + assertGeneratedWhenSupported(json, PrimitiveField.class, codegen); assertThrows( - ForyJsonException.class, - () -> json.fromJson("{\"value\":null}", CustomPrimitiveSetter.class)); + ForyJsonException.class, () -> json.fromJson("{\"value\":null}", PrimitiveSetter.class)); assertThrows( ForyJsonException.class, - () -> - json.fromJson("{\"ignored\":\"\u0100\",\"value\":null}", CustomPrimitiveSetter.class)); + () -> json.fromJson("{\"ignored\":\"\u0100\",\"value\":null}", PrimitiveSetter.class)); assertThrows( ForyJsonException.class, () -> json.fromJson( - "{\"value\":null}".getBytes(StandardCharsets.UTF_8), CustomPrimitiveSetter.class)); - assertGeneratedWhenSupported(json, CustomPrimitiveSetter.class, codegen); + "{\"value\":null}".getBytes(StandardCharsets.UTF_8), PrimitiveSetter.class)); + assertGeneratedWhenSupported(json, PrimitiveSetter.class, codegen); } @Test @@ -2359,11 +2356,11 @@ public static final class GeneratedFloatingFields { public float floatValue; } - public static final class CustomPrimitiveField { + public static final class PrimitiveField { public int value; } - public static final class CustomPrimitiveSetter { + public static final class PrimitiveSetter { private int stored; public void setValue(int value) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java index 80d2393797..503ea4851e 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonTestSupport.java @@ -19,6 +19,9 @@ package org.apache.fory.json; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collections; @@ -215,6 +218,45 @@ static String stringReaderPath(String input) { : "utf16"; } + static Class shadowClass(Class type) throws ClassNotFoundException, IOException { + String name = type.getName(); + byte[] bytes = classBytes(type); + ClassLoader loader = + new ClassLoader(type.getClassLoader()) { + @Override + protected Class loadClass(String className, boolean resolve) + throws ClassNotFoundException { + synchronized (getClassLoadingLock(className)) { + if (!name.equals(className)) { + return super.loadClass(className, resolve); + } + Class loaded = findLoadedClass(className); + if (loaded == null) { + loaded = defineClass(className, bytes, 0, bytes.length); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + } + }; + return loader.loadClass(name); + } + + private static byte[] classBytes(Class type) throws IOException { + String resource = "/" + type.getName().replace('.', '/') + ".class"; + try (InputStream input = type.getResourceAsStream(resource); + ByteArrayOutputStream output = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int read; + while ((read = input.read(buffer)) >= 0) { + output.write(buffer, 0, read); + } + return output.toByteArray(); + } + } + static int pooledStateCount(ForyJson json) { try { return ((Object[]) field(json, "slots")).length; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java index a91950f60e..b193d9d9c7 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonTypeCheckerTest.java @@ -86,7 +86,7 @@ public void defaultExactSkipsChecker() { } @Test - public void customExactUsesChecker() { + public void customExactWriteUsesChecker() { ForyJson json = newJsonBuilder() .registerCodec(CheckedBean.class, nullCodec()) @@ -105,7 +105,7 @@ public void annotatedExactUsesChecker() { } @Test - public void customCodecUsesChecker() { + public void customExactReadUsesChecker() { ForyJson json = newJsonBuilder() .registerCodec(CheckedBean.class, nullCodec()) From ca3f093825f4b12b1b857611e704f1755f15d401 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Sun, 23 Aug 2026 02:22:20 +0800 Subject: [PATCH 11/11] feat(json): read documents from input streams --- .../java/org/apache/fory/json/ForyJson.java | 27 ++++++++++ .../apache/fory/json/JsonInputStreams.java | 41 ++++++++++++++++ .../apache/fory/json/JsonInputStreams.java | 32 ++++++++++++ .../fory/json/ForyJsonInputStreamTest.java | 49 +++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 java/fory-json/src/main/java/org/apache/fory/json/JsonInputStreams.java create mode 100644 java/fory-json/src/main/java9/org/apache/fory/json/JsonInputStreams.java create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/ForyJsonInputStreamTest.java 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 a2dc2736c8..4cebd03ca1 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 @@ -19,6 +19,8 @@ package org.apache.fory.json; +import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.GenericArrayType; import java.lang.reflect.ParameterizedType; @@ -678,6 +680,22 @@ public T fromJson(byte[] bytes, TypeRef typeRef) { } } + /** + * Reads the complete caller-owned stream as UTF-8 and parses exactly one JSON value using {@code + * type} as its declared Java type. This method does not close the stream. + */ + public T fromJson(InputStream input, Class type) { + return fromJson(readAllBytes(input), type); + } + + /** + * Reads the complete caller-owned stream as UTF-8 and parses exactly one JSON value using a + * generic type captured by {@link TypeRef}. This method does not close the stream. + */ + public T fromJson(InputStream input, TypeRef typeRef) { + return fromJson(readAllBytes(input), typeRef); + } + /** * Parses exactly one UTF-8 JSON value from {@code bytes[offset, offset + length)} using {@code * type} as its declared Java type. Trailing non-whitespace content within that range is rejected. @@ -736,6 +754,15 @@ public T fromJson(byte[] bytes, int offset, int length, TypeRef typeRef) } } + private static byte[] readAllBytes(InputStream input) { + Objects.requireNonNull(input, "input"); + try { + return JsonInputStreams.readAllBytes(input); + } catch (IOException e) { + throw new ForyJsonException("Cannot read JSON input stream", e); + } + } + private PooledState acquire() { PooledState[] slots = this.slots; if (slots.length == 1) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/JsonInputStreams.java b/java/fory-json/src/main/java/org/apache/fory/json/JsonInputStreams.java new file mode 100644 index 0000000000..9668d3ab0d --- /dev/null +++ b/java/fory-json/src/main/java/org/apache/fory/json/JsonInputStreams.java @@ -0,0 +1,41 @@ +/* + * 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.IOException; +import java.io.InputStream; + +/** Java 8 implementation for reading a complete JSON input stream. */ +final class JsonInputStreams { + private static final int BUFFER_SIZE = 8192; + + private JsonInputStreams() {} + + static byte[] readAllBytes(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[BUFFER_SIZE]; + int count; + while ((count = input.read(buffer)) != -1) { + output.write(buffer, 0, count); + } + return output.toByteArray(); + } +} diff --git a/java/fory-json/src/main/java9/org/apache/fory/json/JsonInputStreams.java b/java/fory-json/src/main/java9/org/apache/fory/json/JsonInputStreams.java new file mode 100644 index 0000000000..dd356abb54 --- /dev/null +++ b/java/fory-json/src/main/java9/org/apache/fory/json/JsonInputStreams.java @@ -0,0 +1,32 @@ +/* + * 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.IOException; +import java.io.InputStream; + +/** JDK 9 implementation for reading a complete JSON input stream. */ +final class JsonInputStreams { + private JsonInputStreams() {} + + static byte[] readAllBytes(InputStream input) throws IOException { + return input.readAllBytes(); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonInputStreamTest.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonInputStreamTest.java new file mode 100644 index 0000000000..3b5cbefe9a --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonInputStreamTest.java @@ -0,0 +1,49 @@ +/* + * 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 static org.testng.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import org.apache.fory.reflect.TypeRef; +import org.testng.annotations.Test; + +public class ForyJsonInputStreamTest { + @Test + public void inputStreamOverloads() { + ForyJson json = ForyJson.builder().build(); + InputValue value = json.fromJson(input("{\"name\":\"stream\"}"), InputValue.class); + List values = json.fromJson(input("[1,2,3]"), new TypeRef>() {}); + + assertEquals(value.name, "stream"); + assertEquals(values, Arrays.asList(1, 2, 3)); + } + + private static ByteArrayInputStream input(String json) { + return new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + } + + public static final class InputValue { + public String name; + } +}