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 93a30f51b feat(java): add Json object field cache (#3862)
93a30f51b is described below
commit 93a30f51b66833e6a8893556468e8deb2ded995b
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Jul 16 14:19:18 2026 +0530
feat(java): add Json object field cache (#3862)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
docs/guide/java/json-support.md | 9 +-
java/fory-json/README.md | 11 +-
.../main/java/org/apache/fory/json/ForyJson.java | 3 +
.../java/org/apache/fory/json/ForyJsonBuilder.java | 25 +-
.../main/java/org/apache/fory/json/JsonConfig.java | 23 +-
.../java/org/apache/fory/json/codec/MapCodec.java | 22 +-
.../apache/fory/json/reader/FieldNameCache.java | 123 ++++++
.../org/apache/fory/json/reader/JsonReader.java | 32 +-
.../apache/fory/json/reader/Latin1JsonReader.java | 144 ++++++
.../apache/fory/json/reader/Utf16JsonReader.java | 186 +++++++-
.../apache/fory/json/reader/Utf8JsonReader.java | 123 ++++++
.../fory/json/resolver/JsonSharedRegistry.java | 47 +-
.../fory/json/resolver/JsonTypeResolver.java | 6 +
.../org/apache/fory/json/JsonAnyPropertyTest.java | 13 +
.../apache/fory/json/JsonAsyncCompilationTest.java | 1 +
.../apache/fory/json/JsonFieldNameCacheTest.java | 481 +++++++++++++++++++++
.../java/org/apache/fory/json/JsonTestSupport.java | 12 +
.../fory/json/reader/FieldNameCacheTest.java | 105 +++++
18 files changed, 1339 insertions(+), 27 deletions(-)
diff --git a/docs/guide/java/json-support.md b/docs/guide/java/json-support.md
index 2ab106e3f..042320821 100644
--- a/docs/guide/java/json-support.md
+++ b/docs/guide/java/json-support.md
@@ -291,13 +291,16 @@ are rejected.
| `withPropertyNamingStrategy` | `LOWER_CAMEL_CASE`
| Naming of properties without explicit names |
| `withClassLoader` | Snapshotted context loader, then Fory loader
| Resolve annotation subtype class names |
| `maxDepth` | `20`
| Maximum nested object/array depth |
+| `withMaxCachedFieldNames` | `DEFAULT_MAX_CACHED_FIELD_NAMES` (`8192`)
| Field-name cache entries per reader; zero disables it |
| `withConcurrencyLevel` | `max(1, 2 * processors)`
| Reusable operation-state count |
| `withBufferSizeLimitBytes` | 2 MiB
| Reusable capacity retained by each pooled writer |
| `registerCodec` | None
| Exact-class complete-value codec |
| `withTypeChecker` | None
| Application policy in addition to Fory's disallow list |
-Depth, concurrency, and retained buffer limits must be positive. The buffer
setting does not limit
-output size. Builder changes after `build()` do not mutate an existing runtime.
+Depth, concurrency, and retained buffer limits must be positive. The
cached-field-name limit applies
+independently to each reader; zero disables the cache, and the setting does
not limit accepted
+input. The buffer setting does not limit output size. Builder changes after
`build()` do not mutate
+an existing runtime.
In a GraalVM native image, runtime code generation and asynchronous
compilation are automatically
disabled. Every other builder option keeps the behavior described above.
@@ -1108,7 +1111,7 @@ Circular graphs eventually fail `maxDepth`; they are not
reconstructed.
| ---------------------------------- |
-----------------------------------------------------------------------------------------------
|
| `ForyJsonException` | Check JSON grammar, target type,
mapping support, depth, trailing content, or output cause |
| `InsecureException` | Check Fory's disallow list and the
configured type checker |
-| Builder `IllegalArgumentException` | Use positive depth, concurrency, and
retained-buffer values |
+| Builder `IllegalArgumentException` | Check the configured depth,
concurrency, retained-buffer, and cached-field-name limits |
| Declared write fails | Remove wildcard/type variables and pass
an assignable value; primitive declarations reject null |
| Immutable value is empty | Use a record, valid creator, or custom
codec |
| `JsonValue` read fails | Add one plain `String` creator, or
register an exact custom codec |
diff --git a/java/fory-json/README.md b/java/fory-json/README.md
index 0bb3c34a7..ed7503975 100644
--- a/java/fory-json/README.md
+++ b/java/fory-json/README.md
@@ -345,14 +345,17 @@ original key type. Null map keys are rejected.
| `withPropertyNamingStrategy(strategy)` | `LOWER_CAMEL_CASE`
| Name properties without an explicit `JsonProperty` name
|
| `withClassLoader(loader)` | Snapshotted thread context loader,
then Fory JSON loader | Resolve annotation-declared subtype class names
|
| `maxDepth(int)` | `20`
| Maximum nested object/array depth for reads and writes
|
+| `withMaxCachedFieldNames(int)` | `DEFAULT_MAX_CACHED_FIELD_NAMES`
(`8192`) | Field-name cache entries per reader; zero disables
caching |
| `withConcurrencyLevel(int)` | `max(1, 2 * processors)`
| Number of reusable concurrent operation states
|
| `withBufferSizeLimitBytes(int)` | 2 MiB
| Maximum reusable capacity retained by each pooled writer
|
| `registerCodec(type, codec)` | None
| Replace the exact class's complete JSON codec
|
| `withTypeChecker(checker)` | No custom checker
| Apply an application type policy in addition to Fory's
disallow list |
-Depth, concurrency level, and buffer retention limit must be positive. The
buffer retention setting
-does not limit JSON input or output size; it only limits reusable writer
storage retained after an
-operation. Apply request/body size limits at the transport boundary when
parsing untrusted input.
+Depth, concurrency level, and buffer retention limit must be positive. The
cached-field-name limit
+applies independently to each reader. It does not limit accepted JSON input;
zero disables this
+cache. The buffer retention setting does not limit JSON input or output size;
it only limits
+reusable writer storage retained after an operation. Apply request/body size
limits at the
+transport boundary when parsing untrusted input.
Builder mutation after `build()` does not modify an existing `ForyJson`
runtime.
@@ -1249,7 +1252,7 @@ native or xlang protocol when reference identity or
cycles are required.
| ----------------------------------------- |
---------------------------------------------------------------------------------------------------------------------------------------------------
|
| `ForyJsonException` while parsing | Invalid JSON grammar, type
mismatch, unsupported mapping, depth violation, or trailing content; inspect
the message and target type |
| `InsecureException` | Fory's disallow list or the
configured `JsonTypeChecker` rejected a class
|
-| `IllegalArgumentException` from a builder | Depth, concurrency level, or
retained buffer limit is not positive
|
+| `IllegalArgumentException` from a builder | Check the configured depth,
concurrency, retained-buffer, and cached-field-name limits
|
| Declared write is rejected | The value is not assignable to
the declared type, the type contains a wildcard/type variable, or null was
supplied for a primitive |
| Immutable value is not populated | Use a record, a valid
`JsonCreator`, or an exact custom codec
|
| `JsonValue` read fails | Add one plain `String`
`JsonCreator`, or register an exact custom codec
|
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 c2989c3a8..d9ed4f2c6 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
@@ -78,6 +78,9 @@ public final class ForyJson {
/** Default maximum nested JSON object/array depth accepted while reading or
writing. */
public static final int DEFAULT_MAX_DEPTH = 20;
+ /** Default maximum number of short, unescaped ASCII field names cached by
each JSON reader. */
+ public static final int DEFAULT_MAX_CACHED_FIELD_NAMES = 8192;
+
private final JsonConfig config;
private final JsonSharedRegistry sharedRegistry;
private final int secondaryPoolSize;
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 eed84065d..554c23f0b 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
@@ -36,10 +36,11 @@ import org.apache.fory.platform.GraalvmSupport;
*
* <p>Defaults omit null object fields, enable code generation and
asynchronous compilation where
* supported, use JavaBean property discovery, use {@link
PropertyNamingStrategy#LOWER_CAMEL_CASE},
- * snapshot the current thread context class loader, allow a nesting depth of
20, use twice the
- * available processors as the pooled-state concurrency level, retain writer
buffers up to 2 MiB,
- * and install no custom type checker. Field mode disables getter and setter
discovery but continues
- * to discover eligible instance fields across the class hierarchy.
+ * snapshot the current thread context class loader, allow a nesting depth of
20, cache up to 8192
+ * common field names in each reader, use twice the available processors as
the pooled-state
+ * concurrency level, retain writer buffers up to 2 MiB, and install no custom
type checker. Field
+ * mode disables getter and setter discovery but continues to discover
eligible instance fields
+ * across the class hierarchy.
*/
public final class ForyJsonBuilder {
private boolean writeNullFields;
@@ -49,6 +50,7 @@ public final class ForyJsonBuilder {
private PropertyNamingStrategy propertyNamingStrategy =
PropertyNamingStrategy.LOWER_CAMEL_CASE;
private ClassLoader classLoader;
private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH;
+ private int maxCachedFieldNames = ForyJson.DEFAULT_MAX_CACHED_FIELD_NAMES;
private int concurrencyLevel = Math.max(1,
Runtime.getRuntime().availableProcessors() * 2);
private int bufferSizeLimitBytes = 2 * 1024 * 1024;
private JsonTypeChecker typeChecker;
@@ -133,6 +135,20 @@ public final class ForyJsonBuilder {
return this;
}
+ /**
+ * Sets the maximum number of unescaped ASCII object field names of up to 16
characters cached by
+ * each JSON reader.
+ *
+ * <p>The default is {@link ForyJson#DEFAULT_MAX_CACHED_FIELD_NAMES}. The
supported range is 0
+ * through 536870912, inclusive; zero disables field-name caching. Other
field names are parsed
+ * normally without being cached. The limit does not restrict accepted JSON
input.
+ */
+ public ForyJsonBuilder withMaxCachedFieldNames(int maxCachedFieldNames) {
+ JsonConfig.validateMaxCachedFieldNames(maxCachedFieldNames);
+ this.maxCachedFieldNames = maxCachedFieldNames;
+ return this;
+ }
+
/** Sets the number of reusable execution states available to concurrent
root operations. */
public ForyJsonBuilder withConcurrencyLevel(int concurrencyLevel) {
if (concurrencyLevel < 1) {
@@ -200,6 +216,7 @@ public final class ForyJsonBuilder {
propertyNamingStrategy,
fixedClassLoader,
maxDepth,
+ maxCachedFieldNames,
concurrencyLevel,
bufferSizeLimitBytes,
codecRegistry,
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 83c58ef28..e4e4b91c4 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
@@ -33,10 +33,12 @@ import org.apache.fory.json.resolver.CodecRegistry;
* The type checker uses identity semantics because checker instances may
carry different user
* policy despite sharing a class. {@link #getCodegenHash()} identifies only
settings that can
* change generated source; runtime-only settings such as depth and
asynchronous scheduling do not
- * fragment generated class names. Concurrency and retained writer-buffer
limits are also
- * runtime-only and do not fragment generated class names.
+ * fragment generated class names. Concurrency, per-reader field-name cache,
and retained
+ * writer-buffer limits are also runtime-only and do not fragment generated
class names.
*/
public final class JsonConfig {
+ private static final int MAX_CACHED_FIELD_NAMES = 1 << 29;
+
private final boolean writeNullFields;
private final boolean codegenEnabled;
private final boolean asyncCompilationEnabled;
@@ -44,6 +46,7 @@ public final class JsonConfig {
private final PropertyNamingStrategy propertyNamingStrategy;
private final ClassLoader classLoader;
private final int maxDepth;
+ private final int maxCachedFieldNames;
private final int concurrencyLevel;
private final int bufferSizeLimitBytes;
private final CodecRegistry codecRegistry;
@@ -61,6 +64,7 @@ public final class JsonConfig {
PropertyNamingStrategy propertyNamingStrategy,
ClassLoader classLoader,
int maxDepth,
+ int maxCachedFieldNames,
int concurrencyLevel,
int bufferSizeLimitBytes,
CodecRegistry codecRegistry,
@@ -73,6 +77,8 @@ public final class JsonConfig {
Objects.requireNonNull(propertyNamingStrategy,
"propertyNamingStrategy");
this.classLoader = Objects.requireNonNull(classLoader, "classLoader");
this.maxDepth = maxDepth;
+ validateMaxCachedFieldNames(maxCachedFieldNames);
+ this.maxCachedFieldNames = maxCachedFieldNames;
this.concurrencyLevel = concurrencyLevel;
this.bufferSizeLimitBytes = bufferSizeLimitBytes;
this.codecRegistry = codecRegistry;
@@ -114,6 +120,17 @@ public final class JsonConfig {
return maxDepth;
}
+ public int maxCachedFieldNames() {
+ return maxCachedFieldNames;
+ }
+
+ static void validateMaxCachedFieldNames(int maxCachedFieldNames) {
+ if (maxCachedFieldNames < 0 || maxCachedFieldNames >
MAX_CACHED_FIELD_NAMES) {
+ throw new IllegalArgumentException(
+ "maxCachedFieldNames must be between 0 and " +
MAX_CACHED_FIELD_NAMES);
+ }
+ }
+
public int concurrencyLevel() {
return concurrencyLevel;
}
@@ -150,6 +167,7 @@ public final class JsonConfig {
&& propertyNamingStrategy == that.propertyNamingStrategy
&& classLoader == that.classLoader
&& maxDepth == that.maxDepth
+ && maxCachedFieldNames == that.maxCachedFieldNames
&& concurrencyLevel == that.concurrencyLevel
&& bufferSizeLimitBytes == that.bufferSizeLimitBytes
&& typeChecker == that.typeChecker
@@ -165,6 +183,7 @@ public final class JsonConfig {
result = 31 * result + propertyNamingStrategy.hashCode();
result = 31 * result + System.identityHashCode(classLoader);
result = 31 * result + maxDepth;
+ result = 31 * result + maxCachedFieldNames;
result = 31 * result + concurrencyLevel;
result = 31 * result + bufferSizeLimitBytes;
result = 31 * result + System.identityHashCode(typeChecker);
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
index 5b4f8ad12..305df441b 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/MapCodec.java
@@ -75,6 +75,11 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
public Object fromName(String name) {
return name;
}
+
+ @Override
+ public Object readName(JsonReader reader) {
+ return reader.readFieldName();
+ }
};
private static final MapKeyCodec OBJECT_KEY_CODEC =
new MapKeyCodec() {
@@ -96,6 +101,11 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
public Object fromName(String name) {
return name;
}
+
+ @Override
+ public Object readName(JsonReader reader) {
+ return reader.readFieldName();
+ }
};
private final MapFactory factory;
@@ -184,7 +194,7 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
reader.expectNextToken('{');
if (!reader.consumeNextToken('}')) {
do {
- Object key = STRING_KEY_CODEC.readName(reader);
+ Object key = reader.readFieldName();
reader.expectNextToken(':');
map.put(key, codec.readLatin1(reader));
} while (reader.consumeNextToken(','));
@@ -202,7 +212,7 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
reader.expectNextToken('{');
if (!reader.consumeNextToken('}')) {
do {
- Object key = STRING_KEY_CODEC.readName(reader);
+ Object key = reader.readFieldName();
reader.expectNextToken(':');
map.put(key, codec.readUtf16(reader));
} while (reader.consumeNextToken(','));
@@ -220,7 +230,7 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
reader.expectNextToken('{');
if (!reader.consumeNextToken('}')) {
do {
- Object key = STRING_KEY_CODEC.readName(reader);
+ Object key = reader.readFieldName();
reader.expectNextToken(':');
map.put(key, codec.readUtf8(reader));
} while (reader.consumeNextToken(','));
@@ -470,7 +480,7 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
reader.expectNextToken('{');
if (!reader.consumeNextToken('}')) {
do {
- String key = reader.readString();
+ String key = reader.readFieldName();
reader.expectNextToken(':');
map.put(key, readLatin1Value(reader));
} while (reader.consumeNextToken(','));
@@ -490,7 +500,7 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
reader.expectNextToken('{');
if (!reader.consumeNextToken('}')) {
do {
- String key = reader.readString();
+ String key = reader.readFieldName();
reader.expectNextToken(':');
map.put(key, readUtf16Value(reader));
} while (reader.consumeNextToken(','));
@@ -510,7 +520,7 @@ public abstract class MapCodec<T extends Map<?, ?>>
implements JsonValueCodec<T>
reader.expectNextToken('{');
if (!reader.consumeNextToken('}')) {
do {
- String key = reader.readString();
+ String key = reader.readFieldName();
reader.expectNextToken(':');
map.put(key, readUtf8Value(reader));
} while (reader.consumeNextToken(','));
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/reader/FieldNameCache.java
b/java/fory-json/src/main/java/org/apache/fory/json/reader/FieldNameCache.java
new file mode 100644
index 000000000..ce1ae13f9
--- /dev/null
+++
b/java/fory-json/src/main/java/org/apache/fory/json/reader/FieldNameCache.java
@@ -0,0 +1,123 @@
+/*
+ * 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.reader;
+
+import java.util.Objects;
+import org.apache.fory.json.resolver.JsonSharedRegistry.CachedFieldName;
+
+/** Fixed two-candidate cache of shared field-name entries owned by one JSON
reader. */
+final class FieldNameCache {
+ private final int maxEntries;
+ private final int mask;
+ private long[] hashes;
+ private CachedFieldName[] entries;
+ private int size;
+
+ FieldNameCache(int maxEntries) {
+ // JsonConfig owns range validation; its upper bound keeps this doubled
slot count positive.
+ this.maxEntries = maxEntries;
+ int requiredSlots = maxEntries << 1;
+ int slots = Integer.highestOneBit(requiredSlots - 1) << 1;
+ mask = slots - 1;
+ }
+
+ CachedFieldName get(long hash) {
+ CachedFieldName[] localEntries = entries;
+ if (localEntries == null) {
+ return null;
+ }
+ int home = slot(hash);
+ CachedFieldName entry = localEntries[home];
+ if (entry == null) {
+ // Entries are never removed or replaced, so a hash cannot occupy the
adjacent slot while
+ // its home remains empty.
+ return null;
+ }
+ if (hashes[home] == hash) {
+ return entry;
+ }
+ int adjacent = (home + 1) & mask;
+ entry = localEntries[adjacent];
+ return entry != null && hashes[adjacent] == hash ? entry : null;
+ }
+
+ boolean canPut(long hash) {
+ if (size >= maxEntries) {
+ return false;
+ }
+ CachedFieldName[] localEntries = entries;
+ if (localEntries == null) {
+ return true;
+ }
+ int home = slot(hash);
+ CachedFieldName entry = localEntries[home];
+ if (entry == null) {
+ return true;
+ }
+ if (hashes[home] == hash) {
+ return false;
+ }
+ int adjacent = (home + 1) & mask;
+ entry = localEntries[adjacent];
+ return entry == null;
+ }
+
+ void put(long hash, CachedFieldName entry) {
+ Objects.requireNonNull(entry, "entry");
+ CachedFieldName[] localEntries = entries;
+ if (localEntries == null) {
+ hashes = new long[mask + 1];
+ localEntries = new CachedFieldName[mask + 1];
+ entries = localEntries;
+ }
+ int home = slot(hash);
+ CachedFieldName cached = localEntries[home];
+ if (cached == null) {
+ if (size < maxEntries) {
+ hashes[home] = hash;
+ localEntries[home] = entry;
+ size++;
+ }
+ return;
+ }
+ if (hashes[home] == hash) {
+ return;
+ }
+ int adjacent = (home + 1) & mask;
+ cached = localEntries[adjacent];
+ if (cached == null) {
+ if (size < maxEntries) {
+ hashes[adjacent] = hash;
+ localEntries[adjacent] = entry;
+ size++;
+ }
+ return;
+ }
+ if (hashes[adjacent] == hash) {
+ return;
+ }
+ }
+
+ private int slot(long hash) {
+ int spread = (int) (hash ^ (hash >>> 32));
+ spread ^= spread >>> 16;
+ return spread & mask;
+ }
+}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
index 625f3046f..0e27da04a 100644
--- a/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
+++ b/java/fory-json/src/main/java/org/apache/fory/json/reader/JsonReader.java
@@ -135,6 +135,7 @@ public abstract class JsonReader {
private final JsonTypeResolver typeResolver;
protected int position;
private final int maxDepth;
+ protected final FieldNameCache fieldNameCache;
private int depth;
/**
@@ -233,6 +234,9 @@ public abstract class JsonReader {
protected JsonReader(JsonConfig config, JsonTypeResolver typeResolver) {
this.typeResolver = Objects.requireNonNull(typeResolver, "typeResolver");
maxDepth = config.maxDepth();
+ // The configured limit belongs to each reader; pooled-state concurrency
must not divide it.
+ int maxEntries = config.maxCachedFieldNames();
+ fieldNameCache = maxEntries == 0 ? null : new FieldNameCache(maxEntries);
}
/**
@@ -248,6 +252,32 @@ public abstract class JsonReader {
public abstract String readString();
+ /**
+ * Reads a JSON object field name that the caller retains as a String key.
+ *
+ * <p>The returned String has normal value semantics. Implementations may
reuse its reference for
+ * common names, but callers must not depend on reference identity.
+ */
+ public abstract String readFieldName();
+
+ protected static long fieldNameHash(int length, long word0, long word1) {
+ if (length == 0) {
+ return JsonFieldNameHash.MAGIC_HASH_CODE;
+ }
+ if (length <= Long.BYTES) {
+ return word0;
+ }
+ long hash = JsonFieldNameHash.hashPacked(word0, Long.BYTES);
+ for (int i = Long.BYTES; i < length; i++) {
+ hash = JsonFieldNameHash.update(hash, (char) ((word1 >>> ((i -
Long.BYTES) << 3)) & 0xff));
+ }
+ return hash;
+ }
+
+ protected static long fieldNameWord(long word, int length) {
+ return length == Long.BYTES ? word : word & ((1L << (length << 3)) - 1);
+ }
+
@Internal
public final int position() {
return position;
@@ -257,7 +287,7 @@ public abstract class JsonReader {
public final String materializeFieldName(int start) {
int current = position;
position = start;
- String name = readString();
+ String name = readFieldName();
position = current;
return name;
}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
index 9f2ab035e..23895d0c1 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Latin1JsonReader.java
@@ -30,6 +30,8 @@ import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldNameHash;
import org.apache.fory.json.meta.JsonFieldTable;
import org.apache.fory.json.meta.JsonSubtypeScanInfo;
+import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.json.resolver.JsonSharedRegistry.CachedFieldName;
import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.memory.LittleEndian;
import org.apache.fory.memory.NativeByteOrder;
@@ -1605,6 +1607,148 @@ public final class Latin1JsonReader extends JsonReader {
return readStringToken();
}
+ @Override
+ public String readFieldName() {
+ FieldNameCache cache = fieldNameCache;
+ if (cache == null) {
+ return readString();
+ }
+ return readCachedFieldName(cache);
+ }
+
+ private String readCachedFieldName(FieldNameCache cache) {
+ skipWhitespaceFast();
+ byte[] bytes = input;
+ int inputLength = bytes.length;
+ if (position >= inputLength || bytes[position++] != '"') {
+ throw error("Expected string");
+ }
+ int start = position;
+ if (start + Long.BYTES <= inputLength) {
+ long word0 = LittleEndian.getInt64(bytes, start);
+ long stopMask = asciiStringStopMask(word0);
+ if (stopMask != 0) {
+ int length = Long.numberOfTrailingZeros(stopMask) >>> 3;
+ int stop = start + length;
+ int ch = bytes[stop];
+ if (ch != '"') {
+ return readStringStop(start, stop, ch);
+ }
+ position = stop + 1;
+ word0 = fieldNameWord(word0, length);
+ long hash = length == 0 ? JsonFieldNameHash.MAGIC_HASH_CODE : word0;
+ CachedFieldName entry = cache.get(hash);
+ if (entry != null) {
+ return entry.matches(length, word0, 0) ? entry.name() :
newLatin1String(start, stop);
+ }
+ if (!cache.canPut(hash)) {
+ return newLatin1String(start, stop);
+ }
+ return readFieldNameMiss(cache, start, stop, length, word0, 0, hash);
+ }
+ return readFieldNameAfterWord0(cache, start, word0, inputLength);
+ }
+ return readFieldNameTail(cache, start, start, 0, 0, 0);
+ }
+
+ private String readFieldNameAfterWord0(
+ FieldNameCache cache, int start, long word0, int inputLength) {
+ int offset = start + Long.BYTES;
+ if (offset + Long.BYTES <= inputLength) {
+ long word1 = LittleEndian.getInt64(input, offset);
+ long stopMask = asciiStringStopMask(word1);
+ if (stopMask != 0) {
+ int length = Long.numberOfTrailingZeros(stopMask) >>> 3;
+ int stop = offset + length;
+ int ch = input[stop];
+ if (ch != '"') {
+ return readStringStop(start, stop, ch);
+ }
+ position = stop + 1;
+ return resolveFieldName(
+ cache, start, stop, Long.BYTES + length, word0,
fieldNameWord(word1, length));
+ }
+ offset += Long.BYTES;
+ if (offset < inputLength && input[offset] == '"') {
+ position = offset + 1;
+ return resolveFieldName(cache, start, offset, 16, word0, word1);
+ }
+ // Keep the beyond-cache-width scanner out of the short-name and
ordinary-string hot paths.
+ return readLongFieldName(start, offset, inputLength);
+ }
+ return readFieldNameTail(cache, start, offset, Long.BYTES, word0, 0);
+ }
+
+ private String readLongFieldName(int start, int offset, int inputLength) {
+ byte[] bytes = input;
+ int wordEnd = inputLength - Long.BYTES;
+ while (offset <= wordEnd) {
+ long stopMask = asciiStringStopMask(LittleEndian.getInt64(bytes,
offset));
+ if (stopMask == 0) {
+ offset += Long.BYTES;
+ continue;
+ }
+ int stop = offset + (Long.numberOfTrailingZeros(stopMask) >>> 3);
+ int ch = bytes[stop];
+ if (ch == '"') {
+ position = stop + 1;
+ return newLatin1String(start, stop);
+ }
+ return readStringStop(start, stop, ch);
+ }
+ return readStringTokenTail(start, offset, inputLength);
+ }
+
+ private String readFieldNameTail(
+ FieldNameCache cache, int start, int offset, int length, long word0,
long word1) {
+ int inputLength = input.length;
+ while (offset < inputLength) {
+ int ch = input[offset] & 0xff;
+ if (ch == '"') {
+ position = offset + 1;
+ return resolveFieldName(cache, start, offset, length, word0, word1);
+ }
+ if (ch == '\\' || ch >= 0x80 || ch < 0x20) {
+ return readStringStop(start, offset, input[offset]);
+ }
+ if (length < Long.BYTES) {
+ word0 |= ((long) ch) << (length << 3);
+ } else {
+ word1 |= ((long) ch) << ((length - Long.BYTES) << 3);
+ }
+ length++;
+ offset++;
+ }
+ throw error("Unterminated string");
+ }
+
+ private String resolveFieldName(
+ FieldNameCache cache, int start, int end, int length, long word0, long
word1) {
+ long hash = fieldNameHash(length, word0, word1);
+ CachedFieldName entry = cache.get(hash);
+ if (entry != null) {
+ return entry.matches(length, word0, word1) ? entry.name() :
newLatin1String(start, end);
+ }
+ if (!cache.canPut(hash)) {
+ return newLatin1String(start, end);
+ }
+ return readFieldNameMiss(cache, start, end, length, word0, word1, hash);
+ }
+
+ private String readFieldNameMiss(
+ FieldNameCache cache, int start, int end, int length, long word0, long
word1, long hash) {
+ JsonSharedRegistry registry = typeResolver().sharedRegistry();
+ CachedFieldName entry = registry.cachedFieldName(hash);
+ if (entry != null) {
+ cache.put(hash, entry);
+ return entry.matches(length, word0, word1) ? entry.name() :
newLatin1String(start, end);
+ }
+ String candidate = newLatin1String(start, end);
+ entry = registry.cacheFieldName(hash, candidate, word0, word1);
+ cache.put(hash, entry);
+ return entry.matches(length, word0, word1) ? entry.name() : candidate;
+ }
+
@Override
public String readNullableString() {
skipWhitespaceFast();
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
index 38e96824a..65f18223b 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf16JsonReader.java
@@ -29,6 +29,8 @@ import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldNameHash;
import org.apache.fory.json.meta.JsonFieldTable;
import org.apache.fory.json.meta.JsonSubtypeScanInfo;
+import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.json.resolver.JsonSharedRegistry.CachedFieldName;
import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.memory.LittleEndian;
import org.apache.fory.memory.NativeByteOrder;
@@ -63,6 +65,7 @@ public final class Utf16JsonReader extends JsonReader {
private static final long UTF16_BACKSLASH_CHARS = 0x005c005c005c005cL;
private static final long UTF16_CONTROL_LIMIT = 0x0020002000200020L;
private static final long UTF16_NON_LATIN_BYTES = 0xFF00FF00FF00FF00L;
+ private static final long UTF16_NON_ASCII_BITS = 0xFF80FF80FF80FF80L;
private static final long UTF16_SURROGATE_MASK = 0xf800f800f800f800L;
private static final long UTF16_SURROGATE_PREFIX = 0xd800d800d800d800L;
private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10;
@@ -1525,6 +1528,176 @@ public final class Utf16JsonReader extends JsonReader {
return readStringToken();
}
+ @Override
+ public String readFieldName() {
+ FieldNameCache cache = fieldNameCache;
+ if (cache == null) {
+ return readString();
+ }
+ return readCachedFieldName(cache);
+ }
+
+ private String readCachedFieldName(FieldNameCache cache) {
+ skipWhitespaceFast();
+ if (position >= length || charAtFast(position++) != '"') {
+ throw error("Expected string");
+ }
+ int start = position;
+ if (LITTLE_ENDIAN && bytes != null) {
+ return readFieldNameWords(cache, start);
+ }
+ return readFieldNameTail(cache, start, start, 0, 0, 0);
+ }
+
+ private String readFieldNameWords(FieldNameCache cache, int start) {
+ byte[] localBytes = bytes;
+ int inputLength = length;
+ if (start + 4 > inputLength) {
+ return readFieldNameTail(cache, start, start, 0, 0, 0);
+ }
+ long word = LittleEndian.getInt64(localBytes, start << 1);
+ long stopMask =
+ utf16StringStopMask(word, word & UTF16_NON_LATIN_BYTES) | (word &
UTF16_NON_ASCII_BITS);
+ if (stopMask != 0) {
+ int chars = Long.numberOfTrailingZeros(stopMask) >>> 4;
+ int stop = start + chars;
+ if (charAtFast(stop) != '"') {
+ return continueFieldName(start, stop);
+ }
+ position = stop + 1;
+ return resolveShortFieldName(
+ cache, start, stop, chars, fieldNameWord(packAsciiUtf16(word),
chars));
+ }
+ long word0 = packAsciiUtf16(word);
+ int offset = start + 4;
+ if (offset + 4 > inputLength) {
+ return readFieldNameTail(cache, start, offset, 4, word0, 0);
+ }
+ word = LittleEndian.getInt64(localBytes, offset << 1);
+ stopMask =
+ utf16StringStopMask(word, word & UTF16_NON_LATIN_BYTES) | (word &
UTF16_NON_ASCII_BITS);
+ if (stopMask != 0) {
+ int chars = Long.numberOfTrailingZeros(stopMask) >>> 4;
+ int stop = offset + chars;
+ if (charAtFast(stop) != '"') {
+ return continueFieldName(start, stop);
+ }
+ position = stop + 1;
+ word0 |= fieldNameWord(packAsciiUtf16(word), chars) << 32;
+ return resolveShortFieldName(cache, start, stop, 4 + chars, word0);
+ }
+ word0 |= packAsciiUtf16(word) << 32;
+ return readFieldNameAfterWord0(cache, start, offset + 4, word0);
+ }
+
+ private String readFieldNameAfterWord0(FieldNameCache cache, int start, int
offset, long word0) {
+ byte[] localBytes = bytes;
+ long word1 = 0;
+ int nameLength = Long.BYTES;
+ while (nameLength < 16 && offset + 4 <= length) {
+ long word = LittleEndian.getInt64(localBytes, offset << 1);
+ long stopMask =
+ utf16StringStopMask(word, word & UTF16_NON_LATIN_BYTES) | (word &
UTF16_NON_ASCII_BITS);
+ if (stopMask != 0) {
+ int chars = Long.numberOfTrailingZeros(stopMask) >>> 4;
+ int stop = offset + chars;
+ char ch = charAtFast(stop);
+ if (ch != '"') {
+ return continueFieldName(start, stop);
+ }
+ position = stop + 1;
+ long packed = fieldNameWord(packAsciiUtf16(word), chars);
+ word1 |= packed << ((nameLength - Long.BYTES) << 3);
+ return resolveFieldName(cache, start, stop, nameLength + chars, word0,
word1);
+ }
+ long packed = packAsciiUtf16(word);
+ word1 |= packed << ((nameLength - Long.BYTES) << 3);
+ nameLength += 4;
+ offset += 4;
+ }
+ if (nameLength == 16) {
+ if (offset < length && charAtFast(offset) == '"') {
+ position = offset + 1;
+ return resolveFieldName(cache, start, offset, nameLength, word0,
word1);
+ }
+ return continueFieldName(start, offset);
+ }
+ return readFieldNameTail(cache, start, offset, nameLength, word0, word1);
+ }
+
+ private String resolveShortFieldName(
+ FieldNameCache cache, int start, int end, int nameLength, long word0) {
+ long hash = nameLength == 0 ? JsonFieldNameHash.MAGIC_HASH_CODE : word0;
+ CachedFieldName entry = cache.get(hash);
+ if (entry != null) {
+ return entry.matches(nameLength, word0, 0) ? entry.name() :
input.substring(start, end);
+ }
+ if (!cache.canPut(hash)) {
+ return input.substring(start, end);
+ }
+ return readFieldNameMiss(cache, start, end, nameLength, word0, 0, hash);
+ }
+
+ private String readFieldNameTail(
+ FieldNameCache cache, int start, int offset, int nameLength, long word0,
long word1) {
+ while (offset < length) {
+ char ch = charAtFast(offset);
+ if (ch == '"') {
+ position = offset + 1;
+ return resolveFieldName(cache, start, offset, nameLength, word0,
word1);
+ }
+ if (ch == '\\' || ch > 0x7f || ch < 0x20) {
+ return continueFieldName(start, offset);
+ }
+ if (nameLength == 16) {
+ return continueFieldName(start, offset);
+ }
+ if (nameLength < Long.BYTES) {
+ word0 |= ((long) ch) << (nameLength << 3);
+ } else {
+ word1 |= ((long) ch) << ((nameLength - Long.BYTES) << 3);
+ }
+ nameLength++;
+ offset++;
+ }
+ throw error("Unterminated string");
+ }
+
+ private String continueFieldName(int start, int offset) {
+ position = offset;
+ if (LITTLE_ENDIAN && bytes != null) {
+ return readUtf16StringToken(start, offset, false);
+ }
+ return readStringLoop(start, false);
+ }
+
+ private String readFieldNameMiss(
+ FieldNameCache cache, int start, int end, int length, long word0, long
word1, long hash) {
+ JsonSharedRegistry registry = typeResolver().sharedRegistry();
+ CachedFieldName entry = registry.cachedFieldName(hash);
+ if (entry != null) {
+ cache.put(hash, entry);
+ return entry.matches(length, word0, word1) ? entry.name() :
input.substring(start, end);
+ }
+ String candidate = input.substring(start, end);
+ entry = registry.cacheFieldName(hash, candidate, word0, word1);
+ cache.put(hash, entry);
+ return entry.matches(length, word0, word1) ? entry.name() : candidate;
+ }
+
+ private String resolveFieldName(
+ FieldNameCache cache, int start, int end, int length, long word0, long
word1) {
+ long hash = fieldNameHash(length, word0, word1);
+ CachedFieldName entry = cache.get(hash);
+ if (entry != null) {
+ return entry.matches(length, word0, word1) ? entry.name() :
input.substring(start, end);
+ }
+ if (!cache.canPut(hash)) {
+ return input.substring(start, end);
+ }
+ return readFieldNameMiss(cache, start, end, length, word0, word1, hash);
+ }
+
@Override
public String readNullableString() {
skipWhitespaceFast();
@@ -1597,16 +1770,14 @@ public final class Utf16JsonReader extends JsonReader {
private String readStringAfterQuote() {
int start = position;
if (LITTLE_ENDIAN && bytes != null) {
- return readUtf16StringToken(start);
+ return readUtf16StringToken(start, start, false);
}
return readStringLoop(start, false);
}
- private String readUtf16StringToken(int start) {
+ private String readUtf16StringToken(int start, int offset, boolean nonLatin)
{
byte[] localBytes = bytes;
int inputLength = length;
- int offset = start;
- boolean nonLatin = false;
int doubleWordEnd = inputLength - 8;
while (offset <= doubleWordEnd) {
long word = LittleEndian.getInt64(localBytes, offset << 1);
@@ -2415,6 +2586,13 @@ public final class Utf16JsonReader extends JsonReader {
return LITTLE_ENDIAN ? word : word << 8;
}
+ private static long packAsciiUtf16(long word) {
+ return (word & 0xff)
+ | ((word >>> 8) & 0xff00)
+ | ((word >>> 16) & 0xff0000)
+ | ((word >>> 24) & 0xff000000L);
+ }
+
private static long utf16WordMask(int length) {
return length == 4 ? -1L : (1L << (length << 4)) - 1;
}
diff --git
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
index fe0d181ef..3aaa77900 100644
---
a/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
+++
b/java/fory-json/src/main/java/org/apache/fory/json/reader/Utf8JsonReader.java
@@ -30,6 +30,8 @@ import org.apache.fory.json.meta.JsonFieldInfo;
import org.apache.fory.json.meta.JsonFieldNameHash;
import org.apache.fory.json.meta.JsonFieldTable;
import org.apache.fory.json.meta.JsonSubtypeScanInfo;
+import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.json.resolver.JsonSharedRegistry.CachedFieldName;
import org.apache.fory.json.resolver.JsonTypeResolver;
import org.apache.fory.memory.LittleEndian;
import org.apache.fory.memory.NativeByteOrder;
@@ -1698,6 +1700,127 @@ public final class Utf8JsonReader extends JsonReader {
return readStringToken();
}
+ @Override
+ public String readFieldName() {
+ FieldNameCache cache = fieldNameCache;
+ if (cache == null) {
+ return readString();
+ }
+ return readCachedFieldName(cache);
+ }
+
+ private String readCachedFieldName(FieldNameCache cache) {
+ skipWhitespaceFast();
+ byte[] bytes = input;
+ int inputLength = bytes.length;
+ if (position >= inputLength || bytes[position++] != '"') {
+ throw error("Expected string");
+ }
+ int start = position;
+ if (start + Long.BYTES <= inputLength) {
+ long word0 = LittleEndian.getInt64(bytes, start);
+ long stopMask = stringStopMask(word0);
+ if (stopMask != 0) {
+ int length = Long.numberOfTrailingZeros(stopMask) >>> 3;
+ int stop = start + length;
+ int b = bytes[stop];
+ if (b != '"') {
+ return readStringStop(start, stop, b);
+ }
+ position = stop + 1;
+ word0 = fieldNameWord(word0, length);
+ long hash = length == 0 ? JsonFieldNameHash.MAGIC_HASH_CODE : word0;
+ CachedFieldName entry = cache.get(hash);
+ if (entry != null) {
+ return entry.matches(length, word0, 0) ? entry.name() :
newLatin1String(start, stop);
+ }
+ if (!cache.canPut(hash)) {
+ return newLatin1String(start, stop);
+ }
+ return readFieldNameMiss(cache, start, stop, length, word0, 0, hash);
+ }
+ return readFieldNameAfterWord0(cache, start, word0, inputLength);
+ }
+ return readFieldNameTail(cache, start, start, 0, 0, 0);
+ }
+
+ private String readFieldNameAfterWord0(
+ FieldNameCache cache, int start, long word0, int inputLength) {
+ int offset = start + Long.BYTES;
+ if (offset + Long.BYTES <= inputLength) {
+ long word1 = LittleEndian.getInt64(input, offset);
+ long stopMask = stringStopMask(word1);
+ if (stopMask != 0) {
+ int length = Long.numberOfTrailingZeros(stopMask) >>> 3;
+ int stop = offset + length;
+ int b = input[stop];
+ if (b != '"') {
+ return readStringStop(start, stop, b);
+ }
+ position = stop + 1;
+ return resolveFieldName(
+ cache, start, stop, Long.BYTES + length, word0,
fieldNameWord(word1, length));
+ }
+ offset += Long.BYTES;
+ if (offset < inputLength && input[offset] == '"') {
+ position = offset + 1;
+ return resolveFieldName(cache, start, offset, 16, word0, word1);
+ }
+ return readStringTokenLongTail(start, offset, inputLength);
+ }
+ return readFieldNameTail(cache, start, offset, Long.BYTES, word0, 0);
+ }
+
+ private String readFieldNameTail(
+ FieldNameCache cache, int start, int offset, int length, long word0,
long word1) {
+ int inputLength = input.length;
+ while (offset < inputLength) {
+ int ch = input[offset] & 0xff;
+ if (ch == '"') {
+ position = offset + 1;
+ return resolveFieldName(cache, start, offset, length, word0, word1);
+ }
+ if (ch == '\\' || ch >= 0x80 || ch < 0x20) {
+ return readStringStop(start, offset, input[offset]);
+ }
+ if (length < Long.BYTES) {
+ word0 |= ((long) ch) << (length << 3);
+ } else {
+ word1 |= ((long) ch) << ((length - Long.BYTES) << 3);
+ }
+ length++;
+ offset++;
+ }
+ throw error("Unterminated string");
+ }
+
+ private String resolveFieldName(
+ FieldNameCache cache, int start, int end, int length, long word0, long
word1) {
+ long hash = fieldNameHash(length, word0, word1);
+ CachedFieldName entry = cache.get(hash);
+ if (entry != null) {
+ return entry.matches(length, word0, word1) ? entry.name() :
newLatin1String(start, end);
+ }
+ if (!cache.canPut(hash)) {
+ return newLatin1String(start, end);
+ }
+ return readFieldNameMiss(cache, start, end, length, word0, word1, hash);
+ }
+
+ private String readFieldNameMiss(
+ FieldNameCache cache, int start, int end, int length, long word0, long
word1, long hash) {
+ JsonSharedRegistry registry = typeResolver().sharedRegistry();
+ CachedFieldName entry = registry.cachedFieldName(hash);
+ if (entry != null) {
+ cache.put(hash, entry);
+ return entry.matches(length, word0, word1) ? entry.name() :
newLatin1String(start, end);
+ }
+ String candidate = newLatin1String(start, end);
+ entry = registry.cacheFieldName(hash, candidate, word0, word1);
+ cache.put(hash, entry);
+ return entry.matches(length, word0, word1) ? entry.name() : candidate;
+ }
+
@Override
public String readNullableString() {
skipWhitespaceFast();
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 3c1da0bfc..52d0e6025 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
@@ -138,9 +138,12 @@ import org.apache.fory.util.record.RecordUtils;
*
* <p>Accepted type-check results are cached by class name up to a bounded
8192-entry shared cache.
* Once full, new names are checked on every resolution rather than growing
attacker-controlled
- * state. Source-generated model companions and JIT-generated classes are
shared here; concrete JIT
- * codec instances, ordinary type bindings, JIT locks, and callbacks remain
resolver-local. A fresh
- * generic {@link JsonJITContext} is therefore created for every pooled JSON
state.
+ * state. Common short field names admitted by reader-local caches are
published here for
+ * best-effort String reference reuse across readers. Reader-local admission
is the only field-name
+ * capacity gate; the shared field-name map has no explicit limit.
Source-generated model companions
+ * and JIT-generated classes are shared here; concrete JIT codec instances,
ordinary type bindings,
+ * JIT locks, and callbacks remain resolver-local. A fresh generic {@link
JsonJITContext} is
+ * therefore created for every pooled JSON state.
*/
public final class JsonSharedRegistry {
private static final int TYPE_CHECK_CACHE_LIMIT = 8192;
@@ -179,6 +182,7 @@ 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<Long, CachedFieldName> cachedFieldNames;
public JsonSharedRegistry(JsonConfig config) {
this(config, null);
@@ -206,6 +210,7 @@ public final class JsonSharedRegistry {
mapKeyCodecs = new ConcurrentHashMap<>();
generatedCodecs = new ConcurrentHashMap<>();
typesWithoutGeneratedCodec = ConcurrentHashMap.newKeySet();
+ cachedFieldNames = new ConcurrentHashMap<>();
boolean codegenEnabled = config.codegenEnabled();
codegen = codegenEnabled ? new JsonCodegen(config.getCodegenHash(),
classLoader) : null;
asyncCompilationEnabled = codegenEnabled &&
config.asyncCompilationEnabled();
@@ -213,6 +218,42 @@ public final class JsonSharedRegistry {
registerExactCodecs();
}
+ /** Returns the immutable cached entry for {@code hash}, or null when none
was published. */
+ @Internal
+ public CachedFieldName cachedFieldName(long hash) {
+ return cachedFieldNames.get(hash);
+ }
+
+ /** Publishes one already validated short ASCII field name, or returns the
existing hash owner. */
+ @Internal
+ public CachedFieldName cacheFieldName(long hash, String name, long word0,
long word1) {
+ CachedFieldName candidate = new CachedFieldName(name, word0, word1);
+ CachedFieldName existing = cachedFieldNames.putIfAbsent(hash, candidate);
+ return existing == null ? candidate : existing;
+ }
+
+ /** Immutable field-name hash owner retained for best-effort cross-reader
reference reuse. */
+ @Internal
+ public static final class CachedFieldName {
+ private final String name;
+ private final long word0;
+ private final long word1;
+
+ private CachedFieldName(String name, long word0, long word1) {
+ this.name = name;
+ this.word0 = word0;
+ this.word1 = word1;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public boolean matches(int length, long candidateWord0, long
candidateWord1) {
+ return name.length() == length && word0 == candidateWord0 && word1 ==
candidateWord1;
+ }
+ }
+
GeneratedJsonCodec<?> generatedCodec(Class<?> type) {
if (type.getDeclaredAnnotation(JsonType.class) == null) {
return null;
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 6717af985..3f66da527 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
@@ -110,6 +110,12 @@ public final class JsonTypeResolver {
canonicalObjectTypeInfos = new IdentityMap<>();
}
+ /** Returns the shared registry that owns this resolver and its reader cache
domain. */
+ @Internal
+ public JsonSharedRegistry sharedRegistry() {
+ return sharedRegistry;
+ }
+
@Internal
public void lockJIT() {
jitContext.lock();
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/JsonAnyPropertyTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/JsonAnyPropertyTest.java
index 2bdfeb111..a2a62fe24 100644
--- a/java/fory-json/src/test/java/org/apache/fory/json/JsonAnyPropertyTest.java
+++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonAnyPropertyTest.java
@@ -121,6 +121,19 @@ public class JsonAnyPropertyTest extends
ForyJsonTestModels {
assertGeneratedCapabilities(json, FinalAny.class);
}
+ @Test
+ public void retainedKeyIdentity() {
+ ForyJson json = newJson();
+ MutableAny latin1 = json.fromJson("{\"common\":1}", MutableAny.class);
+ String canonical = latin1.properties.keySet().iterator().next();
+
+ MutableAny utf16 = json.fromJson("{\"common\":2,\"键\":3}",
MutableAny.class);
+ MutableAny utf8 =
+ json.fromJson("{\"common\":4}".getBytes(StandardCharsets.UTF_8),
MutableAny.class);
+ assertSame(utf16.properties.keySet().iterator().next(), canonical);
+ assertSame(utf8.properties.keySet().iterator().next(), canonical);
+ }
+
@Test
public void directionalField() {
ForyJson json = newJson();
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 1f927bce7..2aabfb2a2 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
@@ -878,6 +878,7 @@ public class JsonAsyncCompilationTest {
PropertyNamingStrategy.LOWER_CAMEL_CASE,
JsonAsyncCompilationTest.class.getClassLoader(),
ForyJson.DEFAULT_MAX_DEPTH,
+ ForyJson.DEFAULT_MAX_CACHED_FIELD_NAMES,
1,
2 * 1024 * 1024,
codecs,
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
new file mode 100644
index 000000000..a10c9768b
--- /dev/null
+++
b/java/fory-json/src/test/java/org/apache/fory/json/JsonFieldNameCacheTest.java
@@ -0,0 +1,481 @@
+/*
+ * 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 static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+import org.apache.fory.json.meta.JsonFieldNameHash;
+import org.apache.fory.json.reader.Latin1JsonReader;
+import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.json.resolver.JsonSharedRegistry.CachedFieldName;
+import org.apache.fory.reflect.TypeRef;
+import org.testng.annotations.Test;
+
+public class JsonFieldNameCacheTest {
+ @Test
+ public void configuration() {
+ JsonConfig defaults = JsonTestSupport.config(ForyJson.builder().build());
+ assertEquals(defaults.maxCachedFieldNames(),
ForyJson.DEFAULT_MAX_CACHED_FIELD_NAMES);
+ assertThrows(
+ IllegalArgumentException.class, () ->
ForyJson.builder().withMaxCachedFieldNames(-1));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ForyJson.builder().withMaxCachedFieldNames(Integer.MAX_VALUE));
+
+ JsonConfig first =
+ JsonTestSupport.config(
+
ForyJson.builder().withConcurrencyLevel(1).withMaxCachedFieldNames(1).build());
+ JsonConfig second =
+ JsonTestSupport.config(
+
ForyJson.builder().withConcurrencyLevel(1).withMaxCachedFieldNames(2).build());
+ assertNotEquals(first, second);
+ assertNotEquals(first.hashCode(), second.hashCode());
+ assertEquals(first.getCodegenHash(), second.getCodegenHash());
+ assertEquals(first.maxCachedFieldNames(), 1);
+ assertEquals(second.maxCachedFieldNames(), 2);
+ assertEquals(JsonTestSupport.config(newJson(0)).maxCachedFieldNames(), 0);
+ assertEquals(ForyJson.DEFAULT_MAX_CACHED_FIELD_NAMES, 8192);
+ }
+
+ @Test
+ public void representations() {
+ ForyJson json = newJson(64);
+ String latin1 = firstKey(json.fromJson("{\"shared\":1}",
JsonObject.class));
+ String utf16 = firstKey(json.fromJson("{\"shared\":\"你好\"}",
JsonObject.class));
+ String utf8 =
+ firstKey(
+ json.fromJson("{\"shared\":1}".getBytes(StandardCharsets.UTF_8),
JsonObject.class));
+ assertSame(utf16, latin1);
+ assertSame(utf8, latin1);
+ }
+
+ @Test
+ public void retainedKeyPaths() {
+ ForyJson json = newJson(64);
+ String canonical = firstKey(json.fromJson("{\"common\":1}",
JsonObject.class));
+
+ Object dynamic = json.fromJson("{\"common\":1}", Object.class);
+ assertSame(firstKey((Map<?, ?>) dynamic), canonical);
+ assertSame(
+ firstKey(json.fromJson("{\"common\":1}", new TypeRef<Map<String,
Integer>>() {})),
+ canonical);
+ assertSame(
+ firstKey(json.fromJson("{\"common\":\"v\"}", new TypeRef<Map<String,
String>>() {})),
+ canonical);
+ Map<String, TypedFields> generic =
+ json.fromJson("{\"common\":{\"value\":1}}", new TypeRef<Map<String,
TypedFields>>() {});
+ assertSame(firstKey(generic), canonical);
+ assertEquals(generic.get("common").value, 1);
+ assertSame(
+ firstKey(json.fromJson("{\"common\":1}", new TypeRef<Map<Object,
Object>>() {})),
+ canonical);
+ }
+
+ @Test
+ public void convertedKeysAreNotCached() {
+ ForyJson json = newJson(64);
+ Map<Integer, Integer> values =
+ json.fromJson("{\"7\":1}", new TypeRef<Map<Integer, Integer>>() {});
+ assertEquals(values.get(7), Integer.valueOf(1));
+ assertNull(
+ JsonTestSupport.primaryTypeResolver(json)
+ .sharedRegistry()
+ .cachedFieldName(JsonFieldNameHash.hash("7")));
+ }
+
+ @Test
+ public void eligibilityBoundaries() {
+ assertCached(newJson(64), "");
+ assertCached(newJson(64), "a");
+ assertCached(newJson(64), "12345678");
+ assertCached(newJson(64), "123456789");
+ assertCached(newJson(64), "1234567890123456");
+
+ assertNotCached(newJson(64), "12345678901234567");
+ assertNotCached(newJson(64), "café");
+ assertNotCached(newJson(64), "键");
+ ForyJson utf8 = newJson(64);
+ byte[] multibyteName = "{\"键\":1}".getBytes(StandardCharsets.UTF_8);
+ String utf8First = firstKey(utf8.fromJson(multibyteName,
JsonObject.class));
+ String utf8Second = firstKey(utf8.fromJson(multibyteName,
JsonObject.class));
+ assertEquals(utf8Second, "键");
+ assertNotSame(utf8Second, utf8First);
+ ForyJson json = newJson(64);
+ String escapedFirst = firstKey(json.fromJson("{\"na\\u006de\":1}",
JsonObject.class));
+ String escapedSecond = firstKey(json.fromJson("{\"na\\u006de\":1}",
JsonObject.class));
+ assertEquals(escapedFirst, "name");
+ assertNotSame(escapedSecond, escapedFirst);
+ }
+
+ @Test
+ public void longFieldNames() {
+ ForyJson json = newJson(64);
+ assertNotCached(json, "abcdefghijklmnopqrstuvwxyz0123456789");
+ assertNotCached(json, "abcdefghijklmnopé");
+
+ String first = firstKey(json.fromJson("{\"abcdefghijklmnop\\u0071\":1}",
JsonObject.class));
+ String second = firstKey(json.fromJson("{\"abcdefghijklmnop\\u0071\":1}",
JsonObject.class));
+ assertEquals(first, "abcdefghijklmnopq");
+ assertEquals(second, first);
+ assertNotSame(second, first);
+ }
+
+ @Test
+ public void disabledAndLocalFull() {
+ ForyJson disabled = newJson(0);
+ assertNotCached(disabled, "name");
+ assertNull(
+ JsonTestSupport.primaryTypeResolver(disabled)
+ .sharedRegistry()
+ .cachedFieldName(JsonFieldNameHash.hash("name")));
+
+ ForyJson bounded = newJson(1);
+ assertCached(bounded, "first");
+ assertNotCached(bounded, "second");
+ assertCached(bounded, "first");
+ }
+
+ @Test
+ public void localFullFallbacks() {
+ ForyJson utf8 = newJson(1);
+ byte[] utf8First = "{\"first\":1}".getBytes(StandardCharsets.UTF_8);
+ byte[] utf8Second = "{\"second\":1}".getBytes(StandardCharsets.UTF_8);
+ String utf8Canonical = firstKey(utf8.fromJson(utf8First,
JsonObject.class));
+ String utf8Second1 = firstKey(utf8.fromJson(utf8Second, JsonObject.class));
+ String utf8Second2 = firstKey(utf8.fromJson(utf8Second, JsonObject.class));
+ assertNotSame(utf8Second2, utf8Second1);
+ byte[] utf8Long =
"{\"abcdefghijklmnop\":1}".getBytes(StandardCharsets.UTF_8);
+ String utf8Long1 = firstKey(utf8.fromJson(utf8Long, JsonObject.class));
+ String utf8Long2 = firstKey(utf8.fromJson(utf8Long, JsonObject.class));
+ assertEquals(utf8Long2, "abcdefghijklmnop");
+ assertNotSame(utf8Long2, utf8Long1);
+ assertSame(firstKey(utf8.fromJson(utf8First, JsonObject.class)),
utf8Canonical);
+
+ ForyJson utf16 = newJson(1);
+ String utf16Canonical = firstKey(utf16.fromJson("{\"first\":\"中文\"}",
JsonObject.class));
+ String utf16Second1 = firstKey(utf16.fromJson("{\"second\":\"中文\"}",
JsonObject.class));
+ String utf16Second2 = firstKey(utf16.fromJson("{\"second\":\"中文\"}",
JsonObject.class));
+ assertNotSame(utf16Second2, utf16Second1);
+ String utf16Long1 =
firstKey(utf16.fromJson("{\"abcdefghijklmnop\":\"中文\"}", JsonObject.class));
+ String utf16Long2 =
firstKey(utf16.fromJson("{\"abcdefghijklmnop\":\"中文\"}", JsonObject.class));
+ assertEquals(utf16Long2, "abcdefghijklmnop");
+ assertNotSame(utf16Long2, utf16Long1);
+ assertSame(firstKey(utf16.fromJson("{\"first\":\"中文\"}",
JsonObject.class)), utf16Canonical);
+ }
+
+ @Test
+ public void localLimitSkipsShared() {
+ ForyJson json = newJson(1, 2);
+ JsonSharedRegistry registry =
JsonTestSupport.primaryTypeResolver(json).sharedRegistry();
+ Latin1JsonReader secondary =
+ (Latin1JsonReader) JsonTestSupport.secondaryStateField(json, 0,
"latin1Reader");
+ String sharedName = secondary.reset(latin1Bytes("\"b\"")).readFieldName();
+ CachedFieldName shared =
registry.cachedFieldName(JsonFieldNameHash.hash("b"));
+ assertNotNull(shared);
+ assertSame(shared.name(), sharedName);
+ String first = firstKey(json.fromJson("{\"a\":1}", JsonObject.class));
+
+ String fallback1 = firstKey(json.fromJson("{\"b\":1}", JsonObject.class));
+ String fallback2 = firstKey(json.fromJson("{\"b\":1}", JsonObject.class));
+ assertNotSame(fallback1, shared.name());
+ assertNotSame(fallback2, shared.name());
+ assertNotSame(fallback2, fallback1);
+ assertSame(firstKey(json.fromJson("{\"a\":1}", JsonObject.class)), first);
+ assertSame(registry.cachedFieldName(JsonFieldNameHash.hash("b")), shared);
+ CachedFieldName primaryEntry =
registry.cachedFieldName(JsonFieldNameHash.hash("a"));
+ assertNotNull(primaryEntry);
+ assertSame(primaryEntry.name(), first);
+ }
+
+ @Test
+ public void adjacentSlotHit() {
+ ForyJson json = newJson(2);
+ String home = firstKey(json.fromJson("{\"a\":1}", JsonObject.class));
+ String adjacent = firstKey(json.fromJson("{\"e\":1}", JsonObject.class));
+
+ assertSame(firstKey(json.fromJson("{\"e\":2}", JsonObject.class)),
adjacent);
+ assertSame(firstKey(json.fromJson("{\"a\":2}", JsonObject.class)), home);
+ }
+
+ @Test
+ public void localConflictSkipsShared() {
+ ForyJson json = newJson(3, 2);
+ JsonSharedRegistry registry =
JsonTestSupport.primaryTypeResolver(json).sharedRegistry();
+ Latin1JsonReader secondary =
+ (Latin1JsonReader) JsonTestSupport.secondaryStateField(json, 0,
"latin1Reader");
+ String sharedName = secondary.reset(latin1Bytes("\"q\"")).readFieldName();
+ CachedFieldName shared =
registry.cachedFieldName(JsonFieldNameHash.hash("q"));
+ assertNotNull(shared);
+ assertSame(shared.name(), sharedName);
+ String home = firstKey(json.fromJson("{\"a\":1}", JsonObject.class));
+ String adjacent = firstKey(json.fromJson("{\"i\":1}", JsonObject.class));
+
+ String fallback1 = firstKey(json.fromJson("{\"q\":1}", JsonObject.class));
+ String fallback2 = firstKey(json.fromJson("{\"q\":1}", JsonObject.class));
+ assertNotSame(fallback1, shared.name());
+ assertNotSame(fallback2, shared.name());
+ assertNotSame(fallback2, fallback1);
+ assertSame(firstKey(json.fromJson("{\"a\":2}", JsonObject.class)), home);
+ assertSame(firstKey(json.fromJson("{\"i\":2}", JsonObject.class)),
adjacent);
+ assertSame(registry.cachedFieldName(JsonFieldNameHash.hash("q")), shared);
+ }
+
+ @Test
+ public void pooledReadersShareNames() {
+ ForyJson json =
+ ForyJson.builder()
+ .withCodegen(false)
+ .withAsyncCompilation(false)
+ .withConcurrencyLevel(2)
+ .withMaxCachedFieldNames(1)
+ .build();
+ Latin1JsonReader primary =
+ (Latin1JsonReader) JsonTestSupport.primaryStateField(json,
"latin1Reader");
+ Latin1JsonReader secondary =
+ (Latin1JsonReader) JsonTestSupport.secondaryStateField(json, 0,
"latin1Reader");
+
+ String first = primary.reset(latin1Bytes("\"shared\"")).readFieldName();
+ String second = secondary.reset(latin1Bytes("\"shared\"")).readFieldName();
+ assertSame(second, first);
+ }
+
+ @Test
+ public void typedFieldsAreNotCached() {
+ assertTypedFieldsNotCached(false);
+ assertTypedFieldsNotCached(true);
+ }
+
+ @Test
+ public void valuesAreNotCached() {
+ ForyJson json = newJson(64);
+ JsonObject first = json.fromJson("{\"name\":\"name\"}", JsonObject.class);
+ JsonObject second = json.fromJson("{\"name\":\"name\"}", JsonObject.class);
+ String key = firstKey(first);
+ assertNotSame(first.get("name"), key);
+ assertNotSame(second.get("name"), first.get("name"));
+ assertSame(firstKey(second), key);
+ }
+
+ @Test
+ public void separateRuntimes() {
+ String first = firstKey(newJson(64).fromJson("{\"name\":1}",
JsonObject.class));
+ String second = firstKey(newJson(64).fromJson("{\"name\":1}",
JsonObject.class));
+ assertNotSame(second, first);
+ }
+
+ @Test
+ public void malformedNames() {
+ ForyJson json = newJson(64);
+ assertThrows(ForyJsonException.class, () ->
json.fromJson("{\"bad\\q\":1}", JsonObject.class));
+ assertThrows(
+ ForyJsonException.class,
+ () -> json.fromJson("{\"abcdefghijklmnop\\q\":1}", JsonObject.class));
+ assertThrows(
+ ForyJsonException.class, () -> json.fromJson("{\"bad\u0001\":1}",
JsonObject.class));
+ assertThrows(
+ ForyJsonException.class,
+ () -> json.fromJson("{\"abcdefghijklmnop\u0001\":1}",
JsonObject.class));
+ assertThrows(
+ ForyJsonException.class, () -> json.fromJson("{\"abcdefghijklmnopq",
JsonObject.class));
+ assertThrows(ForyJsonException.class, () ->
json.fromJson("{\"\ud800\":1}", JsonObject.class));
+
+ byte[] invalidUtf8 = {'{', '"', (byte) 0xc0, (byte) 0x80, '"', ':', '1',
'}'};
+ assertThrows(ForyJsonException.class, () -> json.fromJson(invalidUtf8,
JsonObject.class));
+ JsonSharedRegistry registry =
JsonTestSupport.primaryTypeResolver(json).sharedRegistry();
+ assertNull(registry.cachedFieldName(JsonFieldNameHash.hash("bad")));
+ assertNull(registry.cachedFieldName(JsonFieldNameHash.hash("")));
+ }
+
+ @Test
+ public void sharedHashCollision() {
+ JsonConfig config = JsonTestSupport.config(newJson(4));
+ JsonSharedRegistry registry = new JsonSharedRegistry(config);
+ CachedFieldName first = registry.cacheFieldName(1L, "a", 'a', 0);
+ CachedFieldName second = registry.cacheFieldName(1L, "b", 'b', 0);
+ assertSame(second, first);
+ assertEquals(first.name(), "a");
+ assertSame(registry.cachedFieldName(1L), first);
+ }
+
+ @Test
+ public void readerHashCollision() {
+ ForyJson json = newJson(8);
+ String expected = "collisionKey";
+ String different = "differentKey";
+ long hash = JsonFieldNameHash.hash(expected);
+ long[] words = pack(different);
+ JsonSharedRegistry registry =
JsonTestSupport.primaryTypeResolver(json).sharedRegistry();
+ CachedFieldName collision =
+ registry.cacheFieldName(hash, new String(different), words[0],
words[1]);
+
+ assertCollisionFallback(
+ collision,
+ firstKey(json.fromJson("{\"collisionKey\":1}", JsonObject.class)),
+ firstKey(json.fromJson("{\"collisionKey\":2}", JsonObject.class)));
+ assertCollisionFallback(
+ collision,
+ firstKey(json.fromJson("{\"collisionKey\":\"中文\"}", JsonObject.class)),
+ firstKey(json.fromJson("{\"collisionKey\":\"中文\"}",
JsonObject.class)));
+ byte[] utf8 = "{\"collisionKey\":1}".getBytes(StandardCharsets.UTF_8);
+ assertCollisionFallback(
+ collision,
+ firstKey(json.fromJson(utf8, JsonObject.class)),
+ firstKey(json.fromJson(utf8, JsonObject.class)));
+ assertSame(registry.cachedFieldName(hash), collision);
+ }
+
+ @Test
+ public void concurrentPublication() throws Exception {
+ JsonConfig config = JsonTestSupport.config(newJson(1));
+ JsonSharedRegistry registry = new JsonSharedRegistry(config);
+ int threads = 8;
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+ CountDownLatch start = new CountDownLatch(1);
+ CountDownLatch done = new CountDownLatch(threads);
+ AtomicReferenceArray<CachedFieldName> results = new
AtomicReferenceArray<>(threads);
+ String name = "name";
+ long hash = JsonFieldNameHash.hash(name);
+ long[] words = pack(name);
+ for (int i = 0; i < threads; i++) {
+ final int index = i;
+ executor.execute(
+ () -> {
+ try {
+ start.await();
+ results.set(
+ index, registry.cacheFieldName(hash, new String(name),
words[0], words[1]));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } finally {
+ done.countDown();
+ }
+ });
+ }
+ start.countDown();
+ assertTrue(done.await(10, TimeUnit.SECONDS));
+ executor.shutdown();
+ executor.awaitTermination(10, TimeUnit.SECONDS);
+ for (int i = 1; i < threads; i++) {
+ assertSame(results.get(i), results.get(0));
+ }
+ assertSame(registry.cachedFieldName(hash), results.get(0));
+ }
+
+ private static ForyJson newJson(int maxCachedFieldNames) {
+ return newJson(maxCachedFieldNames, 1);
+ }
+
+ private static ForyJson newJson(int maxCachedFieldNames, int
concurrencyLevel) {
+ return ForyJson.builder()
+ .withCodegen(false)
+ .withAsyncCompilation(false)
+ .withConcurrencyLevel(concurrencyLevel)
+ .withMaxCachedFieldNames(maxCachedFieldNames)
+ .build();
+ }
+
+ private static void assertTypedFieldsNotCached(boolean codegen) {
+ ForyJson json =
+ ForyJson.builder()
+ .withCodegen(codegen)
+ .withAsyncCompilation(false)
+ .withConcurrencyLevel(1)
+ .withMaxCachedFieldNames(64)
+ .build();
+ TypedFields value = json.fromJson("{\"value\":1}", TypedFields.class);
+ assertEquals(value.value, 1);
+ assertNull(
+ JsonTestSupport.primaryTypeResolver(json)
+ .sharedRegistry()
+ .cachedFieldName(JsonFieldNameHash.hash("value")));
+ }
+
+ private static void assertCached(ForyJson json, String name) {
+ String first = firstKey(json.fromJson(jsonForName(name),
JsonObject.class));
+ String second = firstKey(json.fromJson(jsonForName(name),
JsonObject.class));
+ assertSame(second, first);
+ CachedFieldName entry =
+ JsonTestSupport.primaryTypeResolver(json)
+ .sharedRegistry()
+ .cachedFieldName(JsonFieldNameHash.hash(name));
+ assertNotNull(entry);
+ assertSame(entry.name(), first);
+ }
+
+ private static void assertNotCached(ForyJson json, String name) {
+ String first = firstKey(json.fromJson(jsonForName(name),
JsonObject.class));
+ String second = firstKey(json.fromJson(jsonForName(name),
JsonObject.class));
+ assertEquals(first, name);
+ assertEquals(second, name);
+ assertNotSame(second, first);
+ }
+
+ private static void assertCollisionFallback(
+ CachedFieldName collision, String first, String second) {
+ assertEquals(first, "collisionKey");
+ assertEquals(second, "collisionKey");
+ assertNotSame(first, collision.name());
+ assertNotSame(second, collision.name());
+ assertNotSame(second, first);
+ }
+
+ private static String jsonForName(String name) {
+ return "{\"" + name + "\":1}";
+ }
+
+ private static byte[] latin1Bytes(String json) {
+ return json.getBytes(StandardCharsets.ISO_8859_1);
+ }
+
+ private static String firstKey(Map<?, ?> map) {
+ return (String) map.keySet().iterator().next();
+ }
+
+ private static long[] pack(String name) {
+ long word0 = 0;
+ long word1 = 0;
+ for (int i = 0; i < name.length(); i++) {
+ if (i < Long.BYTES) {
+ word0 |= ((long) name.charAt(i)) << (i << 3);
+ } else {
+ word1 |= ((long) name.charAt(i)) << ((i - Long.BYTES) << 3);
+ }
+ }
+ return new long[] {word0, word1};
+ }
+
+ public static class TypedFields {
+ public 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 1a95060b4..515d43029 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
@@ -43,6 +43,7 @@ final class JsonTestSupport {
PropertyNamingStrategy.LOWER_CAMEL_CASE,
JsonTestSupport.class.getClassLoader(),
ForyJson.DEFAULT_MAX_DEPTH,
+ ForyJson.DEFAULT_MAX_CACHED_FIELD_NAMES,
1,
2 * 1024 * 1024,
new CodecRegistry(),
@@ -135,6 +136,17 @@ final class JsonTestSupport {
}
}
+ static Object secondaryStateField(ForyJson json, int index, String name) {
+ try {
+ AtomicReferenceArray<?> slots = (AtomicReferenceArray<?>) field(json,
"slots");
+ Object pooledState = slots.get(index);
+ Object state = field(pooledState, "state");
+ return field(state, name);
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError(e);
+ }
+ }
+
static JsonConfig config(ForyJson json) {
try {
return (JsonConfig) field(json, "config");
diff --git
a/java/fory-json/src/test/java/org/apache/fory/json/reader/FieldNameCacheTest.java
b/java/fory-json/src/test/java/org/apache/fory/json/reader/FieldNameCacheTest.java
new file mode 100644
index 000000000..5b164b1b5
--- /dev/null
+++
b/java/fory-json/src/test/java/org/apache/fory/json/reader/FieldNameCacheTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.reader;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import java.lang.reflect.Field;
+import org.apache.fory.json.ForyJson;
+import org.apache.fory.json.meta.JsonFieldNameHash;
+import org.apache.fory.json.resolver.JsonSharedRegistry;
+import org.apache.fory.json.resolver.JsonSharedRegistry.CachedFieldName;
+import org.testng.annotations.Test;
+
+public class FieldNameCacheTest {
+ private static final JsonSharedRegistry REGISTRY = registry();
+
+ @Test
+ public void fixedCandidatesAndLimit() {
+ FieldNameCache cache = new FieldNameCache(3);
+ CachedFieldName home = entry("home");
+ CachedFieldName adjacent = entry("adjacent");
+ CachedFieldName fallback = entry("fallback");
+ CachedFieldName unrelated = entry("unrelated");
+
+ assertNull(cache.get(0L));
+ assertTrue(cache.canPut(0L));
+ cache.put(0L, home);
+ assertSame(cache.get(0L), home);
+
+ assertTrue(cache.canPut(8L));
+ cache.put(8L, adjacent);
+ assertSame(cache.get(8L), adjacent);
+
+ assertFalse(cache.canPut(16L));
+ cache.put(16L, fallback);
+ assertNull(cache.get(16L));
+ assertSame(cache.get(0L), home);
+ assertSame(cache.get(8L), adjacent);
+
+ assertTrue(cache.canPut(2L));
+ cache.put(2L, unrelated);
+ assertSame(cache.get(2L), unrelated);
+ assertFalse(cache.canPut(3L));
+ cache.put(3L, fallback);
+ assertNull(cache.get(3L));
+ }
+
+ @Test
+ public void idempotentPut() {
+ FieldNameCache cache = new FieldNameCache(1);
+ CachedFieldName first = entry("first");
+ CachedFieldName second = entry("second");
+
+ cache.put(1L, first);
+ cache.put(1L, first);
+ cache.put(1L, second);
+
+ assertSame(cache.get(1L), first);
+ assertFalse(cache.canPut(1L));
+ }
+
+ private static JsonSharedRegistry registry() {
+ try {
+ ForyJson json = ForyJson.builder().withCodegen(false).build();
+ Field field = ForyJson.class.getDeclaredField("sharedRegistry");
+ field.setAccessible(true);
+ return (JsonSharedRegistry) field.get(json);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private static CachedFieldName entry(String name) {
+ long word0 = 0;
+ long word1 = 0;
+ for (int i = 0; i < name.length(); i++) {
+ if (i < Long.BYTES) {
+ word0 |= ((long) name.charAt(i)) << (i << 3);
+ } else {
+ word1 |= ((long) name.charAt(i)) << ((i - Long.BYTES) << 3);
+ }
+ }
+ return REGISTRY.cacheFieldName(JsonFieldNameHash.hash(name), name, word0,
word1);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]