You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fory.apache.org by ch...@apache.org on 2026/10/09 14:45:08 UTC
(fory) branch main updated: fix(java): ignore JSON processor artifacts in native image (#4146)
This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 5ea3e1005 fix(java): ignore JSON processor artifacts in native image (#4146)
5ea3e1005 is described below
commit 5ea3e10057a921aa5bfecea14f4553915429ee2f
Author: Ricardo Romero <ri...@gmail.com>
AuthorDate: Fri Oct 9 16:45:02 2026 +0200
fix(java): ignore JSON processor artifacts in native image (#4146)
Fixes #4145
Fory JSON Native Image builds now ignore Java annotation-processor
codecs and subtype tables, including artifacts packaged in dependencies.
The Native Image Feature owns model discovery and code generation at
build time; ordinary JVM and Android processor behavior is unchanged.
The native registry retains only classes generated by the hosted
Feature. The existing GraalVM harness now includes real
processor-generated record, Mixin and sealed-subtype artifacts,
asserting that they are present during the build but excluded from the
native image. Those models are exercised with generated and interpreted
JSON configurations in the native executable.
Validation on GraalVM 25.0.4:
- Fory JSON: 1,283 tests passed.
- `JsonTypeProcessorTest`: 74 tests passed.
- JPMS (`-Pnative-module`) and classpath (`-Pnative`) native-image
builds and both executables passed.
- JSON Spotless/Checkstyle and integration-harness Spotless passed.
The harness compiles with `--release 17` and declares both supported
Native Image SDK module names. GraalVM 17.0.12 JSON and full `Main` JPMS
builds and executables passed; the full `Main` JPMS build and executable
also passed on GraalVM 25.0.4.
---------
Co-authored-by: chaokunyang <sh...@gmail.com>
---
.agents/languages/java.md | 1 +
docs/json/graalvm.md | 3 +-
integration_tests/graalvm_tests/README.md | 8 +-
integration_tests/graalvm_tests/pom.xml | 43 +++++++-
.../graalvm_tests/src/main/java/module-info.java | 3 +
.../org/apache/fory/graalvm/ForyJsonExample.java | 1 +
.../apache/fory/graalvm/JsonProcessorExample.java | 80 +++++++++++++++
.../apache/fory/graalvm/JsonProcessorFeature.java | 59 +++++++++++
.../graalvm_tests/native-image.properties | 1 +
.../json/resolver/JsonGeneratedClassRegistry.java | 112 +++------------------
.../fory/json/resolver/JsonSharedRegistry.java | 73 ++++----------
...gistryTest.java => JsonSharedRegistryTest.java} | 48 +--------
12 files changed, 223 insertions(+), 209 deletions(-)
diff --git a/.agents/languages/java.md b/.agents/languages/java.md
index 90f6b3cf8..98d0caff4 100644
--- a/.agents/languages/java.md
+++ b/.agents/languages/java.md
@@ -261,6 +261,7 @@ Load this file when changing anything under `java/` or when Java drives a cross-
Copy the payload through the typed payload API, then swap destination values locally so the path
stays stream-safe and avoids Android-dispatch helper drift.
- Keep GraalVM feature code as a thin metadata/registration layer. Build time should publish metadata needed for runtime reconstruction, not retain concrete generated or user serializer instances in the image heap.
+- Fory JSON Native Image must ignore annotation-processor codec companions and subtype tables during both hosted analysis and runtime. The Native Image Feature owns model discovery, access preparation, subtype closure and code generation, regardless of processor artifacts on the application classpath. Keep processor artifact loading confined to ordinary JVM/Android paths.
- If changes touch GraalVM bootstrap, serializer retention, native-image metadata, or `ObjectStreamSerializer` GraalVM behavior, verify the native-image build and run the produced binary; a plain Java compile is insufficient.
- Put latest-JDK or virtual-thread tests in the latest-JDK test modules with the matching compiler/profile floor, and centralize runtime-version probing in existing compatibility utilities.
- For JDK25+ zero-Unsafe work, preserve serializer-family selection by type and configuration. Do not switch a type from `ObjectStreamSerializer` or another Fory serializer family to `JavaSerializer`, a JDK stream fallback, or any broad `java.* Serializable` fallback by JDK version or no-arg-constructor shape.
diff --git a/docs/json/graalvm.md b/docs/json/graalvm.md
index 508bb9cda..186c2d3a1 100644
--- a/docs/json/graalvm.md
+++ b/docs/json/graalvm.md
@@ -22,7 +22,8 @@ license: |
## Reachable Models
Fory JSON has one Native Image Feature. Java models are discovered from reachable annotations; the
-Feature does not use the Java annotation processor. Add `@JsonType` to each reachable concrete
+Feature does not require the Java annotation processor and ignores its generated JSON artifacts,
+including those packaged in dependencies. Add `@JsonType` to each reachable concrete
Java object model that the native executable reads or writes:
```java
diff --git a/integration_tests/graalvm_tests/README.md b/integration_tests/graalvm_tests/README.md
index 4f15e19f2..b2ab216f6 100644
--- a/integration_tests/graalvm_tests/README.md
+++ b/integration_tests/graalvm_tests/README.md
@@ -1,7 +1,13 @@
# GraalVM Native Image Tests
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
+compiled with annotation processing disabled, except for `JsonProcessorExample`. That fixture uses
+the real Fory annotation processor; a test Feature verifies that its generated codecs and subtype
+table exist during the build but do not become reachable in the native executable. Both generated
+and interpreted JSON configurations exercise those models at runtime.
+
+Install `fory-core`, `fory-json`, and `fory-annotation-processor` from `java/` before building.
+The tests cover direct `JsonType` models, exact
`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.
diff --git a/integration_tests/graalvm_tests/pom.xml b/integration_tests/graalvm_tests/pom.xml
index 8d77979ef..b7ee21b97 100644
--- a/integration_tests/graalvm_tests/pom.xml
+++ b/integration_tests/graalvm_tests/pom.xml
@@ -31,8 +31,8 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>graalvm_tests</artifactId>
<properties>
- <maven.compiler.source>17</maven.compiler.source>
- <maven.compiler.target>17</maven.compiler.target>
+ <!-- Use Java SE APIs so GraalVM's bundled SDK cannot conflict with the compile dependency. -->
+ <maven.compiler.release>17</maven.compiler.release>
<maven.javadoc.skip>true</maven.javadoc.skip>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<native.maven.plugin.version>0.9.28</native.maven.plugin.version>
@@ -57,6 +57,18 @@
</repositories>
<dependencies>
+ <dependency>
+ <groupId>org.graalvm.sdk</groupId>
+ <artifactId>graal-sdk</artifactId>
+ <version>23.1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.graalvm.sdk</groupId>
+ <artifactId>nativeimage</artifactId>
+ <version>23.1.0</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-core</artifactId>
@@ -93,10 +105,33 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
- <source>${maven.compiler.source}</source>
- <target>${maven.compiler.source}</target>
<proc>none</proc>
</configuration>
+ <executions>
+ <execution>
+ <id>generate-json-companions</id>
+ <phase>generate-sources</phase>
+ <goals>
+ <goal>compile</goal>
+ </goals>
+ <configuration>
+ <proc>only</proc>
+ <!-- The compiler excludes its own generated-sources directory from inputs. -->
+ <generatedSourcesDirectory>${project.build.directory}/generated-sources/json</generatedSourcesDirectory>
+ <includes>
+ <include>module-info.java</include>
+ <include>org/apache/fory/graalvm/JsonProcessorExample.java</include>
+ </includes>
+ <annotationProcessorPaths combine.self="override">
+ <path>
+ <groupId>org.apache.fory</groupId>
+ <artifactId>fory-annotation-processor</artifactId>
+ <version>${project.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </execution>
+ </executions>
</plugin>
<plugin>
diff --git a/integration_tests/graalvm_tests/src/main/java/module-info.java b/integration_tests/graalvm_tests/src/main/java/module-info.java
index 9e5f1f5ab..75735c24b 100644
--- a/integration_tests/graalvm_tests/src/main/java/module-info.java
+++ b/integration_tests/graalvm_tests/src/main/java/module-info.java
@@ -20,6 +20,9 @@
module org.apache.fory.graalvm.tests {
requires org.apache.fory.core;
requires org.apache.fory.json;
+ // Native Image's hosted API belongs to sdk on GraalVM 17 and nativeimage on newer releases.
+ requires static org.graalvm.sdk;
+ requires static org.graalvm.nativeimage;
requires java.sql;
// Binary serialization acceptance retains its existing exported and opened model packages.
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 0362610e3..113bafae9 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
@@ -97,6 +97,7 @@ public final class ForyJsonExample {
ClosedJsonConfigs.class.isAnnotationPresent(ForyJsonProvider.class));
if (GraalvmSupport.isGraalRuntime()) {
testHostedCodegenConfigurations();
+ JsonProcessorExample.verify();
}
testModels();
testMethodAccessors();
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/JsonProcessorExample.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/JsonProcessorExample.java
new file mode 100644
index 000000000..130ccd34a
--- /dev/null
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/JsonProcessorExample.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.graalvm;
+
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.annotation.JsonMixin;
+import org.apache.fory.json.annotation.JsonProperty;
+import org.apache.fory.json.annotation.JsonSubTypes;
+import org.apache.fory.json.annotation.JsonType;
+import org.apache.fory.util.Preconditions;
+
+/**
+ * Models compiled with the annotation processor alongside the unprocessed Native Image fixtures.
+ */
+public final class JsonProcessorExample {
+ private JsonProcessorExample() {}
+
+ public static void verify() {
+ for (boolean codegen : new boolean[] {true, false}) {
+ ForyJson json = ForyJson.builder().withCodegen(codegen).build();
+ User user = json.fromJson("{\"id\":7,\"name\":\"Ada\"}", User.class);
+ Preconditions.checkArgument(user.id() == 7 && user.name().equals("Ada"));
+ Preconditions.checkArgument(json.fromJson(json.toJson(user), User.class).equals(user));
+ Message message =
+ json.fromJson("{\"kind\":\"TextMessage\",\"text\":\"hello\"}", Message.class);
+ Preconditions.checkArgument(message.equals(new TextMessage("hello")));
+
+ ForyJson mixinJson =
+ ForyJson.builder().withCodegen(codegen).registerMixin(ExternalMixin.class).build();
+ External external = mixinJson.fromJson("{\"user_id\":9}", External.class);
+ Preconditions.checkArgument(external.getId() == 9);
+ Preconditions.checkArgument(mixinJson.toJson(external).equals("{\"user_id\":9}"));
+ }
+ }
+
+ @JsonType
+ public record User(long id, String name) {}
+
+ @JsonType
+ @JsonSubTypes(property = "kind")
+ public sealed interface Message permits TextMessage {}
+
+ @JsonType
+ public record TextMessage(String text) implements Message {}
+
+ public static final class External {
+ private int id;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+ }
+
+ @JsonMixin(target = External.class)
+ public abstract static class ExternalMixin {
+ @JsonProperty("user_id")
+ public abstract int getId();
+ }
+}
diff --git a/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/JsonProcessorFeature.java b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/JsonProcessorFeature.java
new file mode 100644
index 000000000..fb5f008ef
--- /dev/null
+++ b/integration_tests/graalvm_tests/src/main/java/org/apache/fory/graalvm/JsonProcessorFeature.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.graalvm;
+
+import org.graalvm.nativeimage.hosted.Feature;
+import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
+
+/**
+ * Verifies that real processor artifacts are available but excluded from the Native Image graph.
+ */
+public final class JsonProcessorFeature implements Feature {
+ private static final String[] GENERATED_TYPES = {
+ "org.apache.fory.graalvm.JsonProcessorExample_d_User_ForyJsonCodec",
+ "org.apache.fory.graalvm.JsonProcessorExample_d_TextMessage_ForyJsonCodec",
+ "org.apache.fory.graalvm.JsonProcessorExample_d_Message_ForyJsonSubTypes",
+ "org.apache.fory.graalvm.JsonProcessorExample_d_ExternalMixin_ForyJsonMixin_"
+ + "org_x2e_apache_x2e_fory_x2e_graalvm_x2e_JsonProcessorExample_d_External_ForyJsonCodec"
+ };
+
+ @Override
+ public void beforeAnalysis(BeforeAnalysisAccess access) {
+ for (String name : GENERATED_TYPES) {
+ Class<?> type = access.findClassByName(name);
+ if (type == null) {
+ throw new IllegalStateException("Missing annotation-processor fixture: " + name);
+ }
+ // Also reject hosted construction of transient artifacts, such as subtype tables, which
+ // could otherwise be discarded before the reachability assertion below.
+ RuntimeClassInitialization.initializeAtRunTime(type);
+ }
+ }
+
+ @Override
+ public void afterAnalysis(AfterAnalysisAccess access) {
+ for (String name : GENERATED_TYPES) {
+ if (access.isReachable(access.findClassByName(name))) {
+ throw new IllegalStateException(
+ "Native Image retained an annotation-processor artifact: " + name);
+ }
+ }
+ }
+}
diff --git a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
index a1bf2c11b..0c4f7eca1 100644
--- a/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
+++ b/integration_tests/graalvm_tests/src/main/resources/META-INF/native-image/org.apache.fory/graalvm_tests/native-image.properties
@@ -18,6 +18,7 @@
# https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Reflection/#unsafe-accesses :
# The unsafe offset get on build time may be different from runtime
Args=-H:+ReportExceptionStackTraces \
+ --features=org.apache.fory.graalvm.JsonProcessorFeature \
--initialize-at-build-time=org.apache.fory.graalvm.Example,\
org.apache.fory.graalvm.ForyJsonExample,\
org.apache.fory.graalvm.XlangExample,\
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 0249b6064..de9b5d236 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
@@ -25,18 +25,13 @@ 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.GeneratedCodecKey;
-import org.apache.fory.json.resolver.JsonSharedRegistry.GeneratedClasses;
-import org.apache.fory.reflect.TypeRef;
/** Frozen exact-key registry of generated JSON classes retained in a Native Image. */
@Internal
public final class JsonGeneratedClassRegistry {
private static Map<GeneratedCodecKey, Class<?>> pendingClasses = new HashMap<>();
- private static Map<CompanionKey, GeneratedJsonCodec<?>> pendingCompanions = new HashMap<>();
private static Map<GeneratedCodecKey, Class<?>> generatedClasses = Collections.emptyMap();
- private static CompanionEntry[] companionEntries = new CompanionEntry[0];
private JsonGeneratedClassRegistry() {}
@@ -45,10 +40,19 @@ public final class JsonGeneratedClassRegistry {
if (pendingClasses == null) {
throw new IllegalStateException("Fory JSON generated class registry is frozen");
}
- GeneratedClasses generated = hostedRegistry.generatedClasses();
LinkedHashSet<Class<?>> added = new LinkedHashSet<>();
- mergeClasses(generated.classes(), added);
- mergeSourceCodecs(generated.sourceCodecs(), pendingCompanions, added);
+ for (Map.Entry<GeneratedCodecKey, Class<?>> entry :
+ hostedRegistry.generatedClasses().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());
+ }
+ }
return added;
}
@@ -58,9 +62,7 @@ public final class JsonGeneratedClassRegistry {
return;
}
generatedClasses = pendingClasses;
- snapshotCompanions();
pendingClasses = null;
- pendingCompanions = null;
}
static Class<?> generatedClass(GeneratedCodecKey key) {
@@ -70,94 +72,4 @@ public final class JsonGeneratedClassRegistry {
}
return generatedClasses.get(key);
}
-
- static GeneratedJsonCodec<?> sourceCodec(CompanionKey key) {
- Map<CompanionKey, GeneratedJsonCodec<?>> 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<GeneratedCodecKey, Class<?>> source, Set<Class<?>> added) {
- for (Map.Entry<GeneratedCodecKey, Class<?>> 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());
- }
- }
- }
-
- static void mergeSourceCodecs(
- Map<CompanionKey, ? extends GeneratedJsonCodec<?>> source,
- Map<CompanionKey, GeneratedJsonCodec<?>> target,
- Set<Class<?>> added) {
- for (Map.Entry<CompanionKey, ? extends GeneratedJsonCodec<?>> entry : source.entrySet()) {
- GeneratedJsonCodec<?> codec = entry.getValue();
- 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 " + entry.getKey().type);
- }
- }
- }
-
- 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<CompanionKey, GeneratedJsonCodec<?>> entry : pendingCompanions.entrySet()) {
- companionEntries[index++] = new CompanionEntry(entry.getKey(), entry.getValue());
- }
- }
-
- static final class CompanionKey {
- private final TypeRef<?> type;
- private final Class<?> mixinType;
-
- CompanionKey(TypeRef<?> type, Class<?> mixinType) {
- this.type = type;
- this.mixinType = mixinType;
- }
-
- @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;
- }
-
- @Override
- public int hashCode() {
- return type.hashCode() * 31 + System.identityHashCode(mixinType);
- }
- }
-
- private static final class CompanionEntry {
- private final CompanionKey key;
- private final GeneratedJsonCodec<?> codec;
-
- 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 f496d1641..ce12aeacd 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
@@ -131,7 +131,6 @@ 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.CompanionKey;
import org.apache.fory.platform.AndroidSupport;
import org.apache.fory.platform.GraalvmSupport;
import org.apache.fory.reflect.ReflectionUtils;
@@ -209,7 +208,6 @@ public final class JsonSharedRegistry {
private final ConcurrentHashMap<Class<? extends MapKeyCodec>, MapKeyCodec> mapKeyCodecs;
private final ConcurrentHashMap<Class<?>, GeneratedJsonCodec<?>> generatedCodecs;
private final Set<Class<?>> typesWithoutGeneratedCodec;
- private final ConcurrentHashMap<CompanionKey, GeneratedJsonCodec<?>> generatedCodecCapabilities;
private final ConcurrentHashMap<GeneratedCodecKey, CompletableFuture<Class<?>>>
generatedClassFutures;
// Only ForyJson's fixed-pool reader-local caches publish production entries here, and each reader
@@ -273,7 +271,6 @@ public final class JsonSharedRegistry {
mapKeyCodecs = new ConcurrentHashMap<>();
generatedCodecs = new ConcurrentHashMap<>();
typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet();
- generatedCodecCapabilities = new ConcurrentHashMap<>();
generatedClassFutures = new ConcurrentHashMap<>();
cachedFieldNames = new ConcurrentHashMap<>();
boolean codegenEnabled = config.codegenEnabled();
@@ -301,16 +298,11 @@ public final class JsonSharedRegistry {
}
/** Returns a complete immutable snapshot of every synchronously generated class. */
- GeneratedClasses generatedClasses() {
+ Map<GeneratedCodecKey, Class<?>> generatedClasses() {
if (codegen == null || asyncCompilationEnabled) {
throw new IllegalStateException("Generated class snapshots require synchronous codegen");
}
- Map<GeneratedCodecKey, Class<?>> classes = completedClasses(generatedClassFutures);
- Map<CompanionKey, GeneratedJsonCodec<?>> sourceCodecs =
- generatedCodecCapabilities.isEmpty()
- ? Collections.emptyMap()
- : Collections.unmodifiableMap(new HashMap<>(generatedCodecCapabilities));
- return new GeneratedClasses(classes, sourceCodecs);
+ return completedClasses(generatedClassFutures);
}
private static Map<GeneratedCodecKey, Class<?>> completedClasses(
@@ -331,26 +323,6 @@ public final class JsonSharedRegistry {
return Collections.unmodifiableMap(classes);
}
- static final class GeneratedClasses {
- private final Map<GeneratedCodecKey, Class<?>> classes;
- private final Map<CompanionKey, GeneratedJsonCodec<?>> sourceCodecs;
-
- private GeneratedClasses(
- Map<GeneratedCodecKey, Class<?>> classes,
- Map<CompanionKey, GeneratedJsonCodec<?>> sourceCodecs) {
- this.classes = classes;
- this.sourceCodecs = sourceCodecs;
- }
-
- Map<GeneratedCodecKey, Class<?>> classes() {
- return classes;
- }
-
- Map<CompanionKey, GeneratedJsonCodec<?>> sourceCodecs() {
- return sourceCodecs;
- }
- }
-
CompletableFuture<Class<?>> stringWriterClass(
GeneratedCodecKey key, ObjectCodec<?> owner, JsonTypeResolver resolver) {
return generatedClassFuture(key, () -> codegen.compileStringWriter(key, owner, resolver));
@@ -475,29 +447,15 @@ public final class JsonSharedRegistry {
}
GeneratedJsonCodec<?> generatedCodec(TypeRef<?> type) {
- return generatedCodec(type, true);
- }
-
- private GeneratedJsonCodec<?> generatedCodec(TypeRef<?> type, boolean requireCompanion) {
- if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && !hostedCodegen) {
- // Native hosted analysis owns reflection reachability and generated capabilities. A Java
- // annotation-processor companion is an optional faster operation source, not a prerequisite.
- return JsonGeneratedClassRegistry.sourceCodec(
- new CompanionKey(type, mixinType(type.getRawType())));
- }
- GeneratedJsonCodec<?> codec =
- generatedCodec(type.getRawType(), requireCompanion && !hostedCodegen);
- if (codec != null && hostedCodegen) {
- 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);
- }
- }
- return codec;
+ return generatedCodec(type.getRawType(), true);
}
private GeneratedJsonCodec<?> generatedCodec(Class<?> type, boolean requireCompanion) {
+ // Native Image's hosted Feature owns model discovery and code generation. Ignore processor
+ // artifacts at both image build time and runtime so the classpath cannot change that model.
+ if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE || hostedCodegen) {
+ return null;
+ }
Class<?> mixinType = mixinType(type);
boolean directType = type.getDeclaredAnnotation(JsonType.class) != null;
if (!directType && mixinType == null) {
@@ -515,7 +473,7 @@ public final class JsonSharedRegistry {
}
GeneratedJsonCodec<?> generatedCodecIfPresent(TypeRef<?> type) {
- return generatedCodec(type, false);
+ return generatedCodec(type.getRawType(), false);
}
private GeneratedJsonCodec<?> generatedCodecIfPresent(Class<?> type, Class<?> mixinType) {
@@ -1404,6 +1362,7 @@ public final class JsonSharedRegistry {
Class<?> mixinType = mixinType(targetType);
boolean loadGeneratedCodec =
!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE
+ && !hostedCodegen
&& (targetType.getDeclaredAnnotation(JsonType.class) != null || mixinType != null);
GeneratedJsonCodec<?> generatedCodec =
loadGeneratedCodec ? generatedCodecIfPresent(targetType, mixinType) : null;
@@ -1948,11 +1907,13 @@ public final class JsonSharedRegistry {
throw new ForyJsonException(
"Missing embedded inferred subtype table for " + baseType.getName());
}
- // A generated table owns source-level discriminator names after class-file obfuscation. Prefer
- // it on every JVM; sealed reflection remains the unprocessed Java 17 fallback.
- GeneratedJsonSubtypeTable table = loadGeneratedSubtypeTable(baseType);
- if (table != null) {
- return generatedSubtypes(baseType, table);
+ // JVM/Android processor tables preserve source names after obfuscation. Native Image instead
+ // derives and embeds its sealed closure during hosted analysis, ignoring processor artifacts.
+ if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && !hostedCodegen) {
+ GeneratedJsonSubtypeTable table = loadGeneratedSubtypeTable(baseType);
+ if (table != null) {
+ return generatedSubtypes(baseType, table);
+ }
}
if (AndroidSupport.IS_ANDROID) {
throw missingGeneratedSubtypeTable(baseType, mixinType(baseType));
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/JsonSharedRegistryTest.java
similarity index 56%
rename from java/fory-json/src/test/java/org/apache/fory/json/resolver/JsonGeneratedClassRegistryTest.java
rename to java/fory-json/src/test/java/org/apache/fory/json/resolver/JsonSharedRegistryTest.java
index 478bc65e5..28f1a8bae 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/JsonSharedRegistryTest.java
@@ -19,47 +19,15 @@
package org.apache.fory.json.resolver;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.expectThrows;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-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.json.resolver.JsonGeneratedClassRegistry.CompanionKey;
-import org.apache.fory.reflect.TypeRef;
import org.testng.annotations.Test;
-public class JsonGeneratedClassRegistryTest {
- @Test
- public void mergeSourceCodecs() {
- TypeRef<?> type = TypeRef.of(String.class);
- CompanionKey key = new CompanionKey(type, null);
- Map<CompanionKey, GeneratedJsonCodec<?>> codecs = new HashMap<>();
- Set<Class<?>> added = new LinkedHashSet<>();
- SourceCodec first = new SourceCodec();
- SourceCodec second = new SourceCodec();
- JsonGeneratedClassRegistry.mergeSourceCodecs(
- Collections.singletonMap(key, first), codecs, added);
- JsonGeneratedClassRegistry.mergeSourceCodecs(
- 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(key, new OtherSourceCodec()), codecs, added));
- }
-
+public class JsonSharedRegistryTest {
@Test
public void validateGeneratedNonRecordCreator() throws Exception {
CreatorCodec codec = new CreatorCodec();
@@ -67,20 +35,6 @@ public class JsonGeneratedClassRegistryTest {
assertTrue(codec.matchesCreator(CreatorValue.class.getConstructor(String.class)));
}
- private static class SourceCodec extends GeneratedJsonCodec<String> {
- @Override
- public Class<String> type() {
- return String.class;
- }
-
- @Override
- public JsonFieldAccessor[] fieldAccessors() {
- return new JsonFieldAccessor[0];
- }
- }
-
- private static final class OtherSourceCodec extends SourceCodec {}
-
public static final class CreatorValue {
@JsonValue public final String value;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@fory.apache.org
For additional commands, e-mail: commits-help@fory.apache.org