From 72e85a65fe3fc70fdb8397543200d8bcbb5f0d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Wed, 1 Jul 2026 14:54:23 +0800 Subject: [PATCH 01/40] feat(java): support JSON bean properties --- .../java/org/apache/fory/json/ForyJson.java | 10 +- .../org/apache/fory/json/ForyJsonBuilder.java | 10 +- .../fory/json/codec/BaseObjectCodec.java | 241 +++++++++++++++--- .../apache/fory/json/codegen/JsonCodegen.java | 17 +- .../codegen/JsonGeneratedCodecBuilder.java | 18 ++ .../fory/json/meta/JsonFieldAccessor.java | 79 +++++- .../apache/fory/json/meta/JsonFieldInfo.java | 43 +++- .../json/resolver/JsonSharedRegistry.java | 11 +- .../fory/json/resolver/JsonTypeResolver.java | 2 +- .../org/apache/fory/json/JsonObjectTest.java | 4 +- .../apache/fory/json/JsonPropertyTest.java | 131 ++++++++++ .../apache/fory/json/data/BeanProperties.java | 186 ++++++++++++++ 12 files changed, 697 insertions(+), 55 deletions(-) create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java create mode 100644 java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java diff --git a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java index 8fc4662da3..890e25b2a7 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 @@ -55,10 +55,16 @@ public final class ForyJson { private final AtomicReferenceArray slots; ForyJson( - boolean writeNullFields, boolean codegenEnabled, int maxDepth, CodecRegistry codecRegistry) { + boolean writeNullFields, + boolean codegenEnabled, + boolean propertyDiscoveryEnabled, + int maxDepth, + CodecRegistry codecRegistry) { this.writeNullFields = writeNullFields; this.maxDepth = maxDepth; - sharedRegistry = new JsonSharedRegistry(codegenEnabled, writeNullFields, codecRegistry); + sharedRegistry = + new JsonSharedRegistry( + codegenEnabled, writeNullFields, propertyDiscoveryEnabled, codecRegistry); poolSize = DEFAULT_POOL_SIZE; primarySlot = new AtomicReference<>( 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 17a5cb6c53..b0ed1f58b5 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJsonBuilder.java @@ -26,6 +26,7 @@ public final class ForyJsonBuilder { private boolean writeNullFields; private boolean codegenEnabled = true; + private boolean propertyDiscoveryEnabled = true; private int maxDepth = ForyJson.DEFAULT_MAX_DEPTH; private final CodecRegistry codecRegistry = new CodecRegistry(); @@ -43,6 +44,12 @@ public ForyJsonBuilder withCodegen(boolean codegenEnabled) { return this; } + /** Enables JavaBean getter/setter JSON property discovery. */ + public ForyJsonBuilder withPropertyDiscovery(boolean propertyDiscoveryEnabled) { + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; + return this; + } + /** Sets the maximum nested JSON object/array depth allowed while parsing. */ public ForyJsonBuilder maxDepth(int maxDepth) { if (maxDepth < 1) { @@ -59,6 +66,7 @@ public ForyJsonBuilder registerCodec(Class type, JsonCodec codec) { } public ForyJson build() { - return new ForyJson(writeNullFields, codegenEnabled, maxDepth, codecRegistry); + return new ForyJson( + writeNullFields, codegenEnabled, propertyDiscoveryEnabled, maxDepth, codecRegistry); } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java index 3566caa8bc..2c7cad25f2 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java @@ -20,7 +20,9 @@ package org.apache.fory.json.codec; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -81,7 +83,7 @@ record = RecordUtils.isRecord(type); } } - public static ObjectCodec build(Class type) { + public static ObjectCodec build(Class type, boolean propertyDiscoveryEnabled) { if (type.isInterface() || Modifier.isAbstract(type.getModifiers()) || type.isPrimitive() @@ -93,39 +95,21 @@ boolean record = RecordUtils.isRecord(type); boolean writeExpose = hasWriteExpose(type); boolean readExpose = hasReadExpose(type, record); TreeMap builders = new TreeMap<>(); - for (Class current = type; - current != null && current != Object.class; - current = current.getSuperclass()) { - for (Field field : current.getDeclaredFields()) { - int modifiers = field.getModifiers(); - if (!isEligibleField(field)) { - continue; - } - boolean write = includeWrite(field, writeExpose); - boolean read = (record || !Modifier.isFinal(modifiers)) && includeRead(field, readExpose); - if (!write && !read) { - continue; - } - FieldBuilder builder = new FieldBuilder(field.getName()); - if (write) { - builder.setWriteField(field); - } - if (read) { - builder.setReadField(field); - } - if (builders.put(field.getName(), builder) != null) { - throw new ForyJsonException("Duplicate JSON field " + field.getName()); - } - } + addFields(type, record, writeExpose, readExpose, propertyDiscoveryEnabled, builders); + if (propertyDiscoveryEnabled && !record) { + addAccessors(type, writeExpose, readExpose, builders); } List writes = new ArrayList<>(); List reads = new ArrayList<>(); for (FieldBuilder builder : builders.values()) { + if (!builder.hasWriteSource() && !builder.hasReadSink()) { + continue; + } JsonFieldInfo field = builder.build(record); - if (builder.writeAccessor != null) { + if (builder.hasWriteSource()) { writes.add(field); } - if (builder.readField != null) { + if (builder.hasReadSink()) { reads.add(field); } } @@ -138,6 +122,71 @@ boolean record = RecordUtils.isRecord(type); type, writeArray, readArray, ObjectInstantiators.createObjectInstantiator(type)); } + private static void addFields( + Class type, + boolean record, + boolean writeExpose, + boolean readExpose, + boolean propertyDiscoveryEnabled, + TreeMap builders) { + for (Class current = type; + current != null && current != Object.class; + current = current.getSuperclass()) { + for (Field field : current.getDeclaredFields()) { + int modifiers = field.getModifiers(); + if (!isEligibleField(field)) { + continue; + } + boolean write = includeWrite(field, writeExpose); + boolean readAllowed = includeRead(field, readExpose); + boolean read = (record || !Modifier.isFinal(modifiers)) && readAllowed; + if (!propertyDiscoveryEnabled && !write && !read) { + continue; + } + FieldBuilder builder = + builders.computeIfAbsent(field.getName(), name -> new FieldBuilder(name)); + builder.setField(field, write, read, write, readAllowed); + } + } + } + + private static void addAccessors( + Class type, + boolean writeExpose, + boolean readExpose, + TreeMap builders) { + for (Method method : type.getMethods()) { + if (!isEligibleAccessor(method)) { + continue; + } + String propertyName = getterPropertyName(method); + if (propertyName != null) { + FieldBuilder builder = builders.get(propertyName); + if (builder == null) { + if (writeExpose) { + continue; + } + builder = new FieldBuilder(propertyName); + builders.put(propertyName, builder); + } + builder.setWriteGetter(method, writeExpose); + continue; + } + propertyName = setterPropertyName(method); + if (propertyName != null) { + FieldBuilder builder = builders.get(propertyName); + if (builder == null) { + if (readExpose) { + continue; + } + builder = new FieldBuilder(propertyName); + builders.put(propertyName, builder); + } + builder.setReadSetter(method, readExpose); + } + } + } + public final Class type() { return type; } @@ -384,6 +433,53 @@ private static boolean isEligibleField(Field field) { && !field.isSynthetic(); } + private static boolean isEligibleAccessor(Method method) { + int modifiers = method.getModifiers(); + return Modifier.isPublic(modifiers) + && !Modifier.isStatic(modifiers) + && !method.isSynthetic() + && !method.isBridge(); + } + + private static String getterPropertyName(Method method) { + if (method.getParameterCount() != 0 || method.getReturnType() == void.class) { + return null; + } + String name = method.getName(); + if (name.equals("getClass")) { + return null; + } + if (name.length() > 3 && name.startsWith("get")) { + return decapitalize(name.substring(3)); + } + if (name.length() > 2 + && name.startsWith("is") + && (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class)) { + return decapitalize(name.substring(2)); + } + return null; + } + + private static String setterPropertyName(Method method) { + if (method.getParameterCount() != 1 || method.getReturnType() != void.class) { + return null; + } + String name = method.getName(); + if (name.length() > 3 && name.startsWith("set")) { + return decapitalize(name.substring(3)); + } + return null; + } + + private static String decapitalize(String name) { + if (name.length() > 1 + && Character.isUpperCase(name.charAt(0)) + && Character.isUpperCase(name.charAt(1))) { + return name; + } + return Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + private static boolean includeWrite(Field field, boolean exposeMode) { return include(field, exposeMode, true); } @@ -419,8 +515,13 @@ private static Object[] recordFieldDefaults( private static final class FieldBuilder { private final String name; + private Field field; + private boolean fieldWriteAllowed; + private boolean fieldReadAllowed; private Field writeField; private Field readField; + private Method writeGetter; + private Method readSetter; private JsonFieldAccessor writeAccessor; private JsonFieldAccessor readAccessor; @@ -428,24 +529,88 @@ private FieldBuilder(String name) { this.name = name; } - private void setWriteField(Field field) { - if (writeField != null) { - throw new ForyJsonException("Duplicate public JSON field " + name); + private void setField( + Field field, + boolean writeSource, + boolean readSink, + boolean writeAllowed, + boolean readAllowed) { + if (this.field != null) { + throw new ForyJsonException("Duplicate JSON field " + name); + } + this.field = field; + fieldWriteAllowed = writeAllowed; + fieldReadAllowed = readAllowed; + if (writeSource) { + writeField = field; + } + if (readSink) { + readField = field; } - writeField = field; } - private void setReadField(Field field) { - if (readField != null) { - throw new ForyJsonException("Duplicate public JSON field " + name); + private void setWriteGetter(Method getter, boolean exposeMode) { + if (!methodAllowed(exposeMode, fieldWriteAllowed)) { + return; + } + if (writeGetter != null) { + throw new ForyJsonException("Duplicate JSON getter for property " + name); } - readField = field; + writeGetter = getter; + writeField = null; + } + + private void setReadSetter(Method setter, boolean exposeMode) { + if (!methodAllowed(exposeMode, fieldReadAllowed)) { + return; + } + if (readSetter != null) { + throw new ForyJsonException("Duplicate JSON setter for property " + name); + } + readSetter = setter; + readField = null; + } + + private boolean hasWriteSource() { + return writeGetter != null || writeField != null; + } + + private boolean hasReadSink() { + return readSetter != null || readField != null; } private JsonFieldInfo build(boolean record) { - writeAccessor = writeField == null ? null : JsonFieldAccessor.forField(writeField); - readAccessor = readField == null || record ? null : JsonFieldAccessor.forField(readField); - return new JsonFieldInfo(name, writeField, readField, writeAccessor, readAccessor); + validateTypes(); + writeAccessor = + writeGetter != null + ? JsonFieldAccessor.forGetter(writeGetter) + : (writeField == null ? null : JsonFieldAccessor.forField(writeField)); + readAccessor = + readSetter != null + ? JsonFieldAccessor.forSetter(readSetter) + : (readField == null || record ? null : JsonFieldAccessor.forField(readField)); + return new JsonFieldInfo( + name, writeField, writeGetter, readField, readSetter, writeAccessor, readAccessor); + } + + private boolean methodAllowed(boolean exposeMode, boolean fieldAllowed) { + // A same-named field owns @Expose/@JsonIgnore direction decisions for the JSON property. + return field == null ? !exposeMode : fieldAllowed; + } + + private void validateTypes() { + Type writeType = + writeGetter == null ? fieldType(writeField) : writeGetter.getGenericReturnType(); + Type readType = + readSetter == null ? fieldType(readField) : readSetter.getGenericParameterTypes()[0]; + if (writeType != null && readType != null && !writeType.equals(readType)) { + throw new ForyJsonException( + "Conflicting JSON property types for " + name + ": " + writeType + " and " + readType); + } + } + + private static Type fieldType(Field field) { + return field == null ? null : field.getGenericType(); } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index df786e028e..b2be63e1e3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -21,6 +21,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Map; @@ -203,10 +204,10 @@ static Class readNestedType(JsonFieldInfo property) { private boolean canCompileWrite(JsonFieldInfo property) { Field field = property.writeField(); - if (field == null) { + if (field == null && property.writeGetter() == null) { return false; } - if (!isRecordField(property) && property.writeField() == null) { + if (property.writeGetter() != null && !canCall(property.writeGetter())) { return false; } Class rawType = property.writeRawType(); @@ -221,7 +222,12 @@ private boolean canCompileRead(JsonFieldInfo property, boolean record) { if (!record && property.readAccessor() == null) { return false; } - if (!record && property.readAccessor().coreAccessor() == null) { + if (!record && property.readSetter() != null && !canCall(property.readSetter())) { + return false; + } + if (!record + && property.readSetter() == null + && property.readAccessor().coreAccessor() == null) { return false; } Class rawType = property.readRawType(); @@ -236,6 +242,11 @@ private boolean canCompile(Class type) { return CodeGenerator.sourcePublicAccessible(type) && isVisible(type); } + private boolean canCall(Method method) { + return Modifier.isPublic(method.getModifiers()) + && CodeGenerator.sourcePublicAccessible(method.getDeclaringClass()); + } + private boolean isVisible(Class type) { if (type.isPrimitive()) { return true; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index e5e0877993..55b0f463a3 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -119,6 +119,15 @@ private Method recordReadMethod(Field field) { } Expression fieldValue(JsonFieldInfo property, Expression object) { + Method getter = property.writeGetter(); + if (getter != null) { + return new Expression.Invoke( + object, + getter.getName(), + property.name(), + TypeRef.of(getter.getGenericReturnType()), + !getter.getReturnType().isPrimitive()); + } return getFieldValue(object, writeDescriptor(property)); } @@ -127,6 +136,15 @@ Expression newObject() { } Expression setField(JsonFieldInfo property, Expression object, Expression value) { + Method setter = property.readSetter(); + if (setter != null) { + Class rawType = setter.getParameterTypes()[0]; + TypeRef typeRef = TypeRef.of(setter.getGenericParameterTypes()[0]); + if (!rawType.isAssignableFrom(value.type().getRawType())) { + value = tryInlineCast(value, typeRef); + } + return new Expression.Invoke(object, setter.getName(), value); + } return setFieldValue( object, readDescriptor(property), diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java index 5bed687f76..76503a7759 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldAccessor.java @@ -20,6 +20,9 @@ package org.apache.fory.json.meta; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.apache.fory.json.ForyJsonException; import org.apache.fory.reflect.FieldAccessor; public abstract class JsonFieldAccessor { @@ -27,9 +30,21 @@ public Object getObject(Object target) { throw new UnsupportedOperationException(); } - public abstract Field field(); + public Field field() { + return null; + } - public abstract FieldAccessor coreAccessor(); + public Method getter() { + return null; + } + + public Method setter() { + return null; + } + + public FieldAccessor coreAccessor() { + return null; + } public boolean getBoolean(Object target) { return (Boolean) getObject(target); @@ -103,6 +118,14 @@ public static JsonFieldAccessor forField(Field field) { return new FieldJsonAccessor(FieldAccessor.createAccessor(field)); } + public static JsonFieldAccessor forGetter(Method getter) { + return new GetterJsonAccessor(getter); + } + + public static JsonFieldAccessor forSetter(Method setter) { + return new SetterJsonAccessor(setter); + } + private static final class FieldJsonAccessor extends JsonFieldAccessor { private final FieldAccessor accessor; @@ -210,4 +233,56 @@ public void putChar(Object target, char value) { accessor.putChar(target, value); } } + + private static final class GetterJsonAccessor extends JsonFieldAccessor { + private final Method getter; + + private GetterJsonAccessor(Method getter) { + this.getter = getter; + getter.setAccessible(true); + } + + @Override + public Method getter() { + return getter; + } + + @Override + public Object getObject(Object target) { + try { + return getter.invoke(target); + } catch (IllegalAccessException | InvocationTargetException e) { + throw accessException(getter, e); + } + } + } + + private static final class SetterJsonAccessor extends JsonFieldAccessor { + private final Method setter; + + private SetterJsonAccessor(Method setter) { + this.setter = setter; + setter.setAccessible(true); + } + + @Override + public Method setter() { + return setter; + } + + @Override + public void putObject(Object target, Object value) { + try { + setter.invoke(target, value); + } catch (IllegalAccessException | InvocationTargetException e) { + throw accessException(setter, e); + } + } + } + + private static ForyJsonException accessException(Method method, ReflectiveOperationException e) { + Throwable cause = + e instanceof InvocationTargetException ? ((InvocationTargetException) e).getCause() : e; + return new ForyJsonException("Cannot access JSON property method " + method, cause); + } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index 01a31a374a..b1a4a148f8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -20,6 +20,7 @@ package org.apache.fory.json.meta; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Collection; @@ -54,6 +55,9 @@ public final class JsonFieldInfo { private final String name; private final Field writeField; + private final Method writeGetter; + private final Field readField; + private final Method readSetter; private final Type writeType; private final Class writeRawType; private final Type readType; @@ -98,16 +102,21 @@ public final class JsonFieldInfo { public JsonFieldInfo( String name, Field writeField, + Method writeGetter, Field readField, + Method readSetter, JsonFieldAccessor writeAccessor, JsonFieldAccessor readAccessor) { this.name = name; nameHash = JsonFieldNameHash.hash(name); this.writeField = writeField; - this.writeType = fieldType(writeField); - this.writeRawType = fieldRawType(writeField); - this.readType = fieldType(readField); - this.readRawType = fieldRawType(readField); + this.writeGetter = writeGetter; + this.readField = readField; + this.readSetter = readSetter; + this.writeType = writeType(writeField, writeGetter); + this.writeRawType = writeRawType(writeField, writeGetter); + this.readType = readType(readField, readSetter); + this.readRawType = readRawType(readField, readSetter); this.writeAccessor = writeAccessor; this.readAccessor = readAccessor; writeKind = writeRawType == null ? null : kind(writeRawType); @@ -181,6 +190,10 @@ public Field writeField() { return writeField; } + public Method writeGetter() { + return writeGetter; + } + public Type writeType() { return writeType; } @@ -226,7 +239,11 @@ public Type readType() { } public Field readField() { - return readAccessor == null ? null : readAccessor.field(); + return readField; + } + + public Method readSetter() { + return readSetter; } public Class readRawType() { @@ -249,6 +266,22 @@ private static Class fieldRawType(Field field) { return field == null ? null : field.getType(); } + private static Type writeType(Field field, Method getter) { + return getter == null ? fieldType(field) : getter.getGenericReturnType(); + } + + private static Class writeRawType(Field field, Method getter) { + return getter == null ? fieldRawType(field) : getter.getReturnType(); + } + + private static Type readType(Field field, Method setter) { + return setter == null ? fieldType(field) : setter.getGenericParameterTypes()[0]; + } + + private static Class readRawType(Field field, Method setter) { + return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; + } + public void resolveTypes(JsonTypeResolver typeResolver) { if (writeRawType != null) { writeTypeInfo = typeResolver.getTypeInfo(writeType, writeRawType); 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 4fe557959e..a31d05ceb1 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 @@ -79,10 +79,15 @@ public final class JsonSharedRegistry { private final CodecRegistry customCodecs; private final IdentityHashMap, JsonCodec> exactCodecs; private final JsonCodegen codegen; + private final boolean propertyDiscoveryEnabled; public JsonSharedRegistry( - boolean codegenEnabled, boolean writeNullFields, CodecRegistry customCodecs) { + boolean codegenEnabled, + boolean writeNullFields, + boolean propertyDiscoveryEnabled, + CodecRegistry customCodecs) { this.customCodecs = customCodecs.copy(); + this.propertyDiscoveryEnabled = propertyDiscoveryEnabled; exactCodecs = new IdentityHashMap<>(); codegen = codegenEnabled ? new JsonCodegen(writeNullFields) : null; registerExactCodecs(); @@ -190,6 +195,10 @@ public ObjectCodecs compileObject(BaseObjectCodec codec, JsonTypeResolver localR return codegen == null ? null : codegen.compile(codec, localResolver); } + boolean propertyDiscoveryEnabled() { + return propertyDiscoveryEnabled; + } + private void registerExactCodecs() { exactCodecs.put(Object.class, ScalarCodecs.NaturalCodec.INSTANCE); exactCodecs.put(void.class, ScalarCodecs.VoidCodec.INSTANCE); 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 0b23bbb38b..8b0e43e744 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 @@ -81,7 +81,7 @@ private BaseObjectCodec buildObjectCodec(Class type) { if (cached != null) { return cached; } - ObjectCodec codec = BaseObjectCodec.build(type); + ObjectCodec codec = BaseObjectCodec.build(type, sharedRegistry.propertyDiscoveryEnabled()); // Codegen may ask for nested object metadata that points back to this type. // Publishing before compiling keeps recursive ownership in this resolver cache. objectCodecs.put(type, codec); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 1cd303cb3d..3164b9d5af 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -130,8 +130,8 @@ public void writeNullFields() { } @Test - public void ignoreMethods() { - ForyJson json = ForyJson.builder().build(); + public void fieldOnlyModeIgnoresMethods() { + ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); assertEquals( json.toJson(new MethodsIgnored()), "{\"hidden\":\"hidden\",\"setterCalls\":0,\"value\":\"field\"}"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java new file mode 100644 index 0000000000..904f0bb4e3 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonPropertyTest.java @@ -0,0 +1,131 @@ +/* + * 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.assertThrows; + +import org.apache.fory.json.data.BeanProperties.BooleanBean; +import org.apache.fory.json.data.BeanProperties.ConflictingTypesBean; +import org.apache.fory.json.data.BeanProperties.DuplicateGetterBean; +import org.apache.fory.json.data.BeanProperties.FinalFieldBean; +import org.apache.fory.json.data.BeanProperties.GetterBean; +import org.apache.fory.json.data.BeanProperties.GetterOnlyBean; +import org.apache.fory.json.data.BeanProperties.InheritedChild; +import org.apache.fory.json.data.BeanProperties.InheritedParent; +import org.apache.fory.json.data.BeanProperties.InvalidAccessorBean; +import org.apache.fory.json.data.BeanProperties.MixedBean; +import org.apache.fory.json.data.BeanProperties.OverloadedSetterBean; +import org.apache.fory.json.data.BeanProperties.SetterBean; +import org.apache.fory.json.data.BeanProperties.SetterOnlyBean; +import org.testng.annotations.Test; + +public class JsonPropertyTest extends ForyJsonTestModels { + @Test + public void writePrivateGetters() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new GetterBean()), "{\"id\":17,\"name\":\"getter-field\"}"); + assertGeneratedWhenSupported(json, GetterBean.class); + } + + @Test + public void readPrivateSetters() { + ForyJson json = ForyJson.builder().build(); + SetterBean value = json.fromJson("{\"id\":4,\"name\":\"alpha\"}", SetterBean.class); + assertEquals(SetterBean.id(value), 5); + assertEquals(SetterBean.name(value), "set-alpha"); + assertEquals(SetterBean.setterCalls(value), 2); + assertGeneratedWhenSupported(json, SetterBean.class); + } + + @Test + public void roundTripMixedProperties() { + ForyJson json = ForyJson.builder().build(); + String text = "{\"count\":3,\"name\":\"mixed\",\"score\":5}"; + assertEquals(json.toJson(new MixedBean()), text); + assertEquals(json.toJson(json.fromJson(text, MixedBean.class)), text); + assertGeneratedWhenSupported(json, MixedBean.class); + } + + @Test + public void writeBooleanIsGetter() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new BooleanBean()), "{\"ready\":true}"); + } + + @Test + public void getterOnlyWrites() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new GetterOnlyBean()), "{\"computed\":6}"); + GetterOnlyBean value = json.fromJson("{\"computed\":99}", GetterOnlyBean.class); + assertEquals(json.toJson(value), "{\"computed\":6}"); + } + + @Test + public void setterOnlyReads() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new SetterOnlyBean()), "{}"); + SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class); + assertEquals(SetterOnlyBean.received(value), "set-alpha"); + } + + @Test + public void fieldOnlyMode() { + ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); + assertEquals(json.toJson(new GetterBean()), "{\"id\":7,\"name\":\"field\"}"); + assertEquals(json.toJson(new GetterOnlyBean()), "{}"); + SetterOnlyBean value = json.fromJson("{\"secret\":\"alpha\"}", SetterOnlyBean.class); + assertEquals(SetterOnlyBean.received(value), null); + } + + @Test + public void rejectPropertyConflicts() { + ForyJson json = ForyJson.builder().build(); + assertThrows(ForyJsonException.class, () -> json.toJson(new DuplicateGetterBean())); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("{\"value\":\"alpha\"}", OverloadedSetterBean.class)); + assertThrows(ForyJsonException.class, () -> json.toJson(new ConflictingTypesBean())); + } + + @Test + public void inheritedProperties() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new InheritedChild()), "{\"id\":4,\"name\":\"child\"}"); + InheritedChild value = json.fromJson("{\"id\":7,\"name\":\"json\"}", InheritedChild.class); + assertEquals(InheritedParent.id(value), 8); + assertEquals(value.name, "json"); + } + + @Test + public void ignoreInvalidAccessors() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new InvalidAccessorBean()), "{\"value\":\"field\"}"); + } + + @Test + public void finalFieldsStayReadOnly() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new FinalFieldBean()), "{\"id\":1,\"name\":\"field\"}"); + FinalFieldBean value = json.fromJson("{\"id\":9,\"name\":\"json\"}", FinalFieldBean.class); + assertEquals(value.id, 1); + assertEquals(value.name, "json"); + } +} diff --git a/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java b/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java new file mode 100644 index 0000000000..b54169e461 --- /dev/null +++ b/java/fory-json/src/test/java/org/apache/fory/json/data/BeanProperties.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.json.data; + +public final class BeanProperties { + private BeanProperties() {} + + public static final class GetterBean { + private int id = 7; + private String name = "field"; + + public int getId() { + return id + 10; + } + + public String getName() { + return "getter-" + name; + } + } + + public static final class SetterBean { + private int id; + private String name; + private int setterCalls; + + public void setId(int id) { + this.id = id + 1; + setterCalls++; + } + + public void setName(String name) { + this.name = "set-" + name; + setterCalls++; + } + + public static int id(SetterBean value) { + return value.id; + } + + public static String name(SetterBean value) { + return value.name; + } + + public static int setterCalls(SetterBean value) { + return value.setterCalls; + } + } + + public static final class MixedBean { + public int count = 3; + private String name = "mixed"; + private int score = 5; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + } + + public static final class BooleanBean { + private boolean ready = true; + + public boolean isReady() { + return ready; + } + } + + public static final class GetterOnlyBean { + private transient int seed = 3; + + public int getComputed() { + return seed * 2; + } + } + + public static final class SetterOnlyBean { + private transient String received; + + public void setSecret(String secret) { + received = "set-" + secret; + } + + public static String received(SetterOnlyBean value) { + return value.received; + } + } + + public static final class DuplicateGetterBean { + public boolean getActive() { + return true; + } + + public boolean isActive() { + return true; + } + } + + public static final class OverloadedSetterBean { + public void setValue(int value) {} + + public void setValue(String value) {} + } + + public static final class ConflictingTypesBean { + private String value = "text"; + + public String getValue() { + return value; + } + + public void setValue(int value) {} + } + + public static class InheritedParent { + private int id = 4; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id + 1; + } + + public static int id(InheritedParent value) { + return value.id; + } + } + + public static final class InheritedChild extends InheritedParent { + public String name = "child"; + } + + public static final class InvalidAccessorBean { + public String value = "field"; + + public static String getStaticValue() { + return "static"; + } + + public String getWithArg(String ignored) { + return ignored; + } + + public void getVoidValue() {} + + public String setWrongReturn(String value) { + return value; + } + + public void setNoValue() {} + } + + public static final class FinalFieldBean { + public final int id = 1; + public String name = "field"; + } +} From 78ef5481f883f3a08f83447f1d1ee9f21dbe679b Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 16:23:41 +0800 Subject: [PATCH 02/40] perf(java): optimize Fory JSON UTF8 reads --- .../fory/json/codec/BaseObjectCodec.java | 13 +- .../apache/fory/json/codec/ScalarCodecs.java | 157 ++++++++++++++- .../apache/fory/json/codegen/JsonCodegen.java | 4 +- .../fory/json/codegen/JsonReaderCodegen.java | 22 +-- .../fory/json/reader/Utf8JsonReader.java | 180 ++++++++++++++++++ .../org/apache/fory/json/JsonObjectTest.java | 4 +- .../org/apache/fory/json/JsonScalarTest.java | 111 +++++++++++ 7 files changed, 463 insertions(+), 28 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java index 2c7cad25f2..631141b2d1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/BaseObjectCodec.java @@ -25,9 +25,9 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import org.apache.fory.annotation.Expose; import org.apache.fory.annotation.Internal; import org.apache.fory.json.ForyJsonException; @@ -94,7 +94,7 @@ public static ObjectCodec build(Class type, boolean propertyDiscoveryEnabled) boolean record = RecordUtils.isRecord(type); boolean writeExpose = hasWriteExpose(type); boolean readExpose = hasReadExpose(type, record); - TreeMap builders = new TreeMap<>(); + LinkedHashMap builders = new LinkedHashMap<>(); addFields(type, record, writeExpose, readExpose, propertyDiscoveryEnabled, builders); if (propertyDiscoveryEnabled && !record) { addAccessors(type, writeExpose, readExpose, builders); @@ -128,10 +128,15 @@ private static void addFields( boolean writeExpose, boolean readExpose, boolean propertyDiscoveryEnabled, - TreeMap builders) { + LinkedHashMap builders) { + List> hierarchy = new ArrayList<>(); for (Class current = type; current != null && current != Object.class; current = current.getSuperclass()) { + hierarchy.add(current); + } + for (int i = hierarchy.size() - 1; i >= 0; i--) { + Class current = hierarchy.get(i); for (Field field : current.getDeclaredFields()) { int modifiers = field.getModifiers(); if (!isEligibleField(field)) { @@ -154,7 +159,7 @@ private static void addAccessors( Class type, boolean writeExpose, boolean readExpose, - TreeMap builders) { + LinkedHashMap builders) { for (Method method : type.getMethods()) { if (!isEligibleAccessor(method)) { continue; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 2b58fc72c2..0d928448dc 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -515,7 +515,10 @@ final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolve @Override final Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - String value = reader.readString(); + return readStringValue(reader.readString(), typeInfo); + } + + final Object readStringValue(String value, JsonTypeInfo typeInfo) { try { return fromJsonString(value); } catch (ForyJsonException e) { @@ -897,10 +900,20 @@ String toJsonString(Object value) { @Override Object fromJsonString(String value) { - if (value.length() > 10 && value.charAt(10) == 'T') { - return LocalDate.parse(value.substring(0, 10)); + return parseIsoLocalDate(value); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoLocalDate(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); } - return LocalDate.parse(value); } } @@ -1082,10 +1095,146 @@ String toJsonString(Object value) { @Override Object fromJsonString(String value) { + return parseIsoOffsetDateTime(value); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readIsoOffsetDateTime(); + } catch (RuntimeException e) { + return readStringValue(reader.readString(), typeInfo); + } + } + } + + private static LocalDate parseIsoLocalDate(String value) { + int length = value.length(); + if (length >= 10 + && (length == 10 || value.charAt(10) == 'T') + && value.charAt(4) == '-' + && value.charAt(7) == '-') { + try { + return LocalDate.of(parse4(value, 0), parse2(value, 5), parse2(value, 8)); + } catch (RuntimeException e) { + if (length > 10 && value.charAt(10) == 'T') { + return LocalDate.parse(value.substring(0, 10)); + } + return LocalDate.parse(value); + } + } + return LocalDate.parse(value); + } + + private static OffsetDateTime parseIsoOffsetDateTime(String value) { + try { + // java.time emits these ISO forms, and parsing them directly avoids DateTimeFormatter's + // parsed-field maps on JSON scalar hot paths. Uncommon forms still use the JDK parser. + return parseIsoOffsetDateTimeFast(value); + } catch (RuntimeException e) { return OffsetDateTime.parse(value); } } + private static OffsetDateTime parseIsoOffsetDateTimeFast(String value) { + int length = value.length(); + if (length < 17 + || value.charAt(4) != '-' + || value.charAt(7) != '-' + || value.charAt(10) != 'T' + || value.charAt(13) != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(value, 0); + int month = parse2(value, 5); + int day = parse2(value, 8); + int hour = parse2(value, 11); + int minute = parse2(value, 14); + int second = 0; + int nano = 0; + int index = 16; + if (index < length && value.charAt(index) == ':') { + second = parse2(value, index + 1); + index += 3; + if (index < length && value.charAt(index) == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(value.charAt(fractionEnd))) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(value, fractionStart, fractionEnd); + index = fractionEnd; + } + } + int offsetSeconds = parseOffsetSeconds(value, index); + return OffsetDateTime.of( + year, month, day, hour, minute, second, nano, ZoneOffset.ofTotalSeconds(offsetSeconds)); + } + + private static int parseOffsetSeconds(String value, int index) { + int length = value.length(); + char offset = value.charAt(index); + if (offset == 'Z') { + if (index + 1 != length) { + throw new IllegalArgumentException(); + } + return 0; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 > length || value.charAt(index + 3) != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(value, index + 1); + int minute = parse2(value, index + 4); + int second = 0; + int end = index + 6; + if (end < length) { + if (end + 3 != length || value.charAt(end) != ':') { + throw new IllegalArgumentException(); + } + second = parse2(value, end + 1); + } + int total = hour * 3600 + minute * 60 + second; + return offset == '-' ? -total : total; + } + + private static int parseNano(String value, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + value.charAt(i) - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(String value, int index) { + return parse2(value, index) * 100 + parse2(value, index + 2); + } + + private static int parse2(String value, int index) { + int high = value.charAt(index) - '0'; + int low = value.charAt(index + 1) - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + public static final class AtomicBooleanCodec extends AbstractJsonCodec { public static final AtomicBooleanCodec INSTANCE = new AtomicBooleanCodec(); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index b2be63e1e3..ed79b7ce60 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -305,6 +305,8 @@ static boolean usesReadCodec(JsonFieldInfo property) { case COLLECTION: case MAP: return true; + case OBJECT: + return !usesReadObjectCodec(property); default: return false; } @@ -317,7 +319,7 @@ static boolean usesReadTypeField(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return usesReadObjectCodec(property); + return true; default: return false; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 1736168059..da7f5e87a0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -1017,6 +1017,8 @@ private static boolean usesReadCodec(JsonFieldInfo property) { case COLLECTION: case MAP: return true; + case OBJECT: + return !usesReadObjectCodec(property); default: return false; } @@ -1029,7 +1031,7 @@ private static boolean usesReadTypeField(JsonFieldInfo property) { case MAP: return true; case OBJECT: - return usesReadObjectCodec(property); + return true; default: return false; } @@ -1175,16 +1177,7 @@ private static Expression readRecordObject( Class type, JsonFieldInfo property, int id, int readerMode, Expression object) { if (property.readRawType() == Object.class || !(property.readTypeInfo().codec() instanceof BaseObjectCodec)) { - return assignRecord( - object, - id, - new Expression.Invoke( - fieldRef("p" + id, JsonFieldInfo.class), - "readValue", - TypeRef.of(Object.class), - true, - readerRef(readerMode), - typeResolverRef())); + return assignRecord(object, id, readResolvedValue(property, id, readerMode)); } return new Expression.If( tryReadNullExpr(readerMode), @@ -1331,12 +1324,7 @@ private static Expression readObject( Expression object) { if (property.readRawType() == Object.class || !(property.readTypeInfo().codec() instanceof BaseObjectCodec)) { - return new Expression.Invoke( - fieldRef("p" + id, JsonFieldInfo.class), - "read", - readerRef(readerMode), - object, - typeResolverRef()); + return readResolvedField(builder, property, id, readerMode, object); } return new Expression.If( tryReadNullExpr(readerMode), 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 df345945ea..855c58f411 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 @@ -19,6 +19,9 @@ package org.apache.fory.json.reader; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -623,6 +626,28 @@ public String readNullableStringToken() { return readStringToken(); } + public LocalDate readIsoLocalDate() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoLocalDateToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + + public OffsetDateTime readIsoOffsetDateTime() { + skipWhitespaceFast(); + int mark = position; + try { + return readIsoOffsetDateTimeToken(); + } catch (RuntimeException e) { + position = mark; + throw e; + } + } + private String readStringToken() { byte[] bytes = input; int inputLength = bytes.length; @@ -680,6 +705,161 @@ private String readStringToken() { throw error("Unterminated string"); } + private LocalDate readIsoLocalDateToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 12 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int dateStart = offset; + if (bytes[dateStart + 4] != '-' || bytes[dateStart + 7] != '-') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, dateStart); + int month = parse2(bytes, dateStart + 5); + int day = parse2(bytes, dateStart + 8); + int end = dateStart + 10; + int ch = bytes[end]; + if (ch == '"') { + position = end + 1; + return LocalDate.of(year, month, day); + } + if (ch == 'T') { + position = scanSimpleStringTail(bytes, end + 1); + return LocalDate.of(year, month, day); + } + throw new IllegalArgumentException(); + } + + private OffsetDateTime readIsoOffsetDateTimeToken() { + byte[] bytes = input; + int offset = position; + int length = bytes.length; + if (offset + 19 > length || bytes[offset++] != '"') { + throw error("Expected string"); + } + int start = offset; + if (bytes[start + 4] != '-' + || bytes[start + 7] != '-' + || bytes[start + 10] != 'T' + || bytes[start + 13] != ':') { + throw new IllegalArgumentException(); + } + int year = parse4(bytes, start); + int month = parse2(bytes, start + 5); + int day = parse2(bytes, start + 8); + int hour = parse2(bytes, start + 11); + int minute = parse2(bytes, start + 14); + int second = 0; + int nano = 0; + int index = start + 16; + if (index < length && bytes[index] == ':') { + second = parse2(bytes, index + 1); + index += 3; + if (index < length && bytes[index] == '.') { + int fractionStart = index + 1; + int fractionEnd = fractionStart; + while (fractionEnd < length && isDigit(bytes[fractionEnd])) { + fractionEnd++; + } + if (fractionEnd == fractionStart || fractionEnd - fractionStart > 9) { + throw new IllegalArgumentException(); + } + nano = parseNano(bytes, fractionStart, fractionEnd); + index = fractionEnd; + } + } + long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); + position = (int) offsetAndEnd; + return OffsetDateTime.of( + year, + month, + day, + hour, + minute, + second, + nano, + ZoneOffset.ofTotalSeconds((int) (offsetAndEnd >> 32))); + } + + private int scanSimpleStringTail(byte[] bytes, int offset) { + int length = bytes.length; + while (offset < length) { + int b = bytes[offset++]; + if (b == '"') { + return offset; + } + if (b == '\\' || b < 0x20 || b < 0) { + throw new IllegalArgumentException(); + } + } + throw error("Unterminated string"); + } + + private static long parseOffsetAndEnd(byte[] bytes, int index, int length) { + int offset = bytes[index]; + if (offset == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + return ((long) (index + 2)) & 0xFFFF_FFFFL; + } + if (offset != '+' && offset != '-') { + throw new IllegalArgumentException(); + } + if (index + 6 >= length || bytes[index + 3] != ':') { + throw new IllegalArgumentException(); + } + int hour = parse2(bytes, index + 1); + int minute = parse2(bytes, index + 4); + int second = 0; + int end = index + 6; + if (bytes[end] == ':') { + if (end + 3 >= length) { + throw new IllegalArgumentException(); + } + second = parse2(bytes, end + 1); + end += 3; + } + if (bytes[end] != '"') { + throw new IllegalArgumentException(); + } + int total = hour * 3600 + minute * 60 + second; + if (offset == '-') { + total = -total; + } + return ((long) total << 32) | ((long) (end + 1) & 0xFFFF_FFFFL); + } + + private static int parseNano(byte[] bytes, int start, int end) { + int nano = 0; + for (int i = start; i < end; i++) { + nano = nano * 10 + bytes[i] - '0'; + } + for (int i = end - start; i < 9; i++) { + nano *= 10; + } + return nano; + } + + private static int parse4(byte[] bytes, int index) { + return parse2(bytes, index) * 100 + parse2(bytes, index + 2); + } + + private static int parse2(byte[] bytes, int index) { + int high = bytes[index] - '0'; + int low = bytes[index + 1] - '0'; + if (high < 0 || high > 9 || low < 0 || low > 9) { + throw new IllegalArgumentException(); + } + return high * 10 + low; + } + + private static boolean isDigit(byte b) { + return b >= '0' && b <= '9'; + } + private String readStringStop(int start, int stop, int b) { position = stop + 1; if (b >= 0 && b < 0x20) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 3164b9d5af..070b45354b 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -126,7 +126,7 @@ public void writeNullFields() { ForyJson json = ForyJson.builder().writeNullFields(true).build(); assertEquals( json.toJson(new PublicFields()), - "{\"active\":true,\"id\":7,\"missing\":null,\"name\":\"fory\"}"); + "{\"active\":true,\"id\":7,\"name\":\"fory\",\"missing\":null}"); } @Test @@ -134,7 +134,7 @@ public void fieldOnlyModeIgnoresMethods() { ForyJson json = ForyJson.builder().withPropertyDiscovery(false).build(); assertEquals( json.toJson(new MethodsIgnored()), - "{\"hidden\":\"hidden\",\"setterCalls\":0,\"value\":\"field\"}"); + "{\"setterCalls\":0,\"value\":\"field\",\"hidden\":\"hidden\"}"); MethodsIgnored value = json.fromJson("{\"hidden\":\"json\",\"value\":\"json\"}", MethodsIgnored.class); assertEquals(hiddenValue(value), "json"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 84c1f8afbf..242aae6bf2 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -29,13 +29,25 @@ import java.nio.file.Paths; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.Optional; +import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.data.BoxedScalars; import org.apache.fory.json.data.CoreScalarFields; import org.apache.fory.json.data.NaturalObjectValue; import org.apache.fory.json.data.NaturalValues; import org.apache.fory.json.data.NumericBoundaries; import org.apache.fory.json.data.PublicFields; +import org.apache.fory.json.reader.JsonReader; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf16JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.json.resolver.JsonTypeInfo; +import org.apache.fory.json.resolver.JsonTypeResolver; +import org.apache.fory.json.writer.JsonWriter; +import org.apache.fory.json.writer.StringJsonWriter; +import org.apache.fory.json.writer.Utf8JsonWriter; import org.testng.annotations.Test; public class JsonScalarTest extends ForyJsonTestModels { @@ -225,11 +237,50 @@ public void readLocalDateFromDateTime() { ForyJson json = ForyJson.builder().build(); LocalDate expected = LocalDate.of(2023, 7, 2); assertEquals(json.fromJson("\"2023-07-02T16:00:00.000Z\"", LocalDate.class), expected); + assertEquals( + json.fromJson( + "\"2023-07-02T16:00:00.000Z\"".getBytes(StandardCharsets.UTF_8), LocalDate.class), + expected); LocalDateFields fields = json.fromJson("{\"value\":\"2023-07-02T16:00:00.000Z\"}", LocalDateFields.class); assertEquals(fields.value, expected); } + @Test + public void readOffsetDateTime() { + ForyJson json = ForyJson.builder().build(); + OffsetDateTime utc = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 0, ZoneOffset.UTC); + assertEquals(json.fromJson("\"2024-02-03T04:05:06Z\"", OffsetDateTime.class), utc); + assertEquals( + json.fromJson( + "\"2024-02-03T04:05:06\\u005A\"".getBytes(StandardCharsets.UTF_8), + OffsetDateTime.class), + utc); + + OffsetDateTime nanos = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + assertEquals( + json.fromJson( + "\"2024-02-03T04:05:06.123456789+08:30\"".getBytes(StandardCharsets.UTF_8), + OffsetDateTime.class), + nanos); + + OffsetDateTime minutePrecision = + OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 0, ZoneOffset.ofHoursMinutes(-5, -30)); + OffsetDateTimeFields fields = + json.fromJson("{\"value\":\"2024-02-03T04:05-05:30\"}", OffsetDateTimeFields.class); + assertEquals(fields.value, minutePrecision); + } + + @Test + public void objectFieldUsesUtf8Codec() { + ForyJson json = + ForyJson.builder().registerCodec(ModeAwareValue.class, new ModeAwareCodec()).build(); + ModeAwareHolder holder = + json.fromJson("{\"value\":{}}".getBytes(StandardCharsets.UTF_8), ModeAwareHolder.class); + assertEquals(holder.value.mode, "utf8"); + } + @Test public void wrapStringScalarParseErrors() { ForyJson json = ForyJson.builder().build(); @@ -263,4 +314,64 @@ public static final class FilePathFields { public static final class LocalDateFields { public LocalDate value; } + + public static final class OffsetDateTimeFields { + public OffsetDateTime value; + } + + public static final class ModeAwareHolder { + public ModeAwareValue value; + } + + public static final class ModeAwareValue { + public final String mode; + + ModeAwareValue(String mode) { + this.mode = mode; + } + } + + private static final class ModeAwareCodec implements JsonCodec { + @Override + public void write(JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public void writeString(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public void writeUtf8(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeNull(); + } + + @Override + public Object read(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("generic"); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("latin1"); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("utf16"); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.skipValue(); + return new ModeAwareValue("utf8"); + } + } } From 04d8e2d174cdda6dca1a1ce390e35c42e067edee Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 16:31:22 +0800 Subject: [PATCH 03/40] perf(java): specialize JSON string arrays --- .../apache/fory/json/codec/ArrayCodec.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 9724abbd92..4aad04c1f9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -58,6 +58,8 @@ public static ArrayCodec create(Class componentType, JsonTypeResolver resolve return FloatArrayCodec.INSTANCE; } else if (componentType == double.class) { return DoubleArrayCodec.INSTANCE; + } else if (componentType == String.class) { + return StringArrayCodec.INSTANCE; } return new ObjectArrayCodec(componentType, resolver.getTypeInfo(componentType, componentType)); } @@ -634,6 +636,145 @@ Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver re } } + public static final class StringArrayCodec extends ArrayCodec { + private static final StringArrayCodec INSTANCE = new StringArrayCodec(); + + private StringArrayCodec() { + super(String.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + if (array[i] == null) { + writer.writeNull(); + } else { + writer.writeString(array[i]); + } + } + writer.writeArrayEnd(); + } + + @Override + void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeStringElement(i, array[i]); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + String[] array = (String[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeStringElement(i, array[i]); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNullableString(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readLatin1( + Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf16( + Utf16JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new String[0]; + } + String[] values = new String[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readNextNullableString(); + } while (reader.consumeNextToken(',')); + reader.expectNextToken(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + public static final class ObjectArrayCodec extends ArrayCodec { private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; From 2163cd5f1f2de161df59b3be8a78e814af3eb143 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 17:19:04 +0800 Subject: [PATCH 04/40] perf(java): speed up Fory JSON long writes --- .../fory/json/writer/Utf8JsonWriter.java | 68 +++++++++---------- .../org/apache/fory/json/JsonScalarTest.java | 18 +++++ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 5ba1ec5e08..b55563ae0e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -31,6 +31,7 @@ public final class Utf8JsonWriter extends JsonWriter { "-2147483648".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = "-9223372036854775808".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); + private static final long EIGHT_DIGITS = 100_000_000L; private static final long HIGH_BITS = 0x8080808080808080L; private static final int INT_HIGH_BITS = 0x80808080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; @@ -115,16 +116,7 @@ public void writeLong(long value) { buffer[position++] = (byte) '-'; value = -value; } - if (value <= Integer.MAX_VALUE) { - writePositiveIntNoEnsure((int) value); - return; - } - int start = position; - do { - buffer[position++] = (byte) ('0' + value % 10); - value /= 10; - } while (value != 0); - reverse(start, position - 1); + writePositiveLongNoEnsure(value); } @Override @@ -672,14 +664,6 @@ private void writeByteRaw(byte value) { buffer[position++] = value; } - private void reverse(int start, int end) { - while (start < end) { - byte tmp = buffer[start]; - buffer[start++] = buffer[end]; - buffer[end--] = tmp; - } - } - private void ensure(int additional) { int minCapacity = position + additional; if (minCapacity > buffer.length) { @@ -750,40 +734,56 @@ private void writeLongNoEnsure(long value) { buffer[position++] = (byte) '-'; value = -value; } + writePositiveLongNoEnsure(value); + } + + private void writePositiveIntNoEnsure(int value) { + position = writePositiveInt(buffer, position, value); + } + + private void writePositiveLongNoEnsure(long value) { if (value <= Integer.MAX_VALUE) { writePositiveIntNoEnsure((int) value); return; } - int start = position; - do { - buffer[position++] = (byte) ('0' + value % 10); - value /= 10; - } while (value != 0); - reverse(start, position - 1); - } - - private void writePositiveIntNoEnsure(int value) { byte[] bytes = buffer; int pos = position; + long high = value / EIGHT_DIGITS; + int low = (int) (value - high * EIGHT_DIGITS); + if (high <= Integer.MAX_VALUE) { + pos = writePositiveInt(bytes, pos, (int) high); + } else { + long top = high / EIGHT_DIGITS; + int middle = (int) (high - top * EIGHT_DIGITS); + pos = writePositiveInt(bytes, pos, (int) top); + pos = writePadded8Digits(bytes, pos, middle); + } + position = writePadded8Digits(bytes, pos, low); + } + + private static int writePositiveInt(byte[] bytes, int pos, int value) { if (value < 10000) { - position = writeIntUpTo4(bytes, pos, value); - return; + return writeIntUpTo4(bytes, pos, value); } int high = divide10000(value); int low = value - high * 10000; if (high < 10000) { if (high >= 1000) { - position = writePadded8(bytes, pos, high, low); - return; + return writePadded8(bytes, pos, high, low); } pos = writeIntUpTo4(bytes, pos, high); - position = writePadded4(bytes, pos, low); - return; + return writePadded4(bytes, pos, low); } int top = divide10000(high); int middle = high - top * 10000; pos = writeIntUpTo4(bytes, pos, top); - position = writePadded8(bytes, pos, middle, low); + return writePadded8(bytes, pos, middle, low); + } + + private static int writePadded8Digits(byte[] bytes, int pos, int value) { + int high = divide10000(value); + int low = value - high * 10000; + return writePadded8(bytes, pos, high, low); } private static int divide10000(int value) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 242aae6bf2..ac5ee0a583 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -127,6 +127,24 @@ public void readNumericBoundaries() { "{\"intMax\":2147483648,\"text\":\"" + ZH_TEXT + "\"}", NumericBoundaries.class)); } + @Test + public void writeNumericBoundaries() { + ForyJson json = ForyJson.builder().build(); + NumericBoundaries value = new NumericBoundaries(); + value.intMax = Integer.MAX_VALUE; + value.intMin = Integer.MIN_VALUE; + value.longMax = Long.MAX_VALUE; + value.longMin = Long.MIN_VALUE; + value.small = -7; + value.text = "ok"; + String expected = + "{\"intMax\":2147483647,\"intMin\":-2147483648," + + "\"longMax\":9223372036854775807,\"longMin\":-9223372036854775808," + + "\"small\":-7,\"text\":\"ok\"}"; + assertEquals(json.toJson(value), expected); + assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); + } + @Test public void writeReadCoreScalarFields() { ForyJson json = ForyJson.builder().build(); From 430486d30520f9b3014eb2e4d0aa1d6a70a9e9b0 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 17:29:00 +0800 Subject: [PATCH 05/40] perf(java): write JSON scalar dates directly --- .../apache/fory/json/codec/ScalarCodecs.java | 17 ++- .../fory/json/writer/Utf8JsonWriter.java | 128 ++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 46 +++++++ 3 files changed, 190 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 0d928448dc..fa0caf8745 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -509,7 +509,7 @@ final void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolv } @Override - final void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeString(toJsonString(value)); } @@ -745,6 +745,11 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeUuid((UUID) value); + } + @Override Object fromJsonString(String value) { return UUID.fromString(value); @@ -898,6 +903,11 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeLocalDate((LocalDate) value); + } + @Override Object fromJsonString(String value) { return parseIsoLocalDate(value); @@ -1093,6 +1103,11 @@ String toJsonString(Object value) { return value.toString(); } + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + writer.writeOffsetDateTime((OffsetDateTime) value); + } + @Override Object fromJsonString(String value) { return parseIsoOffsetDateTime(value); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index b55563ae0e..a430cfcedd 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -19,7 +19,10 @@ package org.apache.fory.json.writer; +import java.time.LocalDate; +import java.time.OffsetDateTime; import java.util.Arrays; +import java.util.UUID; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.memory.LittleEndian; @@ -38,6 +41,8 @@ public final class Utf8JsonWriter extends JsonWriter { private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; + private static final byte[] HEX_DIGITS = + "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; @@ -168,6 +173,61 @@ public void writeString(String value) { writeStringChars(value); } + public void writeUuid(UUID value) { + ensure(38); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + long high = value.getMostSignificantBits(); + pos = writeHex(bytes, pos, high, 60, 8); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, high, 28, 4); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, high, 12, 4); + long low = value.getLeastSignificantBits(); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, low, 60, 4); + bytes[pos++] = (byte) '-'; + pos = writeHex(bytes, pos, low, 44, 12); + bytes[pos++] = (byte) '"'; + position = pos; + } + + public void writeLocalDate(LocalDate value) { + int year = value.getYear(); + if (year < 0 || year > 9999) { + writeString(value.toString()); + return; + } + ensure(12); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + bytes[pos++] = (byte) '"'; + position = pos; + } + + public void writeOffsetDateTime(OffsetDateTime value) { + int year = value.getYear(); + if (year < 0 || year > 9999 || value.getOffset().getTotalSeconds() != 0) { + writeString(value.toString()); + return; + } + ensure(32); + byte[] bytes = buffer; + int pos = position; + bytes[pos++] = (byte) '"'; + pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + bytes[pos++] = (byte) 'T'; + pos = + writeTime( + bytes, pos, value.getHour(), value.getMinute(), value.getSecond(), value.getNano()); + bytes[pos++] = (byte) 'Z'; + bytes[pos++] = (byte) '"'; + position = pos; + } + private void writeStringChars(String value) { int length = value.length(); ensure(length + 2); @@ -786,6 +846,74 @@ private static int writePadded8Digits(byte[] bytes, int pos, int value) { return writePadded8(bytes, pos, high, low); } + private static int writeHex(byte[] bytes, int pos, long value, int shift, int count) { + for (int i = 0; i < count; i++) { + bytes[pos++] = HEX_DIGITS[(int) ((value >>> shift) & 0xF)]; + shift -= 4; + } + return pos; + } + + private static int writeLocalDate(byte[] bytes, int pos, int year, int month, int day) { + pos = writePadded4(bytes, pos, year); + bytes[pos++] = (byte) '-'; + pos = writeTwoDigits(bytes, pos, month); + bytes[pos++] = (byte) '-'; + return writeTwoDigits(bytes, pos, day); + } + + private static int writeTime(byte[] bytes, int pos, int hour, int minute, int second, int nano) { + pos = writeTwoDigits(bytes, pos, hour); + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, minute); + if (second != 0 || nano != 0) { + bytes[pos++] = (byte) ':'; + pos = writeTwoDigits(bytes, pos, second); + if (nano != 0) { + bytes[pos++] = (byte) '.'; + pos = writeNano(bytes, pos, nano); + } + } + return pos; + } + + private static int writeNano(byte[] bytes, int pos, int nano) { + if (nano % 1_000_000 == 0) { + return writePadded3(bytes, pos, nano / 1_000_000); + } + if (nano % 1000 == 0) { + int micros = nano / 1000; + int high = micros / 1000; + int low = micros - high * 1000; + pos = writePadded3(bytes, pos, high); + return writePadded3(bytes, pos, low); + } + int first = nano / 100000000; + int rem = nano - first * 100000000; + int middle = rem / 10000; + int low = rem - middle * 10000; + bytes[pos++] = (byte) ('0' + first); + pos = writePadded4(bytes, pos, middle); + return writePadded4(bytes, pos, low); + } + + private static int writePadded3(byte[] bytes, int pos, int value) { + int high = value / 100; + int rem = value - high * 100; + int middle = rem / 10; + bytes[pos++] = (byte) ('0' + high); + bytes[pos++] = (byte) ('0' + middle); + bytes[pos++] = (byte) ('0' + (rem - middle * 10)); + return pos; + } + + private static int writeTwoDigits(byte[] bytes, int pos, int value) { + int high = value / 10; + bytes[pos++] = (byte) ('0' + high); + bytes[pos++] = (byte) ('0' + (value - high * 10)); + return pos; + } + private static int divide10000(int value) { return (int) (((long) value * 1759218605L) >> 44); } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index ac5ee0a583..a611405673 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -32,6 +32,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Optional; +import java.util.UUID; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.data.BoxedScalars; import org.apache.fory.json.data.CoreScalarFields; @@ -180,6 +181,51 @@ public void writeReadCoreScalarFields() { assertEquals(read.uuid, value.uuid); } + @Test + public void writeUtf8ScalarFormats() { + ForyJson json = ForyJson.builder().build(); + UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + assertEquals( + new String(json.toJsonBytes(uuid), StandardCharsets.UTF_8), + "\"123e4567-e89b-12d3-a456-426614174000\""); + assertEquals( + new String(json.toJsonBytes(LocalDate.of(2024, 2, 3)), StandardCharsets.UTF_8), + "\"2024-02-03\""); + + OffsetDateTimeFields fields = new OffsetDateTimeFields(); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 0, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 0, 1_000_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:00.001Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 120_000_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.120Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123_400_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.123400Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 1_000, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.000001Z\"}"); + fields.value = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"2024-02-03T04:05:06.123456789Z\"}"); + + OffsetDateTime offset = + OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.ofHoursMinutes(8, 30)); + fields.value = offset; + assertEquals( + new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), + "{\"value\":\"" + offset + "\"}"); + } + @Test public void readScalarRoots() { ForyJson json = ForyJson.builder().build(); From 256887eff09be5ae81532801803246f7fd18a22f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:03:04 +0800 Subject: [PATCH 06/40] feat(java): add Fory JSON OutputStream writes --- .../java/org/apache/fory/json/ForyJson.java | 23 +++++++++++++ .../fory/json/writer/Utf8JsonWriter.java | 10 ++++++ .../org/apache/fory/json/JsonObjectTest.java | 32 +++++++++++++++++++ 3 files changed, 65 insertions(+) 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 890e25b2a7..a5a0b43bad 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/ForyJson.java @@ -19,7 +19,9 @@ package org.apache.fory.json; +import java.io.OutputStream; import java.lang.reflect.Type; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.codec.GeneratedObjectCodec; @@ -117,6 +119,27 @@ public byte[] toJsonBytes(Object value) { } } + /** Serializes {@code value} as UTF-8 JSON to {@code output}. */ + public void writeJsonTo(Object value, OutputStream output) { + Objects.requireNonNull(output, "output"); + PooledState entry = acquire(); + JsonState state = entry.state; + Utf8JsonWriter writer = state.utf8Writer; + try { + if (value == null) { + writer.writeNull(); + } else { + JsonTypeResolver resolver = state.typeResolver; + JsonTypeInfo typeInfo = state.rootTypeInfo(value.getClass()); + typeInfo.codec().writeUtf8(writer, value, resolver); + } + writer.writeTo(output); + } finally { + writer.reset(); + release(entry); + } + } + public T fromJson(String json, Class type) { PooledState entry = acquire(); JsonState state = entry.state; diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index a430cfcedd..7d3e5510ee 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -19,6 +19,8 @@ package org.apache.fory.json.writer; +import java.io.IOException; +import java.io.OutputStream; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Arrays; @@ -94,6 +96,14 @@ public byte[] toJsonBytes() { return Arrays.copyOf(buffer, position); } + public void writeTo(OutputStream output) { + try { + output.write(buffer, 0, position); + } catch (IOException e) { + throw new ForyJsonException("Cannot write JSON output", e); + } + } + @Override public void writeNull() { writeAscii("null"); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java index 070b45354b..1fc6ba3bca 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonObjectTest.java @@ -20,7 +20,11 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -48,6 +52,34 @@ public void writePublicFields() { "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); } + @Test + public void writeJsonToOutputStream() { + ForyJson json = ForyJson.builder().build(); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + json.writeJsonTo(new PublicFields(), output); + assertEquals( + new String(output.toByteArray(), StandardCharsets.UTF_8), + "{\"active\":true,\"id\":7,\"name\":\"fory\"}"); + + output.reset(); + json.writeJsonTo(null, output); + assertEquals(new String(output.toByteArray(), StandardCharsets.UTF_8), "null"); + + OutputStream failing = + new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("closed"); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + throw new IOException("closed"); + } + }; + assertThrows(ForyJsonException.class, () -> json.writeJsonTo(new PublicFields(), failing)); + } + @Test public void writeFirstIntGenerated() { ForyJson json = ForyJson.builder().build(); From 6b633dfcfe720490bd0b25d63e5da7d599a74b36 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:13:04 +0800 Subject: [PATCH 07/40] perf(java): specialize Fory JSON generated scalars --- .../fory/json/codegen/JsonWriterCodegen.java | 23 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 22 ++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index 689dfa2951..dd555048d6 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -19,6 +19,9 @@ package org.apache.fory.json.codegen; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.UUID; import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; @@ -454,6 +457,9 @@ private Expression writeValue( return writeStringCollection(value, utf8); } return writeCodec(id, value, utf8); + case OBJECT: + Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8); + return scalar == null ? writeCodec(id, value, utf8) : scalar; default: return writeCodec(id, value, utf8); } @@ -501,6 +507,23 @@ private static Expression writeCodec(int id, Expression value, boolean utf8) { typeResolverRef()); } + private static Expression writeExactUtf8Scalar(Class rawType, Expression value, boolean utf8) { + if (!utf8) { + return null; + } + String writerMethod; + if (rawType == UUID.class) { + writerMethod = "writeUuid"; + } else if (rawType == LocalDate.class) { + writerMethod = "writeLocalDate"; + } else if (rawType == OffsetDateTime.class) { + writerMethod = "writeOffsetDateTime"; + } else { + return null; + } + return new Expression.Invoke(writerRef(true), writerMethod, value); + } + private static Expression writeStringCollection(Expression value, boolean utf8) { return new Expression.ListExpression( new Expression.Invoke(writerRef(utf8), "writeArrayStart"), diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index a611405673..c3c8a7d2e9 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -226,6 +226,22 @@ public void writeUtf8ScalarFormats() { "{\"value\":\"" + offset + "\"}"); } + @Test + public void writeGeneratedUtf8Scalars() { + ForyJson json = ForyJson.builder().build(); + Utf8ScalarFields fields = new Utf8ScalarFields(); + fields.uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + fields.date = LocalDate.of(2024, 2, 3); + fields.timestamp = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); + String expected = + "{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06.123456789Z\"}"; + assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); + assertEquals(json.toJson(fields), expected); + assertGeneratedWhenSupported(json, Utf8ScalarFields.class); + } + @Test public void readScalarRoots() { ForyJson json = ForyJson.builder().build(); @@ -383,6 +399,12 @@ public static final class OffsetDateTimeFields { public OffsetDateTime value; } + public static final class Utf8ScalarFields { + public UUID uuid; + public LocalDate date; + public OffsetDateTime timestamp; + } + public static final class ModeAwareHolder { public ModeAwareValue value; } From 8ea53d651e6317a5d16355ebdada253e470c1cdb Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:35:01 +0800 Subject: [PATCH 08/40] perf(java): optimize Fory JSON string collections --- .../fory/json/codegen/JsonWriterCodegen.java | 3 +++ .../fory/json/writer/Utf8JsonWriter.java | 18 ++++++++++++++++++ .../org/apache/fory/json/JsonRecordTest.java | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index dd555048d6..c5af91c933 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -525,6 +525,9 @@ private static Expression writeExactUtf8Scalar(Class rawType, Expression valu } private static Expression writeStringCollection(Expression value, boolean utf8) { + if (utf8) { + return new Expression.Invoke(writerRef(true), "writeStringCollection", value); + } return new Expression.ListExpression( new Expression.Invoke(writerRef(utf8), "writeArrayStart"), new Expression.ForEach( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 7d3e5510ee..420a5a78a7 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -23,7 +23,9 @@ import java.io.OutputStream; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.UUID; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonFieldInfo; @@ -386,6 +388,22 @@ private void writeStringFieldChars(byte[] prefix, String value) { writeStringNoEnsure(value); } + public void writeStringCollection(Collection values) { + writeArrayStart(); + if (values.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) values; + for (int i = 0, size = list.size(); i < size; i++) { + writeStringElement(i, list.get(i)); + } + } else { + int index = 0; + for (String value : values) { + writeStringElement(index++, value); + } + } + writeArrayEnd(); + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java index b65328b5bc..d2b23f264f 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonRecordTest.java @@ -50,7 +50,7 @@ public void writeReadRecordClass() throws Exception { .newInstance(7, ZH_TEXT, Arrays.asList("a", "b"), child); ForyJson json = ForyJson.builder().build(); String expected = - "{\"child\":{\"label\":\"kid\"},\"id\":7,\"name\":\"你好,Fory\"," + "\"tags\":[\"a\",\"b\"]}"; + "{\"id\":7,\"name\":\"你好,Fory\",\"tags\":[\"a\",\"b\"]," + "\"child\":{\"label\":\"kid\"}}"; assertEquals(json.toJson(value), expected); assertEquals(new String(json.toJsonBytes(value), StandardCharsets.UTF_8), expected); assertGeneratedWhenSupported(json, type); From 40f27e9a6eae48cdc2ccf23af4ad5501312a3ec8 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 18:49:06 +0800 Subject: [PATCH 09/40] perf(java): specialize Fory JSON BigDecimal writes --- .../org/apache/fory/json/codegen/JsonWriterCodegen.java | 6 ++++++ .../src/test/java/org/apache/fory/json/JsonScalarTest.java | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index c5af91c933..f9dd5e6b5f 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -19,6 +19,7 @@ package org.apache.fory.json.codegen; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.UUID; @@ -518,6 +519,11 @@ private static Expression writeExactUtf8Scalar(Class rawType, Expression valu writerMethod = "writeLocalDate"; } else if (rawType == OffsetDateTime.class) { writerMethod = "writeOffsetDateTime"; + } else if (rawType == BigDecimal.class) { + return new Expression.Invoke( + writerRef(true), + "writeNumber", + new Expression.Invoke(value, "toString", TypeRef.of(String.class)).inline()); } else { return null; } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index c3c8a7d2e9..3f1dcdefe1 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -23,6 +23,7 @@ import static org.testng.Assert.assertThrows; import java.io.File; +import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -231,10 +232,12 @@ public void writeGeneratedUtf8Scalars() { ForyJson json = ForyJson.builder().build(); Utf8ScalarFields fields = new Utf8ScalarFields(); fields.uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"); + fields.decimal = new BigDecimal("12345.6789"); fields.date = LocalDate.of(2024, 2, 3); fields.timestamp = OffsetDateTime.of(2024, 2, 3, 4, 5, 6, 123456789, ZoneOffset.UTC); String expected = "{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":12345.6789," + "\"date\":\"2024-02-03\"," + "\"timestamp\":\"2024-02-03T04:05:06.123456789Z\"}"; assertEquals(new String(json.toJsonBytes(fields), StandardCharsets.UTF_8), expected); @@ -401,6 +404,7 @@ public static final class OffsetDateTimeFields { public static final class Utf8ScalarFields { public UUID uuid; + public BigDecimal decimal; public LocalDate date; public OffsetDateTime timestamp; } From 2ab0254dcc36cdad5a0ea1892701ae521da76bab Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 19:29:09 +0800 Subject: [PATCH 10/40] perf(java): speed up Fory JSON BigDecimal reads --- .../apache/fory/json/codec/ScalarCodecs.java | 9 ++ .../fory/json/reader/Utf8JsonReader.java | 83 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 20 +++++ 3 files changed, 112 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index fa0caf8745..83ee1ed779 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -581,6 +581,15 @@ String toJsonNumber(Object value) { Object fromJsonNumber(String value) { return new BigDecimal(value); } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + return reader.readBigDecimal(); + } } public static final class Float16Codec extends AbstractJsonCodec { 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 855c58f411..7c74b74b61 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 @@ -19,6 +19,7 @@ package org.apache.fory.json.reader; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -354,6 +355,11 @@ public long readLongTokenValue() { return readLongToken(); } + public BigDecimal readBigDecimal() { + skipWhitespaceFast(); + return readBigDecimalToken(); + } + private long readLongToken() { byte[] bytes = input; int offset = position; @@ -456,6 +462,83 @@ private long readNegativeLongToken(int start) { return result; } + private BigDecimal readBigDecimalToken() { + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + boolean negative = false; + int ch = bytes[offset]; + if (ch == '-') { + negative = true; + offset++; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + ch = bytes[offset]; + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { + return readBigDecimalFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } + } + position = offset; + return BigDecimal.valueOf(negative ? -unscaled : unscaled, scale); + } + + private BigDecimal readBigDecimalFallback(int start) { + // Keep overflow and exponent forms on the existing string constructor path so the fast path + // only owns decimals that fit exactly as long + scale. + position = start; + return new BigDecimal(readNumber()); + } + @Override public int readFieldNameInt() { skipWhitespaceFast(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 3f1dcdefe1..c0697e3cd6 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -254,6 +254,26 @@ public void readScalarRoots() { assertEquals( json.fromJson("\"\uD83D\uDE00\u1234\"".getBytes(StandardCharsets.UTF_8), String.class), "\uD83D\uDE00\u1234"); + assertEquals( + json.fromJson("0.100".getBytes(StandardCharsets.UTF_8), BigDecimal.class), + new BigDecimal("0.100")); + assertEquals( + json.fromJson( + "12345678901234567890.123".getBytes(StandardCharsets.UTF_8), BigDecimal.class), + new BigDecimal("12345678901234567890.123")); + } + + @Test + public void readGeneratedUtf8BigDecimal() { + ForyJson json = ForyJson.builder().build(); + byte[] input = + ("{\"uuid\":\"123e4567-e89b-12d3-a456-426614174000\"," + + "\"decimal\":0.12345678901234567," + + "\"date\":\"2024-02-03\"," + + "\"timestamp\":\"2024-02-03T04:05:06Z\"}") + .getBytes(StandardCharsets.UTF_8); + Utf8ScalarFields fields = json.fromJson(input, Utf8ScalarFields.class); + assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); } @Test From c48e586e43013466b89501412276ebc0afea7c67 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 19:47:48 +0800 Subject: [PATCH 11/40] perf(java): fast-path UTC JSON timestamps --- .../java/org/apache/fory/json/reader/Utf8JsonReader.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 7c74b74b61..57372d529c 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 @@ -853,6 +853,13 @@ private OffsetDateTime readIsoOffsetDateTimeToken() { index = fractionEnd; } } + if (index < length && bytes[index] == 'Z') { + if (index + 1 >= length || bytes[index + 1] != '"') { + throw new IllegalArgumentException(); + } + position = index + 2; + return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); + } long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); position = (int) offsetAndEnd; return OffsetDateTime.of( From e3b320f80139520de5282731ea421d912ceffbc4 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 20:42:27 +0800 Subject: [PATCH 12/40] perf(java): split Fory JSON generated writers --- .../codegen/JsonGeneratedCodecBuilder.java | 5 +- .../fory/json/codegen/JsonWriterCodegen.java | 214 ++++++++++++------ 2 files changed, 147 insertions(+), 72 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java index 55b0f463a3..33ee0e568a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonGeneratedCodecBuilder.java @@ -121,12 +121,15 @@ private Method recordReadMethod(Field field) { Expression fieldValue(JsonFieldInfo property, Expression object) { Method getter = property.writeGetter(); if (getter != null) { + // JSON writers check the returned member value directly. Requesting expression-level null + // state here only emits an unused boolean for each nullable getter and bloats generated + // object writers enough to hurt C2 inlining. return new Expression.Invoke( object, getter.getName(), property.name(), TypeRef.of(getter.getGenericReturnType()), - !getter.getReturnType().isPrimitive()); + false); } return getFieldValue(object, writeDescriptor(property)); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index f9dd5e6b5f..a5cca49ba9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -22,11 +22,15 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; import java.util.UUID; import org.apache.fory.codegen.Code; import org.apache.fory.codegen.CodegenContext; import org.apache.fory.codegen.Expression; import org.apache.fory.codegen.Expression.Reference; +import org.apache.fory.codegen.ExpressionOptimizer; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.meta.JsonFieldInfo; @@ -40,6 +44,11 @@ import org.apache.fory.reflect.TypeRef; final class JsonWriterCodegen { + private static final int MIN_SPLIT_MEMBERS = 12; + // Keep generated member helpers below C2's big-method range without fragmenting them into + // tiny calls. This mirrors Fory core's object-codec split strategy for large generated codecs. + private static final int MEMBER_GROUP_SIZE = 10; + private final JsonCodegen codegen; private final boolean writeNullFields; @@ -213,31 +222,81 @@ private Expression writeExpression( "object", new Expression.Cast( new Reference("value", TypeRef.of(Object.class)), TypeRef.of(type))); + Reference writer = writerRef(utf8); + Reference typeResolver = typeResolverRef(); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); Expression index = null; if (!objectStartFused) { - expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectStart")); + expressions.add(new Expression.Invoke(writer, "writeObjectStart")); index = new Expression.Variable("index", Expression.Literal.ofInt(0)); expressions.add(index); } boolean commaKnown = objectStartFused; + boolean splitMembers = properties.length >= MIN_SPLIT_MEMBERS; + List memberGroup = splitMembers ? new ArrayList<>(MEMBER_GROUP_SIZE) : null; for (int i = 0; i < properties.length; i++) { + Expression member; if (objectStartFused && i == 0) { - expressions.add( + member = writeObjectStartPrimitive( - properties[i], builder.fieldValue(properties[i], object), utf8)); + properties[i], builder.fieldValue(properties[i], object), utf8, writer); } else { - expressions.add(writeProp(builder, properties[i], i, utf8, commaKnown, index, object)); + member = + writeProp( + builder, properties[i], i, utf8, commaKnown, index, object, writer, typeResolver); + } + if (splitMembers && commaKnown) { + memberGroup.add(member); + if (memberGroup.size() == MEMBER_GROUP_SIZE) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + } else { + if (splitMembers) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + expressions.add(member); } if (writeNullFields || properties[i].writeRawType().isPrimitive()) { commaKnown = true; } } - expressions.add(new Expression.Invoke(writerRef(utf8), "writeObjectEnd")); + if (splitMembers) { + addMemberGroup(builder, expressions, memberGroup, object, writer, typeResolver); + } + expressions.add(new Expression.Invoke(writer, "writeObjectEnd")); return expressions; } + private static void addMemberGroup( + JsonGeneratedCodecBuilder builder, + Expression.ListExpression expressions, + List memberGroup, + Expression object, + Reference writer, + Reference typeResolver) { + if (memberGroup.isEmpty()) { + return; + } + if (memberGroup.size() == 1) { + expressions.add(memberGroup.get(0)); + memberGroup.clear(); + return; + } + LinkedHashSet cutPoints = new LinkedHashSet<>(); + cutPoints.add(object); + cutPoints.add(writer); + cutPoints.add(typeResolver); + expressions.add( + ExpressionOptimizer.invokeGenerated( + builder.context(), + cutPoints, + new Expression.ListExpression(new ArrayList<>(memberGroup)), + "writeMembers", + false)); + memberGroup.clear(); + } + private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { if (properties.length == 0 || !properties[0].writeRawType().isPrimitive()) { return false; @@ -254,16 +313,16 @@ private static boolean canFuseObjectStart(JsonFieldInfo[] properties) { } private static Expression writeObjectStartPrimitive( - JsonFieldInfo property, Expression value, boolean utf8) { + JsonFieldInfo property, Expression value, boolean utf8, Expression writer) { switch (property.writeKind()) { case BYTE: case SHORT: case INT: return new Expression.Invoke( - writerRef(utf8), "writeObjectIntField", prefixRef(utf8, false, 0), value); + writer, "writeObjectIntField", prefixRef(utf8, false, 0), value); case LONG: return new Expression.Invoke( - writerRef(utf8), "writeObjectLongField", prefixRef(utf8, false, 0), value); + writer, "writeObjectLongField", prefixRef(utf8, false, 0), value); default: throw new ForyJsonException( "Unsupported generated object-start kind " + property.writeKind()); @@ -277,11 +336,13 @@ private Expression writeProp( boolean utf8, boolean commaKnown, Expression index, - Expression object) { + Expression object, + Expression writer, + Expression typeResolver) { Class rawType = property.writeRawType(); if (rawType.isPrimitive()) { return writePrimitive( - property, id, builder.fieldValue(property, object), utf8, commaKnown, index); + property, id, builder.fieldValue(property, object), utf8, commaKnown, index, writer); } Expression value = new Expression.Variable( @@ -295,24 +356,24 @@ private Expression writeProp( new Expression.If( eq(value, nullValue), new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - new Expression.Invoke(writerRef(utf8), "writeNull")), - writeValue(property, id, value, utf8, commaKnown, index))); + writeFieldName(id, utf8, commaKnown, index, writer), + new Expression.Invoke(writer, "writeNull")), + writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver))); } return new Expression.ListExpression( value, - writeFieldName(id, utf8, commaKnown, index), + writeFieldName(id, utf8, commaKnown, index, writer), new Expression.If( eq(value, nullValue), - new Expression.Invoke(writerRef(utf8), "writeNull"), - writeValue(property, id, value, utf8, true, index))); + new Expression.Invoke(writer, "writeNull"), + writeValue(property, id, value, utf8, true, index, writer, typeResolver))); } Expression write = isPrefixValue(property.writeKind()) - ? writeValue(property, id, value, utf8, commaKnown, index) + ? writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver) : new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - writeValue(property, id, value, utf8, true, index)); + writeFieldName(id, utf8, commaKnown, index, writer), + writeValue(property, id, value, utf8, true, index, writer, typeResolver)); return new Expression.ListExpression(value, new Expression.If(ne(value, nullValue), write)); } @@ -322,21 +383,22 @@ private Expression writePrimitive( Expression value, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer) { switch (property.writeKind()) { case BOOLEAN: return writeRawFieldValue( - utf8, commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index)); + commaKnown, index, booleanFieldValue(id, value, utf8, commaKnown, index), writer); case BYTE: case SHORT: case INT: - return writeNumberField(id, value, false, utf8, commaKnown, index); + return writeNumberField(id, value, false, utf8, commaKnown, index, writer); case LONG: - return writeNumberField(id, value, true, utf8, commaKnown, index); + return writeNumberField(id, value, true, utf8, commaKnown, index, writer); default: return new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index), - writePrimitiveScalar(property.writeKind(), value, utf8)); + writeFieldName(id, utf8, commaKnown, index, writer), + writePrimitiveScalar(property.writeKind(), value, writer)); } } @@ -346,15 +408,16 @@ private static Expression writeNumberField( boolean longValue, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer) { String writerMethod = longValue ? "writeLongField" : "writeIntField"; if (commaKnown) { - return new Expression.Invoke(writerRef(utf8), writerMethod, prefixRef(utf8, true, id), value); + return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( - writerRef(utf8), + writer, writerMethod, prefixRef(utf8, false, id), prefixRef(utf8, true, id), @@ -365,15 +428,19 @@ private static Expression writeNumberField( } private static Expression writeStringField( - int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { + int id, + Expression value, + boolean utf8, + boolean commaKnown, + Expression index, + Expression writer) { if (commaKnown) { - return new Expression.Invoke( - writerRef(utf8), "writeStringField", prefixRef(utf8, true, id), value); + return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = new Expression.ListExpression( new Expression.Invoke( - writerRef(utf8), + writer, "writeStringField", prefixRef(utf8, false, id), prefixRef(utf8, true, id), @@ -384,7 +451,7 @@ private static Expression writeStringField( } private static Expression writeFieldName( - int id, boolean utf8, boolean commaKnown, Expression index) { + int id, boolean utf8, boolean commaKnown, Expression index, Expression writer) { Expression prefix = commaKnown ? prefixRef(utf8, true, id) @@ -395,8 +462,7 @@ private static Expression writeFieldName( true, TypeRef.of(byte[].class)); Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeRawValue", prefix)); + new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", prefix)); if (!commaKnown) { expressions.add(increment(index)); } @@ -409,12 +475,13 @@ private Expression writeValue( Expression value, boolean utf8, boolean commaKnown, - Expression index) { + Expression index, + Expression writer, + Expression typeResolver) { JsonFieldKind kind = property.writeKind(); switch (kind) { case BOOLEAN: return writeRawFieldValue( - utf8, commaKnown, index, booleanFieldValue( @@ -422,7 +489,8 @@ private Expression writeValue( new Expression.Invoke(value, "booleanValue", TypeRef.of(boolean.class)).inline(), utf8, commaKnown, - index)); + index), + writer); case BYTE: case SHORT: case INT: @@ -432,7 +500,8 @@ private Expression writeValue( false, utf8, commaKnown, - index); + index, + writer); case LONG: return writeNumberField( id, @@ -440,37 +509,37 @@ private Expression writeValue( true, utf8, commaKnown, - index); + index, + writer); case STRING: - return writeStringField(id, value, utf8, commaKnown, index); + return writeStringField(id, value, utf8, commaKnown, index, writer); case ENUM: return writeRawFieldValue( - utf8, commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index)); + commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index), writer); case FLOAT: case DOUBLE: case CHAR: - return writeScalar(kind, value, utf8); + return writeScalar(kind, value, writer); case ARRAY: case MAP: - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); case COLLECTION: if (property.writeElementRawType() == String.class) { - return writeStringCollection(value, utf8); + return writeStringCollection(value, utf8, writer); } - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); case OBJECT: - Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8); - return scalar == null ? writeCodec(id, value, utf8) : scalar; + Expression scalar = writeExactUtf8Scalar(property.writeRawType(), value, utf8, writer); + return scalar == null ? writeCodec(id, value, utf8, writer, typeResolver) : scalar; default: - return writeCodec(id, value, utf8); + return writeCodec(id, value, utf8, writer, typeResolver); } } private static Expression writeRawFieldValue( - boolean utf8, boolean commaKnown, Expression index, Expression value) { + boolean commaKnown, Expression index, Expression value, Expression writer) { Expression.ListExpression expressions = - new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeRawValue", value)); + new Expression.ListExpression(new Expression.Invoke(writer, "writeRawValue", value)); if (!commaKnown) { expressions.add(increment(index)); } @@ -499,16 +568,18 @@ private static Expression enumFieldValue( .inline(); } - private static Expression writeCodec(int id, Expression value, boolean utf8) { + private static Expression writeCodec( + int id, Expression value, boolean utf8, Expression writer, Expression typeResolver) { return new Expression.Invoke( fieldRef("c" + id, JsonCodec.class), utf8 ? "writeUtf8" : "writeString", - writerRef(utf8), + writer, value, - typeResolverRef()); + typeResolver); } - private static Expression writeExactUtf8Scalar(Class rawType, Expression value, boolean utf8) { + private static Expression writeExactUtf8Scalar( + Class rawType, Expression value, boolean utf8, Expression writer) { if (!utf8) { return null; } @@ -521,45 +592,46 @@ private static Expression writeExactUtf8Scalar(Class rawType, Expression valu writerMethod = "writeOffsetDateTime"; } else if (rawType == BigDecimal.class) { return new Expression.Invoke( - writerRef(true), + writer, "writeNumber", new Expression.Invoke(value, "toString", TypeRef.of(String.class)).inline()); } else { return null; } - return new Expression.Invoke(writerRef(true), writerMethod, value); + return new Expression.Invoke(writer, writerMethod, value); } - private static Expression writeStringCollection(Expression value, boolean utf8) { + private static Expression writeStringCollection( + Expression value, boolean utf8, Expression writer) { if (utf8) { - return new Expression.Invoke(writerRef(true), "writeStringCollection", value); + return new Expression.Invoke(writer, "writeStringCollection", value); } return new Expression.ListExpression( - new Expression.Invoke(writerRef(utf8), "writeArrayStart"), + new Expression.Invoke(writer, "writeArrayStart"), new Expression.ForEach( value, TypeRef.of(String.class), true, (index, element) -> - new Expression.Invoke(writerRef(utf8), "writeStringElement", index, element)), - new Expression.Invoke(writerRef(utf8), "writeArrayEnd")); + new Expression.Invoke(writer, "writeStringElement", index, element)), + new Expression.Invoke(writer, "writeArrayEnd")); } - private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf8) { + private Expression writeScalar(JsonFieldKind kind, Expression value, Expression writer) { switch (kind) { case FLOAT: return new Expression.Invoke( - writerRef(utf8), + writer, "writeFloat", new Expression.Invoke(value, "floatValue", TypeRef.of(float.class)).inline()); case DOUBLE: return new Expression.Invoke( - writerRef(utf8), + writer, "writeDouble", new Expression.Invoke(value, "doubleValue", TypeRef.of(double.class)).inline()); case CHAR: return new Expression.Invoke( - writerRef(utf8), + writer, "writeChar", new Expression.Invoke(value, "charValue", TypeRef.of(char.class)).inline()); default: @@ -567,14 +639,14 @@ private Expression writeScalar(JsonFieldKind kind, Expression value, boolean utf } } - private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, boolean utf8) { + private Expression writePrimitiveScalar(JsonFieldKind kind, Expression value, Expression writer) { switch (kind) { case FLOAT: - return new Expression.Invoke(writerRef(utf8), "writeFloat", value); + return new Expression.Invoke(writer, "writeFloat", value); case DOUBLE: - return new Expression.Invoke(writerRef(utf8), "writeDouble", value); + return new Expression.Invoke(writer, "writeDouble", value); case CHAR: - return new Expression.Invoke(writerRef(utf8), "writeChar", value); + return new Expression.Invoke(writer, "writeChar", value); default: throw new ForyJsonException("Unsupported generated primitive kind " + kind); } From 1a8c83cdb4889873daf78c0a69146e98ec4f4bfd Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 20:55:14 +0800 Subject: [PATCH 13/40] perf(java): add Fory JSON common array codecs --- .../apache/fory/json/codec/ArrayCodec.java | 382 +++++++++++++++++- .../apache/fory/json/codec/ScalarCodecs.java | 157 +++++++ .../json/resolver/JsonSharedRegistry.java | 9 + .../apache/fory/json/JsonContainerTest.java | 100 +++++ .../org/apache/fory/json/JsonScalarTest.java | 24 ++ 5 files changed, 671 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 4aad04c1f9..de3ca843b0 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -58,6 +58,22 @@ public static ArrayCodec create(Class componentType, JsonTypeResolver resolve return FloatArrayCodec.INSTANCE; } else if (componentType == double.class) { return DoubleArrayCodec.INSTANCE; + } else if (componentType == Integer.class) { + return BoxedIntArrayCodec.INSTANCE; + } else if (componentType == Long.class) { + return BoxedLongArrayCodec.INSTANCE; + } else if (componentType == Boolean.class) { + return BoxedBooleanArrayCodec.INSTANCE; + } else if (componentType == Short.class) { + return BoxedShortArrayCodec.INSTANCE; + } else if (componentType == Byte.class) { + return BoxedByteArrayCodec.INSTANCE; + } else if (componentType == Character.class) { + return BoxedCharArrayCodec.INSTANCE; + } else if (componentType == Float.class) { + return BoxedFloatArrayCodec.INSTANCE; + } else if (componentType == Double.class) { + return BoxedDoubleArrayCodec.INSTANCE; } else if (componentType == String.class) { return StringArrayCodec.INSTANCE; } @@ -842,6 +858,367 @@ private Object toArray(List values) { } } + public static final class BoxedIntArrayCodec extends ArrayCodec { + private static final BoxedIntArrayCodec INSTANCE = new BoxedIntArrayCodec(); + + private BoxedIntArrayCodec() { + super(Integer.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Integer[] array = (Integer[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Integer element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Integer[0]; + } + Integer[] values = new Integer[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedLongArrayCodec extends ArrayCodec { + private static final BoxedLongArrayCodec INSTANCE = new BoxedLongArrayCodec(); + + private BoxedLongArrayCodec() { + super(Long.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Long[] array = (Long[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Long element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeLong(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Long[0]; + } + Long[] values = new Long[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedBooleanArrayCodec extends ArrayCodec { + private static final BoxedBooleanArrayCodec INSTANCE = new BoxedBooleanArrayCodec(); + + private BoxedBooleanArrayCodec() { + super(Boolean.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Boolean[] array = (Boolean[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Boolean element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeBoolean(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Boolean[0]; + } + Boolean[] values = new Boolean[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : reader.readBoolean(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedShortArrayCodec extends ArrayCodec { + private static final BoxedShortArrayCodec INSTANCE = new BoxedShortArrayCodec(); + + private BoxedShortArrayCodec() { + super(Short.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Short[] array = (Short[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Short element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Short[0]; + } + Short[] values = new Short[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : readShort(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedByteArrayCodec extends ArrayCodec { + private static final BoxedByteArrayCodec INSTANCE = new BoxedByteArrayCodec(); + + private BoxedByteArrayCodec() { + super(Byte.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Byte[] array = (Byte[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Byte element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeInt(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Byte[0]; + } + Byte[] values = new Byte[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : readByte(reader.readInt()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedCharArrayCodec extends ArrayCodec { + private static final BoxedCharArrayCodec INSTANCE = new BoxedCharArrayCodec(); + + private BoxedCharArrayCodec() { + super(Character.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Character[] array = (Character[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Character element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeChar(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Character[0]; + } + Character[] values = new Character[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + String value = reader.readNullableString(); + values[size++] = value == null ? null : readChar(value); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedFloatArrayCodec extends ArrayCodec { + private static final BoxedFloatArrayCodec INSTANCE = new BoxedFloatArrayCodec(); + + private BoxedFloatArrayCodec() { + super(Float.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Float[] array = (Float[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Float element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeFloat(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Float[0]; + } + Float[] values = new Float[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : Float.parseFloat(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + + public static final class BoxedDoubleArrayCodec extends ArrayCodec { + private static final BoxedDoubleArrayCodec INSTANCE = new BoxedDoubleArrayCodec(); + + private BoxedDoubleArrayCodec() { + super(Double.class); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + Double[] array = (Double[]) value; + writer.writeArrayStart(); + for (int i = 0; i < array.length; i++) { + writer.writeComma(i); + Double element = array[i]; + if (element == null) { + writer.writeNull(); + } else { + writer.writeDouble(element); + } + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new Double[0]; + } + Double[] values = new Double[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.tryReadNull() ? null : Double.parseDouble(reader.readNumber()); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return Arrays.copyOf(values, size); + } + } + private static void rejectNull(JsonReader reader) { if (reader.tryReadNull()) { throw new ForyJsonException("Cannot read null into primitive array element"); @@ -881,7 +1258,10 @@ private static byte readByte(int value) { } private static char readChar(JsonReader reader) { - String value = reader.readString(); + return readChar(reader.readString()); + } + + private static char readChar(String value) { if (value.length() != 1) { throw new ForyJsonException("Expected one-character JSON string for char"); } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 83ee1ed779..fbb00260b2 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -56,8 +56,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.meta.JsonAsciiToken; @@ -1351,6 +1354,160 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso } } + public static final class AtomicIntegerArrayCodec extends AbstractJsonCodec { + public static final AtomicIntegerArrayCodec INSTANCE = new AtomicIntegerArrayCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicIntegerArray array = (AtomicIntegerArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeInt(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicIntegerArray array = (AtomicIntegerArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeInt(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicIntegerArray(0); + } + int[] values = new int[8]; + int size = 0; + do { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into AtomicIntegerArray element"); + } + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readInt(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicIntegerArray(Arrays.copyOf(values, size)); + } + } + + public static final class AtomicLongArrayCodec extends AbstractJsonCodec { + public static final AtomicLongArrayCodec INSTANCE = new AtomicLongArrayCodec(); + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicLongArray array = (AtomicLongArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeLong(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicLongArray array = (AtomicLongArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + writer.writeLong(array.get(i)); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicLongArray(0); + } + long[] values = new long[8]; + int size = 0; + do { + if (reader.tryReadNull()) { + throw new ForyJsonException("Cannot read null into AtomicLongArray element"); + } + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = reader.readLong(); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicLongArray(Arrays.copyOf(values, size)); + } + } + + public static final class AtomicReferenceArrayCodec extends AbstractJsonCodec { + private final JsonTypeInfo valueTypeInfo; + private final JsonCodec valueCodec; + + public AtomicReferenceArrayCodec(java.lang.reflect.Type valueType, JsonTypeResolver resolver) { + Class valueRawType = CodecUtils.rawType(valueType, Object.class); + valueTypeInfo = resolver.getTypeInfo(valueType, valueRawType); + valueCodec = valueTypeInfo.codec(); + } + + @Override + void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicReferenceArray array = (AtomicReferenceArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + valueCodec.write(writer, array.get(i), resolver); + } + writer.writeArrayEnd(); + } + + @Override + void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { + AtomicReferenceArray array = (AtomicReferenceArray) value; + writer.writeArrayStart(); + for (int i = 0, length = array.length(); i < length; i++) { + writer.writeComma(i); + valueCodec.writeUtf8(writer, array.get(i), resolver); + } + writer.writeArrayEnd(); + } + + @Override + Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expect('['); + if (reader.consume(']')) { + reader.exitDepth(); + return new AtomicReferenceArray<>(0); + } + Object[] values = new Object[8]; + int size = 0; + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = valueCodec.read(reader, valueTypeInfo, resolver); + } while (reader.consume(',')); + reader.expect(']'); + reader.exitDepth(); + return new AtomicReferenceArray<>(Arrays.copyOf(values, size)); + } + } + public static final class OptionalCodec extends AbstractJsonCodec { private final JsonTypeInfo valueTypeInfo; private final JsonCodec valueCodec; 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 a31d05ceb1..8bdb3c3e26 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 @@ -56,8 +56,11 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.regex.Pattern; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.codec.ArrayCodec; @@ -121,6 +124,10 @@ public JsonCodec createCodec( return new ScalarCodecs.AtomicReferenceCodec( CodecUtils.elementType(typeRef.getType()), localResolver); } + if (rawType == AtomicReferenceArray.class) { + return new ScalarCodecs.AtomicReferenceArrayCodec( + CodecUtils.elementType(typeRef.getType()), localResolver); + } if (Calendar.class.isAssignableFrom(rawType)) { return ScalarCodecs.CalendarCodec.INSTANCE; } @@ -228,7 +235,9 @@ private void registerExactCodecs() { exactCodecs.put(StringBuffer.class, ScalarCodecs.StringBufferCodec.INSTANCE); exactCodecs.put(AtomicBoolean.class, ScalarCodecs.AtomicBooleanCodec.INSTANCE); exactCodecs.put(AtomicInteger.class, ScalarCodecs.AtomicIntegerCodec.INSTANCE); + exactCodecs.put(AtomicIntegerArray.class, ScalarCodecs.AtomicIntegerArrayCodec.INSTANCE); exactCodecs.put(AtomicLong.class, ScalarCodecs.AtomicLongCodec.INSTANCE); + exactCodecs.put(AtomicLongArray.class, ScalarCodecs.AtomicLongArrayCodec.INSTANCE); exactCodecs.put(Currency.class, ScalarCodecs.CurrencyCodec.INSTANCE); exactCodecs.put(File.class, ScalarCodecs.FileCodec.INSTANCE); exactCodecs.put(URI.class, ScalarCodecs.UriCodec.INSTANCE); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 43bec41c67..b9c5007e90 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -31,6 +31,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.concurrent.atomic.AtomicLongArray; +import java.util.concurrent.atomic.AtomicReferenceArray; import org.apache.fory.json.data.FastContainers; import org.apache.fory.json.data.MapKeyFields; import org.apache.fory.json.data.Nested; @@ -261,6 +264,76 @@ public void readPrimitiveArrayRoots() { assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", int[].class)); } + @Test + public void writeReadBoxedPrimitiveArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new Integer[] {1, null, -2}), "[1,null,-2]"); + assertEquals( + json.fromJson("[1,null,-2]".getBytes(StandardCharsets.UTF_8), Integer[].class), + new Integer[] {1, null, -2}); + assertEquals( + json.fromJson("[9223372036854775807,null,-9]", Long[].class), + new Long[] {Long.MAX_VALUE, null, -9L}); + assertEquals( + json.fromJson("[true,null,false]".getBytes(StandardCharsets.UTF_8), Boolean[].class), + new Boolean[] {Boolean.TRUE, null, Boolean.FALSE}); + assertEquals( + json.fromJson("[32767,null,-32768]", Short[].class), + new Short[] {Short.MAX_VALUE, null, Short.MIN_VALUE}); + assertEquals( + json.fromJson("[127,null,-128]", Byte[].class), + new Byte[] {Byte.MAX_VALUE, null, Byte.MIN_VALUE}); + assertEquals( + json.fromJson("[\"a\",null,\"你\"]", Character[].class), new Character[] {'a', null, '你'}); + assertEquals( + json.fromJson("[1.5,null,-2.25]", Float[].class), new Float[] {1.5f, null, -2.25f}); + assertEquals( + json.fromJson("[1.5,null,-2.25]".getBytes(StandardCharsets.UTF_8), Double[].class), + new Double[] {1.5d, null, -2.25d}); + assertEquals( + new String(json.toJsonBytes(new Character[] {'x', null, '文'}), StandardCharsets.UTF_8), + "[\"x\",null,\"文\"]"); + + assertThrows(ForyJsonException.class, () -> json.fromJson("[128]", Byte[].class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[\"ab\"]", Character[].class)); + } + + @Test + public void writeReadAtomicArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new AtomicIntegerArray(new int[] {1, -2, 3})), "[1,-2,3]"); + assertAtomicInts(json.fromJson("[1,-2,3]", AtomicIntegerArray.class), 1, -2, 3); + assertEquals( + new String( + json.toJsonBytes(new AtomicLongArray(new long[] {4L, -5L})), StandardCharsets.UTF_8), + "[4,-5]"); + assertAtomicLongs( + json.fromJson("[4,-5]".getBytes(StandardCharsets.UTF_8), AtomicLongArray.class), 4L, -5L); + + AtomicReferenceArray refs = + new AtomicReferenceArray<>(new String[] {"a", null, ZH_TEXT}); + assertEquals(json.toJson(refs), "[\"a\",null,\"你好,Fory\"]"); + assertAtomicRefs( + json.fromJson( + "[\"a\",null,\"你好,Fory\"]".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}), + "a", + null, + ZH_TEXT); + + String fieldsJson = "{\"ints\":[7,8],\"longs\":[9,-10],\"refs\":[\"b\",null]}"; + AtomicArrayFields fields = + json.fromJson(fieldsJson.getBytes(StandardCharsets.UTF_8), AtomicArrayFields.class); + assertAtomicInts(fields.ints, 7, 8); + assertAtomicLongs(fields.longs, 9L, -10L); + assertAtomicRefs(fields.refs, "b", null); + assertEquals(json.toJson(fields), fieldsJson); + + assertThrows( + ForyJsonException.class, () -> json.fromJson("[1,null]", AtomicIntegerArray.class)); + assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", AtomicLongArray.class)); + } + public static final class Shelf { public NoteList notes; } @@ -274,4 +347,31 @@ public static final class Note { public static final class PaletteGroups extends HashMap {} public static final class PaletteCodes extends HashMap {} + + public static final class AtomicArrayFields { + public AtomicIntegerArray ints = new AtomicIntegerArray(new int[] {7, 8}); + public AtomicLongArray longs = new AtomicLongArray(new long[] {9L, -10L}); + public AtomicReferenceArray refs = new AtomicReferenceArray<>(new String[] {"b", null}); + } + + private static void assertAtomicInts(AtomicIntegerArray array, int... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } + + private static void assertAtomicLongs(AtomicLongArray array, long... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } + + private static void assertAtomicRefs(AtomicReferenceArray array, String... expected) { + assertEquals(array.length(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(array.get(i), expected[i]); + } + } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index c0697e3cd6..02fa6e7c10 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -34,6 +34,10 @@ import java.time.ZoneOffset; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.apache.fory.json.codec.JsonCodec; import org.apache.fory.json.data.BoxedScalars; import org.apache.fory.json.data.CoreScalarFields; @@ -50,6 +54,7 @@ import org.apache.fory.json.writer.JsonWriter; import org.apache.fory.json.writer.StringJsonWriter; import org.apache.fory.json.writer.Utf8JsonWriter; +import org.apache.fory.reflect.TypeRef; import org.testng.annotations.Test; public class JsonScalarTest extends ForyJsonTestModels { @@ -182,6 +187,25 @@ public void writeReadCoreScalarFields() { assertEquals(read.uuid, value.uuid); } + @Test + public void writeReadAtomicScalars() { + ForyJson json = ForyJson.builder().build(); + assertEquals(json.toJson(new AtomicBoolean(true)), "true"); + assertEquals(json.fromJson("false", AtomicBoolean.class).get(), false); + assertEquals(json.toJson(new AtomicInteger(12)), "12"); + assertEquals(json.fromJson("13", AtomicInteger.class).get(), 13); + assertEquals(json.toJson(new AtomicLong(14L)), "14"); + assertEquals(json.fromJson("15", AtomicLong.class).get(), 15L); + assertEquals(json.toJson(new AtomicReference<>("value")), "\"value\""); + + AtomicReference value = + json.fromJson("\"typed\"", new TypeRef>() {}); + assertEquals(value.get(), "typed"); + AtomicReference nullValue = + json.fromJson("null", new TypeRef>() {}); + assertEquals(nullValue.get(), null); + } + @Test public void writeUtf8ScalarFormats() { ForyJson json = ForyJson.builder().build(); From d0b9bd8454e1bf68ef348b622b3d5cfe6e133273 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 22:48:24 +0800 Subject: [PATCH 14/40] perf(java): split Fory JSON generated readers --- .../fory/json/codegen/JsonReaderCodegen.java | 406 ++++++++++++++++-- .../fory/json/JsonGeneratedCodecTest.java | 54 +++ 2 files changed, 435 insertions(+), 25 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index da7f5e87a0..d44722105b 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -46,6 +46,8 @@ final class JsonReaderCodegen { private static final int LATIN1_READER = JsonCodegen.LATIN1_READER; private static final int UTF16_READER = JsonCodegen.UTF16_READER; private static final int UTF8_READER = JsonCodegen.UTF8_READER; + private static final int MIN_SPLIT_READ_FIELDS = 12; + private static final int READ_FIELD_GROUP_SIZE = 2; private final JsonCodegen codegen; @@ -125,7 +127,8 @@ String genReaderCode( ctx, "public", "readLatin1", - fastReadExpression(builder, "readLatin1Slow", type, properties, LATIN1_READER, record), + fastReadExpression( + builder, "readLatin1", "readLatin1Slow", type, properties, LATIN1_READER, record), Object.class, Latin1JsonReader.class, "reader", @@ -133,6 +136,16 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readLatin1", + "readLatin1Slow", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); addSlowReadMethods( ctx, builder, @@ -146,7 +159,8 @@ String genReaderCode( ctx, "public", "readUtf16", - fastReadExpression(builder, "readUtf16Slow", type, properties, UTF16_READER, record), + fastReadExpression( + builder, "readUtf16", "readUtf16Slow", type, properties, UTF16_READER, record), Object.class, Utf16JsonReader.class, "reader", @@ -154,6 +168,16 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readUtf16", + "readUtf16Slow", + Utf16JsonReader.class, + type, + properties, + UTF16_READER, + record); addSlowReadMethods( ctx, builder, @@ -167,7 +191,8 @@ String genReaderCode( ctx, "public", "readUtf8", - fastReadExpression(builder, "readUtf8Slow", type, properties, UTF8_READER, record), + fastReadExpression( + builder, "readUtf8", "readUtf8Slow", type, properties, UTF8_READER, record), Object.class, Utf8JsonReader.class, "reader", @@ -175,11 +200,58 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addFastReadGroupMethods( + ctx, + builder, + "readUtf8", + "readUtf8Slow", + Utf8JsonReader.class, + type, + properties, + UTF8_READER, + record); addSlowReadMethods( ctx, builder, "readUtf8Slow", Utf8JsonReader.class, type, properties, UTF8_READER, record); return ctx.genCode(); } + private void addFastReadGroupMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + String slowMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + if (!shouldSplitFastRead(properties)) { + return; + } + Class objectType = record ? Object[].class : type; + for (int start = 0; start < properties.length; ) { + int end = readGroupEnd(properties, start); + addGeneratedMethod( + ctx, + "final", + readGroupMethod(readMethod, start), + fastReadGroupExpression( + builder, slowMethod, type, properties, start, end, readerMode, record), + boolean.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + long[].class, + "fieldHashes"); + start = end; + } + } + private void addSlowReadMethods( CodegenContext ctx, JsonGeneratedCodecBuilder builder, @@ -338,11 +410,16 @@ private Expression readExpression( private Expression fastReadExpression( JsonGeneratedCodecBuilder builder, + String readMethod, String slowMethod, Class type, JsonFieldInfo[] properties, int readerMode, boolean record) { + if (shouldSplitFastRead(properties)) { + return splitFastReadExpression( + builder, readMethod, slowMethod, type, properties, readerMode, record); + } Expression object = objectExpression(builder, record); Expression.ListExpression expressions = new Expression.ListExpression(); expressions.add(object); @@ -371,6 +448,86 @@ private Expression fastReadExpression( return expressions; } + private Expression splitFastReadExpression( + JsonGeneratedCodecBuilder builder, + String readMethod, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + Expression object = objectExpression(builder, record); + Expression.ListExpression expressions = new Expression.ListExpression(); + expressions.add(object); + expressions.add(expectExpr(readerMode, '{')); + expressions.add(new Expression.If(consumeExpr(readerMode, '}'), returnObject(object, record))); + Expression hashes = + new Expression.Variable("localFieldHashes", fieldRef("fieldHashes", long[].class)); + expressions.add(hashes); + for (int start = 0; start < properties.length; ) { + int end = readGroupEnd(properties, start); + Expression groupCall = + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readGroupMethod(readMethod, start), + "", + TypeRef.of(boolean.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + hashes); + expressions.add(new Expression.If(not(groupCall), returnObject(object, record))); + start = end; + } + expressions.add(returnObject(object, record)); + return expressions; + } + + private Expression fastReadGroupExpression( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int start, + int end, + int readerMode, + boolean record) { + Expression object = objectParam(type, record); + Expression hashes = new Reference("fieldHashes", TypeRef.of(long[].class)); + Expression[] skips = new Expression[properties.length]; + Expression.ListExpression expressions = new Expression.ListExpression(); + for (int i = start + 1; i < end; i++) { + skips[i] = new Expression.Variable("skip" + i, Expression.Literal.False); + expressions.add(skips[i]); + } + for (int i = start; i < end; i++) { + Expression read = + fastReadField( + builder, + slowMethod, + type, + properties, + i, + end, + true, + readerMode, + object, + hashes, + skips, + record); + expressions.add(i == start ? read : new Expression.If(not(skips[i]), read)); + } + if (end < properties.length) { + expressions.add(returnTrue()); + } else { + expressions.add(returnFalse()); + } + return expressions; + } + private Expression fastReadField( JsonGeneratedCodecBuilder builder, String slowMethod, @@ -382,8 +539,36 @@ private Expression fastReadField( Expression hashes, Expression[] skips, boolean record) { + return fastReadField( + builder, + slowMethod, + type, + properties, + index, + properties.length, + false, + readerMode, + object, + hashes, + skips, + record); + } + + private Expression fastReadField( + JsonGeneratedCodecBuilder builder, + String slowMethod, + Class type, + JsonFieldInfo[] properties, + int index, + int groupEnd, + boolean groupHelper, + int readerMode, + Expression object, + Expression hashes, + Expression[] skips, + boolean record) { if (isPackedName(properties[index].name())) { - return new Expression.If( + return statementIf( tryReadNextFieldNameColon(readerMode, properties[index]), new Expression.ListExpression( readField( @@ -395,21 +580,43 @@ private Expression fastReadField( object, record, usesTokenValueRead(readerMode)), - fieldEnd(slowMethod, properties.length, index, readerMode, object, record)), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index, + readerMode, + object, + record)), nextDirectFallback( builder, slowMethod, type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, skips, - record)); + record), + groupHelper); } return nextDirectFallback( - builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + builder, + slowMethod, + type, + properties, + index, + groupEnd, + groupHelper, + readerMode, + object, + hashes, + skips, + record); } private Expression nextDirectFallback( @@ -418,14 +625,16 @@ private Expression nextDirectFallback( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, Expression[] skips, boolean record) { int nextIndex = index + 1; - if (nextIndex < properties.length && isPackedName(properties[nextIndex].name())) { - return new Expression.If( + if (nextIndex < groupEnd && isPackedName(properties[nextIndex].name())) { + return statementIf( tryReadNextFieldNameColon(readerMode, properties[nextIndex]), new Expression.ListExpression( readField( @@ -437,22 +646,44 @@ private Expression nextDirectFallback( object, record, usesTokenValueRead(readerMode)), - fieldEnd(slowMethod, properties.length, nextIndex, readerMode, object, record), - new Expression.Assign(skips[nextIndex], Expression.Literal.True)), + new Expression.Assign(skips[nextIndex], Expression.Literal.True), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + nextIndex, + readerMode, + object, + record)), hashFallback( builder, slowMethod, type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, skips, - record)); + record), + groupHelper); } return hashFallback( - builder, slowMethod, type, properties, index, readerMode, object, hashes, skips, record); + builder, + slowMethod, + type, + properties, + index, + groupEnd, + groupHelper, + readerMode, + object, + hashes, + skips, + record); } private Expression hashFallback( @@ -461,6 +692,8 @@ private Expression hashFallback( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, @@ -475,6 +708,8 @@ private Expression hashFallback( type, properties, index, + groupEnd, + groupHelper, readerMode, object, hashes, @@ -489,6 +724,8 @@ private Expression fastReadFieldFromHash( Class type, JsonFieldInfo[] properties, int index, + int groupEnd, + boolean groupHelper, int readerMode, Expression object, Expression hashes, @@ -496,9 +733,9 @@ private Expression fastReadFieldFromHash( Expression fieldHash, boolean record) { Expression fallback; - if (index + 1 < properties.length) { + if (index + 1 < groupEnd) { fallback = - new Expression.If( + statementIf( eq(fieldHash, arrayValue(hashes, index + 1)), new Expression.ListExpression( expectExpr(readerMode, ':'), @@ -511,19 +748,40 @@ private Expression fastReadFieldFromHash( object, record, false), - fieldEnd(slowMethod, properties.length, index + 1, readerMode, object, record), - new Expression.Assign(skips[index + 1], Expression.Literal.True)), - slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record)); + new Expression.Assign(skips[index + 1], Expression.Literal.True), + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index + 1, + readerMode, + object, + record)), + slowConsumedReturn( + slowMethod, index, fieldIndexInvoke(fieldHash), object, record, groupHelper), + groupHelper); } else { - fallback = slowConsumedReturn(slowMethod, index, fieldIndexInvoke(fieldHash), object, record); + fallback = + slowConsumedReturn( + slowMethod, index, fieldIndexInvoke(fieldHash), object, record, groupHelper); } - return new Expression.If( + return statementIf( ne(fieldHash, arrayValue(hashes, index)), fallback, new Expression.ListExpression( expectExpr(readerMode, ':'), readField(builder, type, properties[index], index, readerMode, object, record, false), - fieldEnd(slowMethod, properties.length, index, readerMode, object, record))); + fieldEnd( + slowMethod, + properties.length, + groupEnd, + groupHelper, + index, + readerMode, + object, + record)), + groupHelper); } private static boolean isPackedName(String name) { @@ -544,6 +802,44 @@ private static long packedNameMask(int length) { return length == Long.BYTES ? -1L : (1L << (length << 3)) - 1L; } + private static boolean shouldSplitFastRead(JsonFieldInfo[] properties) { + return properties.length >= MIN_SPLIT_READ_FIELDS; + } + + private static String readGroupMethod(String readMethod, int start) { + return readMethod + "Group" + start; + } + + private static int readGroupEnd(JsonFieldInfo[] properties, int start) { + int weight = 0; + int index = start; + while (index < properties.length) { + int fieldWeight = readFieldWeight(properties[index]); + if (index > start && weight + fieldWeight > READ_FIELD_GROUP_SIZE) { + break; + } + weight += fieldWeight; + index++; + if (fieldWeight >= READ_FIELD_GROUP_SIZE) { + break; + } + } + return index; + } + + private static int readFieldWeight(JsonFieldInfo property) { + switch (property.readKind()) { + case ARRAY: + case COLLECTION: + case MAP: + case OBJECT: + case ENUM: + return READ_FIELD_GROUP_SIZE; + default: + return 1; + } + } + private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { if (record) { return new Expression.Variable( @@ -562,6 +858,22 @@ private Expression returnObject(Expression object, boolean record) { return new Expression.Return(object); } + private static Expression returnTrue() { + return new Expression.Return(Expression.Literal.True); + } + + private static Expression returnFalse() { + return new Expression.Return(Expression.Literal.False); + } + + private static Expression statementIf( + Expression predicate, Expression trueExpr, Expression falseExpr, boolean statementOnly) { + if (statementOnly) { + return new Expression.If(predicate, trueExpr, falseExpr, false, TypeRef.of(void.class)); + } + return new Expression.If(predicate, trueExpr, falseExpr); + } + private Expression slowConsumedReturn( String slowMethod, int index, Expression firstFieldIndex, Expression object, boolean record) { return new Expression.ListExpression( @@ -569,6 +881,36 @@ private Expression slowConsumedReturn( returnObject(object, record)); } + private Expression slowConsumedReturn( + String slowMethod, + int index, + Expression firstFieldIndex, + Expression object, + boolean record, + boolean groupHelper) { + if (!groupHelper) { + return slowConsumedReturn(slowMethod, index, firstFieldIndex, object, record); + } + return new Expression.ListExpression( + slowCall(slowMethod, object, Expression.Literal.ofInt(index), firstFieldIndex), + returnFalse()); + } + + private Expression fieldEnd( + String slowMethod, + int propertyCount, + int groupEnd, + boolean groupHelper, + int index, + int readerMode, + Expression object, + boolean record) { + if (!groupHelper) { + return fieldEnd(slowMethod, propertyCount, index, readerMode, object, record); + } + return fastReadGroupEnd(slowMethod, propertyCount, index, readerMode, object); + } + private Expression fieldEnd( String slowMethod, int propertyCount, @@ -585,6 +927,17 @@ private Expression fieldEnd( slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount))); } + private Expression fastReadGroupEnd( + String slowMethod, int propertyCount, int index, int readerMode, Expression object) { + if (index + 1 < propertyCount) { + return new Expression.If(not(consumeCommaOrEndObjectExpr(readerMode)), returnFalse()); + } + return new Expression.ListExpression( + new Expression.If( + consumeCommaOrEndObjectExpr(readerMode), + slowCall(slowMethod, object, Expression.Literal.ofInt(propertyCount)))); + } + private Expression slowReadExpression( JsonGeneratedCodecBuilder builder, Class type, @@ -1354,19 +1707,22 @@ private static Expression readEnumValue( readEnumMethod(readerMode, tokenValueRead, hashFallback), "", TypeRef.of(Object.class), - true, + false, false, readerRef(readerMode)), TypeRef.of(enumType)); } private static Expression readResolvedValue(JsonFieldInfo property, int id, int readerMode) { + // Generated readers either branch on JSON null before calling a non-null codec path, or assign + // nullable reference results directly. Requesting expression null-state here only emits dead + // boolean locals around codec calls and bloats hot generated reader methods. return new Expression.Cast( new Expression.Invoke( fieldRef("r" + id, JsonCodec.class), readObjectMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), @@ -1379,7 +1735,7 @@ private static Expression readCollectionValue(JsonFieldInfo property, int id, in fieldRef("r" + id, CollectionCodec.class), readObjectNonNullMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), @@ -1395,7 +1751,7 @@ private static Expression readObjectValue( codec, readObjectNonNullMethod(readerMode), TypeRef.of(Object.class), - true, + false, readerRef(readerMode), fieldRef("t" + id, JsonTypeInfo.class), typeResolverRef()), diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index cf6a10d7d1..5c58e516ed 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -119,4 +119,58 @@ public void readGeneratedCollectionFields() { json.fromJson(input.getBytes(StandardCharsets.UTF_8), GeneratedCollectionFields.class)); assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); } + + @Test + public void readSplitGeneratedFields() { + ForyJson json = ForyJson.builder().build(); + String ordered = + "{\"f0\":0,\"f1\":\"one\",\"f2\":2,\"f3\":\"three\",\"f4\":4,\"f5\":\"five\"," + + "\"f6\":6,\"f7\":\"seven\",\"f8\":8,\"f9\":\"nine\",\"f10\":10," + + "\"f11\":\"eleven\",\"f12\":12,\"f13\":\"thirteen\"}"; + assertWideFields(json.fromJson(ordered, WideFields.class)); + assertWideFields(json.fromJson(ordered.getBytes(StandardCharsets.UTF_8), WideFields.class)); + + String boundaryFallback = + "{\"f0\":0,\"f2\":2,\"f1\":\"one\",\"f3\":\"three\",\"f4\":4,\"f5\":\"five\"," + + "\"f6\":6,\"f7\":\"seven\",\"f8\":8,\"f9\":\"nine\",\"f10\":10," + + "\"f11\":\"eleven\",\"f12\":12,\"f13\":\"thirteen\"}"; + assertWideFields(json.fromJson(boundaryFallback, WideFields.class)); + assertWideFields( + json.fromJson(boundaryFallback.getBytes(StandardCharsets.UTF_8), WideFields.class)); + assertGeneratedWhenSupported(json, WideFields.class); + } + + private static void assertWideFields(WideFields value) { + assertEquals(value.f0, 0); + assertEquals(value.f1, "one"); + assertEquals(value.f2, 2); + assertEquals(value.f3, "three"); + assertEquals(value.f4, 4); + assertEquals(value.f5, "five"); + assertEquals(value.f6, 6); + assertEquals(value.f7, "seven"); + assertEquals(value.f8, 8); + assertEquals(value.f9, "nine"); + assertEquals(value.f10, 10); + assertEquals(value.f11, "eleven"); + assertEquals(value.f12, 12); + assertEquals(value.f13, "thirteen"); + } + + public static class WideFields { + public int f0; + public String f1; + public int f2; + public String f3; + public int f4; + public String f5; + public int f6; + public String f7; + public int f8; + public String f9; + public int f10; + public String f11; + public int f12; + public String f13; + } } From 69a30d85e150871b4d4d0fc3b4842e32f55b905c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 1 Jul 2026 23:44:09 +0800 Subject: [PATCH 15/40] perf(java): pack Fory JSON generated field prefixes --- .../fory/json/codegen/JsonWriterCodegen.java | 68 ++++++++++++++-- .../fory/json/writer/Utf8JsonWriter.java | 77 +++++++++++++++++++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index a5cca49ba9..af6333d7c1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -318,9 +318,17 @@ private static Expression writeObjectStartPrimitive( case BYTE: case SHORT: case INT: + if (utf8 && canPackPrefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectIntField", packedPrefixArgs(property, false, value)); + } return new Expression.Invoke( writer, "writeObjectIntField", prefixRef(utf8, false, 0), value); case LONG: + if (utf8 && canPackPrefix(property, false)) { + return new Expression.Invoke( + writer, "writeObjectLongField", packedPrefixArgs(property, false, value)); + } return new Expression.Invoke( writer, "writeObjectLongField", prefixRef(utf8, false, 0), value); default: @@ -356,13 +364,13 @@ private Expression writeProp( new Expression.If( eq(value, nullValue), new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), new Expression.Invoke(writer, "writeNull")), writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver))); } return new Expression.ListExpression( value, - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), new Expression.If( eq(value, nullValue), new Expression.Invoke(writer, "writeNull"), @@ -372,7 +380,7 @@ private Expression writeProp( isPrefixValue(property.writeKind()) ? writeValue(property, id, value, utf8, commaKnown, index, writer, typeResolver) : new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), writeValue(property, id, value, utf8, true, index, writer, typeResolver)); return new Expression.ListExpression(value, new Expression.If(ne(value, nullValue), write)); } @@ -392,17 +400,18 @@ private Expression writePrimitive( case BYTE: case SHORT: case INT: - return writeNumberField(id, value, false, utf8, commaKnown, index, writer); + return writeNumberField(property, id, value, false, utf8, commaKnown, index, writer); case LONG: - return writeNumberField(id, value, true, utf8, commaKnown, index, writer); + return writeNumberField(property, id, value, true, utf8, commaKnown, index, writer); default: return new Expression.ListExpression( - writeFieldName(id, utf8, commaKnown, index, writer), + writeFieldName(property, id, utf8, commaKnown, index, writer), writePrimitiveScalar(property.writeKind(), value, writer)); } } private static Expression writeNumberField( + JsonFieldInfo property, int id, Expression value, boolean longValue, @@ -412,6 +421,9 @@ private static Expression writeNumberField( Expression writer) { String writerMethod = longValue ? "writeLongField" : "writeIntField"; if (commaKnown) { + if (utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke(writer, writerMethod, packedPrefixArgs(property, true, value)); + } return new Expression.Invoke(writer, writerMethod, prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = @@ -428,6 +440,7 @@ private static Expression writeNumberField( } private static Expression writeStringField( + JsonFieldInfo property, int id, Expression value, boolean utf8, @@ -435,6 +448,10 @@ private static Expression writeStringField( Expression index, Expression writer) { if (commaKnown) { + if (utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke( + writer, "writeStringField", packedPrefixArgs(property, true, value)); + } return new Expression.Invoke(writer, "writeStringField", prefixRef(utf8, true, id), value); } Expression.ListExpression expressions = @@ -451,7 +468,15 @@ private static Expression writeStringField( } private static Expression writeFieldName( - int id, boolean utf8, boolean commaKnown, Expression index, Expression writer) { + JsonFieldInfo property, + int id, + boolean utf8, + boolean commaKnown, + Expression index, + Expression writer) { + if (commaKnown && utf8 && canPackPrefix(property, true)) { + return new Expression.Invoke(writer, "writeRawValue", packedPrefixArgs(property, true)); + } Expression prefix = commaKnown ? prefixRef(utf8, true, id) @@ -495,6 +520,7 @@ private Expression writeValue( case SHORT: case INT: return writeNumberField( + property, id, new Expression.Invoke(value, "intValue", TypeRef.of(int.class)).inline(), false, @@ -504,6 +530,7 @@ private Expression writeValue( writer); case LONG: return writeNumberField( + property, id, new Expression.Invoke(value, "longValue", TypeRef.of(long.class)).inline(), true, @@ -512,7 +539,7 @@ private Expression writeValue( index, writer); case STRING: - return writeStringField(id, value, utf8, commaKnown, index, writer); + return writeStringField(property, id, value, utf8, commaKnown, index, writer); case ENUM: return writeRawFieldValue( commaKnown, index, enumFieldValue(id, value, utf8, commaKnown, index), writer); @@ -546,6 +573,31 @@ private static Expression writeRawFieldValue( return expressions; } + private static Expression[] packedPrefixArgs( + JsonFieldInfo property, boolean comma, Expression... extraArgs) { + byte[] prefix = comma ? property.utf8CommaNamePrefix() : property.utf8NamePrefix(); + Expression[] args = new Expression[3 + extraArgs.length]; + args[0] = Expression.Literal.ofLong(packedPrefixWord(prefix, 0)); + args[1] = Expression.Literal.ofLong(packedPrefixWord(prefix, Long.BYTES)); + args[2] = Expression.Literal.ofInt(prefix.length); + System.arraycopy(extraArgs, 0, args, 3, extraArgs.length); + return args; + } + + private static boolean canPackPrefix(JsonFieldInfo property, boolean comma) { + int length = comma ? property.utf8CommaNamePrefix().length : property.utf8NamePrefix().length; + return length <= Long.BYTES * 2; + } + + private static long packedPrefixWord(byte[] prefix, int offset) { + long word = 0; + int end = Math.min(prefix.length, offset + Long.BYTES); + for (int i = offset; i < end; i++) { + word |= (prefix[i] & 0xffL) << ((i - offset) << 3); + } + return word; + } + private static Expression booleanFieldValue( int id, Expression value, boolean utf8, boolean commaKnown, Expression index) { return new Expression.Invoke( diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 420a5a78a7..7d431f61a8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -328,6 +328,12 @@ public void writeIntField(byte[] prefix, int value) { writeIntNoEnsure(value); } + public void writeIntField(long prefix0, long prefix1, int prefixLength, int value) { + ensurePackedPrefix(prefixLength, 11); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeIntNoEnsure(value); + } + public void writeObjectIntField(byte[] namePrefix, int value) { ensure(namePrefix.length + 12); buffer[position++] = (byte) '{'; @@ -335,6 +341,13 @@ public void writeObjectIntField(byte[] namePrefix, int value) { writeIntNoEnsure(value); } + public void writeObjectIntField(long prefix0, long prefix1, int prefixLength, int value) { + ensurePackedPrefix(prefixLength, 12); + buffer[position++] = (byte) '{'; + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeIntNoEnsure(value); + } + public void writeLongField(byte[] namePrefix, byte[] commaNamePrefix, int index, long value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeLongField(prefix, value); @@ -346,6 +359,12 @@ public void writeLongField(byte[] prefix, long value) { writeLongNoEnsure(value); } + public void writeLongField(long prefix0, long prefix1, int prefixLength, long value) { + ensurePackedPrefix(prefixLength, 20); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeLongNoEnsure(value); + } + public void writeObjectLongField(byte[] namePrefix, long value) { ensure(namePrefix.length + 21); buffer[position++] = (byte) '{'; @@ -353,6 +372,13 @@ public void writeObjectLongField(byte[] namePrefix, long value) { writeLongNoEnsure(value); } + public void writeObjectLongField(long prefix0, long prefix1, int prefixLength, long value) { + ensurePackedPrefix(prefixLength, 21); + buffer[position++] = (byte) '{'; + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeLongNoEnsure(value); + } + public void writeStringField(byte[] namePrefix, byte[] commaNamePrefix, int index, String value) { byte[] prefix = index == 0 ? namePrefix : commaNamePrefix; writeStringField(prefix, value); @@ -382,12 +408,42 @@ public void writeStringField(byte[] prefix, String value) { writeStringFieldChars(prefix, value); } + public void writeStringField(long prefix0, long prefix1, int prefixLength, String value) { + if (STRING_BYTES_BACKED) { + byte[] bytes = StringSerializer.getStringBytes(value); + byte stringCoder = StringSerializer.getStringCoder(value); + int start = position; + if (StringSerializer.isLatin1Coder(stringCoder)) { + ensurePackedPrefix(prefixLength, bytes.length + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeLatin1StringNoEnsure(bytes)) { + return; + } + position = start; + } else if (StringSerializer.isUtf16Coder(stringCoder)) { + ensurePackedPrefix(prefixLength, (bytes.length >> 1) * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + if (writeUtf16StringNoEnsure(bytes)) { + return; + } + position = start; + } + } + writeStringFieldChars(prefix0, prefix1, prefixLength, value); + } + private void writeStringFieldChars(byte[] prefix, String value) { ensure(prefix.length + value.length() * 3 + 2); writeRawNoEnsure(prefix); writeStringNoEnsure(value); } + private void writeStringFieldChars(long prefix0, long prefix1, int prefixLength, String value) { + ensurePackedPrefix(prefixLength, value.length() * 3 + 2); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + writeStringNoEnsure(value); + } + public void writeStringCollection(Collection values) { writeArrayStart(); if (values.getClass() == ArrayList.class) { @@ -457,6 +513,11 @@ public void writeRawValue(byte[] value) { writeRaw(value); } + public void writeRawValue(long prefix0, long prefix1, int prefixLength) { + ensure(packedPrefixSize(prefixLength)); + writePackedRawNoEnsure(prefix0, prefix1, prefixLength); + } + @Override public void writeObjectStart() { writeByteRaw((byte) '{'); @@ -747,6 +808,22 @@ private void writeRawNoEnsure(byte[] bytes) { position += bytes.length; } + private void writePackedRawNoEnsure(long prefix0, long prefix1, int prefixLength) { + LittleEndian.putInt64(buffer, position, prefix0); + if (prefixLength > Long.BYTES) { + LittleEndian.putInt64(buffer, position + Long.BYTES, prefix1); + } + position += prefixLength; + } + + private void ensurePackedPrefix(int prefixLength, int additionalAfterPrefix) { + ensure(Math.max(packedPrefixSize(prefixLength), prefixLength + additionalAfterPrefix)); + } + + private static int packedPrefixSize(int prefixLength) { + return prefixLength <= Long.BYTES ? Long.BYTES : Long.BYTES * 2; + } + private void writeByteRaw(byte value) { ensure(1); buffer[position++] = value; From 591c2840579425315fa55f46c45af7faa75dec57 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 00:01:10 +0800 Subject: [PATCH 16/40] perf(java): specialize Fory JSON ArrayList object writes --- .../fory/json/codec/CollectionCodec.java | 81 ++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index dfd70c8346..66c4d78ac9 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -447,13 +447,26 @@ private ObjectCollectionCodec( @Override void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeArrayStart(); - int index = 0; - for (Object element : (Collection) value) { - writer.writeComma(index++); - if (element == null) { - writer.writeNull(); - } else { - elementCodec.writeNonNull(writer, element, resolver); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeNonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -462,13 +475,26 @@ void writeNonNull(JsonWriter writer, Object value, JsonTypeResolver resolver) { @Override void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeArrayStart(); - int index = 0; - for (Object element : (Collection) value) { - writer.writeComma(index++); - if (element == null) { - writer.writeNull(); - } else { - elementCodec.writeStringNonNull(writer, element, resolver); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeStringNonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); @@ -477,13 +503,26 @@ void writeStringNonNull(StringJsonWriter writer, Object value, JsonTypeResolver @Override void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver resolver) { writer.writeArrayStart(); - int index = 0; - for (Object element : (Collection) value) { - writer.writeComma(index++); - if (element == null) { - writer.writeNull(); - } else { - elementCodec.writeUtf8NonNull(writer, element, resolver); + if (value.getClass() == ArrayList.class) { + ArrayList list = (ArrayList) value; + for (int index = 0, size = list.size(); index < size; index++) { + Object element = list.get(index); + writer.writeComma(index); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } + } + } else { + int index = 0; + for (Object element : (Collection) value) { + writer.writeComma(index++); + if (element == null) { + writer.writeNull(); + } else { + elementCodec.writeUtf8NonNull(writer, element, resolver); + } } } writer.writeArrayEnd(); From 0f285ad176a21a35dedddeebea4a3892cd0245b3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 00:14:17 +0800 Subject: [PATCH 17/40] perf(java): copy Fory JSON numeric bytes directly --- .../apache/fory/json/writer/Utf8JsonWriter.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 7d431f61a8..9ae87903ea 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -141,7 +141,7 @@ public void writeFloat(float value) { if (!Float.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite float " + value); } - writeAscii(Float.toString(value)); + writeAsciiNumber(Float.toString(value)); } @Override @@ -149,12 +149,12 @@ public void writeDouble(double value) { if (!Double.isFinite(value)) { throw new ForyJsonException("JSON does not support non-finite double " + value); } - writeAscii(Double.toString(value)); + writeAsciiNumber(Double.toString(value)); } @Override public void writeNumber(String value) { - writeAscii(value); + writeAsciiNumber(value); } @Override @@ -798,6 +798,15 @@ private void writeAsciiNoEnsure(String value) { } } + private void writeAsciiNumber(String value) { + if (STRING_BYTES_BACKED + && StringSerializer.isLatin1Coder(StringSerializer.getStringCoder(value))) { + writeRaw(StringSerializer.getStringBytes(value)); + return; + } + writeAscii(value); + } + private void writeRaw(byte[] bytes) { ensure(bytes.length); writeRawNoEnsure(bytes); From f454260f3eb7ca4a0ebb1c6c4216018dcf97901f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 00:54:57 +0800 Subject: [PATCH 18/40] perf(java): copy Fory JSON short Latin1 tails --- .../fory/json/writer/Utf8JsonWriter.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 9ae87903ea..54d766d361 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -41,16 +41,21 @@ public final class Utf8JsonWriter extends JsonWriter { private static final long EIGHT_DIGITS = 100_000_000L; private static final long HIGH_BITS = 0x8080808080808080L; private static final int INT_HIGH_BITS = 0x80808080; + private static final int SHORT_HIGH_BITS = 0x8080; private static final long ASCII_CONTROL_OFFSET = 0x6060606060606060L; private static final int INT_ASCII_CONTROL_OFFSET = 0x60606060; + private static final int SHORT_ASCII_CONTROL_OFFSET = 0x6060; private static final long ONE_BYTES = 0x0101010101010101L; private static final int INT_ONE_BYTES = 0x01010101; + private static final int SHORT_ONE_BYTES = 0x0101; private static final byte[] HEX_DIGITS = "0123456789abcdef".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final long QUOTE_BYTES_COMPLEMENT = ~0x2222222222222222L; private static final int INT_QUOTE_BYTES_COMPLEMENT = ~0x22222222; + private static final int SHORT_QUOTE_BYTES_COMPLEMENT = ~0x2222; private static final long BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C5C5C5C5CL; private static final int INT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C5C5C; + private static final int SHORT_BACKSLASH_BYTES_COMPLEMENT = ~0x5C5C; private static final long UTF16_ASCII_MASK = 0xFF80FF80FF80FF80L; private static final int[] DIGIT_TRIPLES = new int[1000]; private static final int[] DIGIT_QUADS = new int[10000]; @@ -586,6 +591,15 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { i += 4; } } + if (i + 2 <= length) { + int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (isJsonAsciiShort(word)) { + bytes[pos] = (byte) word; + bytes[pos + 1] = (byte) (word >>> 8); + pos += 2; + i += 2; + } + } for (; i < length; i++) { byte ch = value[i]; if (isJsonAsciiByte(ch)) { @@ -879,6 +893,14 @@ private static boolean isJsonAsciiInt(int word) { == INT_HIGH_BITS; } + private static boolean isJsonAsciiShort(int word) { + return (((word + SHORT_ASCII_CONTROL_OFFSET) & ~word) & SHORT_HIGH_BITS) == SHORT_HIGH_BITS + && (((word ^ SHORT_QUOTE_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) + == SHORT_HIGH_BITS + && (((word ^ SHORT_BACKSLASH_BYTES_COMPLEMENT) + SHORT_ONE_BYTES) & SHORT_HIGH_BITS) + == SHORT_HIGH_BITS; + } + private static int packUtf16Ascii(long word) { return (int) ((word & 0xFFL) From 8f4f461aa91aee9dff780a8c0d519743521cba64 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 01:54:37 +0800 Subject: [PATCH 19/40] perf(java): retain medium Fory JSON writer buffers --- .../java/org/apache/fory/json/writer/StringJsonWriter.java | 3 ++- .../main/java/org/apache/fory/json/writer/Utf8JsonWriter.java | 3 ++- .../src/test/java/org/apache/fory/json/JsonStringTest.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java index ebe10dcffc..c273195f9d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/StringJsonWriter.java @@ -30,7 +30,8 @@ public final class StringJsonWriter extends JsonWriter { private static final byte LATIN1 = 0; private static final byte UTF16 = 1; - private static final int RETAINED_CAPACITY = 8192; + // Pooled writers should retain medium buffers to avoid reallocating common JSON outputs. + private static final int RETAINED_CAPACITY = 64 * 1024; private static final byte[] MIN_INT_BYTES = "-2147483648".getBytes(StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = "-9223372036854775808".getBytes(StandardCharsets.ISO_8859_1); diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 54d766d361..d62912637c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -33,7 +33,8 @@ import org.apache.fory.serializer.StringSerializer; public final class Utf8JsonWriter extends JsonWriter { - private static final int RETAINED_CAPACITY = 8192; + // Pooled writers should retain medium buffers to avoid reallocating common JSON outputs. + private static final int RETAINED_CAPACITY = 64 * 1024; private static final byte[] MIN_INT_BYTES = "-2147483648".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); private static final byte[] MIN_LONG_BYTES = diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index 823873b160..c6beef60dc 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -119,7 +119,7 @@ public void stringWriterShrinksOnReset() throws Exception { assertTrue(writerBufferLength(writer) > 8192); writer.toJson(); writer.reset(); - assertEquals(writerBufferLength(writer), 8192); + assertEquals(writerBufferLength(writer), 65536); writer.writeString("café"); assertEquals(writer.toJson(), "\"café\""); } From 4935994b130fcdbdee352e081debee117315b0f6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 02:00:21 +0800 Subject: [PATCH 20/40] perf(java): emit exact Fory JSON array writes --- .../fory/json/codegen/JsonWriterCodegen.java | 16 ++++++++++++++ .../fory/json/writer/Utf8JsonWriter.java | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java index af6333d7c1..a0ce024439 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonWriterCodegen.java @@ -548,6 +548,8 @@ private Expression writeValue( case CHAR: return writeScalar(kind, value, writer); case ARRAY: + Expression array = writeExactUtf8Array(property.writeRawType(), value, utf8, writer); + return array == null ? writeCodec(id, value, utf8, writer, typeResolver) : array; case MAP: return writeCodec(id, value, utf8, writer, typeResolver); case COLLECTION: @@ -653,6 +655,20 @@ private static Expression writeExactUtf8Scalar( return new Expression.Invoke(writer, writerMethod, value); } + private static Expression writeExactUtf8Array( + Class rawType, Expression value, boolean utf8, Expression writer) { + if (!utf8) { + return null; + } + if (rawType == String[].class) { + return new Expression.Invoke(writer, "writeStringArray", value); + } + if (rawType == long[].class) { + return new Expression.Invoke(writer, "writeLongArray", value); + } + return null; + } + private static Expression writeStringCollection( Expression value, boolean utf8, Expression writer) { if (utf8) { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index d62912637c..84d6509d8e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -466,6 +466,27 @@ public void writeStringCollection(Collection values) { writeArrayEnd(); } + public void writeStringArray(String[] values) { + writeArrayStart(); + for (int i = 0; i < values.length; i++) { + writeStringElement(i, values[i]); + } + writeArrayEnd(); + } + + public void writeLongArray(long[] values) { + ensure(2); + buffer[position++] = '['; + for (int i = 0; i < values.length; i++) { + ensure(22); + if (i != 0) { + buffer[position++] = ','; + } + writeLongNoEnsure(values[i]); + } + buffer[position++] = ']'; + } + public void writeStringElement(int index, String value) { int comma = index == 0 ? 0 : 1; if (value == null) { From c3b85e3f213b6af29730504d6cfe20b4ec38bda6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 04:29:29 +0800 Subject: [PATCH 21/40] perf(json): add short latin1 string writer path --- .../fory/json/writer/Utf8JsonWriter.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 84d6509d8e..47710c4999 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -580,6 +580,9 @@ private boolean writeLatin1String(byte[] value) { private boolean writeLatin1StringNoEnsure(byte[] value) { int length = value.length; + if (length <= 32) { + return writeShortLatin1StringNoEnsure(value, length); + } byte[] bytes = buffer; int start = position; int pos = start; @@ -636,6 +639,68 @@ private boolean writeLatin1StringNoEnsure(byte[] value) { return true; } + private boolean writeShortLatin1StringNoEnsure(byte[] value, int length) { + byte[] bytes = buffer; + int start = position; + int pos = start; + bytes[pos++] = (byte) '"'; + int i = 0; + if (length > 15) { + long word0 = LittleEndian.getInt64(value, 0); + long word1 = LittleEndian.getInt64(value, 8); + if (!isJsonAsciiWord(word0) || !isJsonAsciiWord(word1)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word0); + LittleEndian.putInt64(bytes, pos + 8, word1); + pos += 16; + i = 16; + } + if (i + 8 <= length) { + long word = LittleEndian.getInt64(value, i); + if (!isJsonAsciiWord(word)) { + position = start; + return false; + } + LittleEndian.putInt64(bytes, pos, word); + pos += 8; + i += 8; + } + if (i + 4 <= length) { + int word = LittleEndian.getInt32(value, i); + if (!isJsonAsciiInt(word)) { + position = start; + return false; + } + LittleEndian.putInt32(bytes, pos, word); + pos += 4; + i += 4; + } + if (i + 2 <= length) { + int word = (value[i] & 0xFF) | ((value[i + 1] & 0xFF) << 8); + if (!isJsonAsciiShort(word)) { + position = start; + return false; + } + bytes[pos] = (byte) word; + bytes[pos + 1] = (byte) (word >>> 8); + pos += 2; + i += 2; + } + if (i < length) { + byte ch = value[i]; + if (!isJsonAsciiByte(ch)) { + position = start; + return false; + } + bytes[pos++] = ch; + } + bytes[pos++] = (byte) '"'; + position = pos; + return true; + } + private boolean writeUtf16String(byte[] value) { int length = value.length; ensure((length >> 1) * 3 + 2); From fea28c84968c38cff45c346839993f78560ce04c Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 04:44:00 +0800 Subject: [PATCH 22/40] perf(json): split latin1 string writer dispatch --- .../org/apache/fory/json/writer/Utf8JsonWriter.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index 47710c4999..eb67d1e70a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -575,14 +575,22 @@ public void writeComma(int index) { private boolean writeLatin1String(byte[] value) { int length = value.length; ensure(length + 2); - return writeLatin1StringNoEnsure(value); + return writeLatin1StringNoEnsure(value, length); } private boolean writeLatin1StringNoEnsure(byte[] value) { int length = value.length; + return writeLatin1StringNoEnsure(value, length); + } + + private boolean writeLatin1StringNoEnsure(byte[] value, int length) { if (length <= 32) { return writeShortLatin1StringNoEnsure(value, length); } + return writeLongLatin1StringNoEnsure(value, length); + } + + private boolean writeLongLatin1StringNoEnsure(byte[] value, int length) { byte[] bytes = buffer; int start = position; int pos = start; From a3758bf18233e6d1573a4ee115f98ccd31916a9f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 05:39:39 +0800 Subject: [PATCH 23/40] perf(json): match long ascii field tokens --- .../fory/json/codegen/JsonReaderCodegen.java | 27 ++++++- .../apache/fory/json/meta/JsonAsciiToken.java | 28 ++++++++ .../fory/json/reader/Latin1JsonReader.java | 31 ++++++++ .../fory/json/reader/Utf8JsonReader.java | 31 ++++++++ .../fory/json/JsonGeneratedCodecTest.java | 71 +++++++++++++++++++ 5 files changed, 186 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index d44722105b..a99ae52ea8 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -567,7 +567,7 @@ private Expression fastReadField( Expression hashes, Expression[] skips, boolean record) { - if (isPackedName(properties[index].name())) { + if (isDirectName(readerMode, properties[index].name())) { return statementIf( tryReadNextFieldNameColon(readerMode, properties[index]), new Expression.ListExpression( @@ -633,7 +633,7 @@ private Expression nextDirectFallback( Expression[] skips, boolean record) { int nextIndex = index + 1; - if (nextIndex < groupEnd && isPackedName(properties[nextIndex].name())) { + if (nextIndex < groupEnd && isDirectName(readerMode, properties[nextIndex].name())) { return statementIf( tryReadNextFieldNameColon(readerMode, properties[nextIndex]), new Expression.ListExpression( @@ -789,6 +789,18 @@ private static boolean isPackedName(String name) { if (length == 0 || length > Long.BYTES) { return false; } + return isAsciiName(name); + } + + private static boolean isDirectName(int readerMode, String name) { + if (readerMode == LATIN1_READER || readerMode == UTF8_READER) { + return JsonAsciiToken.isLongPackable(fieldNameToken(name)); + } + return isPackedName(name); + } + + private static boolean isAsciiName(String name) { + int length = name.length(); for (int i = 0; i < length; i++) { char ch = name.charAt(i); if (ch == 0 || ch > 0xFF) { @@ -1234,6 +1246,17 @@ private static Expression tryReadNextFieldNameColon(int readerMode, JsonFieldInf Expression.Literal.ofInt(tokenLength)) .inline(); } + if (suffixLength > 3) { + return new Expression.Invoke( + readerRef(readerMode), + "tryReadNextFieldNameToken8", + TypeRef.of(boolean.class), + Expression.Literal.ofLong(JsonAsciiToken.prefix(token)), + Expression.Literal.ofLong(JsonAsciiToken.suffixLong(token)), + Expression.Literal.ofLong(JsonAsciiToken.suffixMask(tokenLength)), + Expression.Literal.ofInt(tokenLength)) + .inline(); + } return new Expression.Invoke( readerRef(readerMode), "tryReadNextFieldNameToken" + suffixLength, diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java index 44b504e865..16285ff41e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonAsciiToken.java @@ -21,6 +21,7 @@ public final class JsonAsciiToken { private static final int MAX_SUFFIX_LENGTH = 3; + private static final int MAX_LONG_SUFFIX_LENGTH = Long.BYTES; private JsonAsciiToken() {} @@ -29,6 +30,19 @@ public static boolean isPackable(String token) { if (length == 0 || suffixLength(length) > MAX_SUFFIX_LENGTH) { return false; } + return isLatin1Token(token); + } + + public static boolean isLongPackable(String token) { + int length = token.length(); + if (length == 0 || suffixLength(length) > MAX_LONG_SUFFIX_LENGTH) { + return false; + } + return isLatin1Token(token); + } + + private static boolean isLatin1Token(String token) { + int length = token.length(); for (int i = 0; i < length; i++) { char ch = token.charAt(i); if (ch == 0 || ch > 0xFF) { @@ -61,6 +75,20 @@ public static int suffix(String token) { return value; } + public static long suffixLong(String token) { + int suffixLength = suffixLength(token.length()); + long value = 0; + for (int i = 0; i < suffixLength; i++) { + value |= (long) (token.charAt(i + Long.BYTES) & 0xFF) << (i << 3); + } + return value; + } + + public static long suffixMask(int tokenLength) { + int suffixLength = suffixLength(tokenLength); + return suffixLength == Long.BYTES ? -1L : (1L << (suffixLength << 3)) - 1; + } + public static int suffixLength(int tokenLength) { return Math.max(0, tokenLength - Long.BYTES); } 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 8a9ae15696..729d7159f1 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 @@ -854,6 +854,37 @@ private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, i return false; } + public boolean tryReadNextFieldNameToken8( + long prefix, long suffix, long suffixMask, int tokenLength) { + return tryReadNextRawToken8(prefix, suffix, suffixMask, tokenLength); + } + + private boolean tryReadNextRawToken8(long prefix, long suffix, long suffixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && LittleEndian.getInt64(bytes, mark) == prefix + && readTokenSuffix(bytes, suffixOffset, tokenLength, suffixMask) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private static long readTokenSuffix( + byte[] bytes, int suffixOffset, int tokenLength, long suffixMask) { + if (suffixOffset + Long.BYTES <= bytes.length) { + return LittleEndian.getInt64(bytes, suffixOffset) & suffixMask; + } + int suffixLength = tokenLength - Long.BYTES; + long suffix = 0; + for (int i = 0; i < suffixLength; i++) { + suffix |= (long) (bytes[suffixOffset + i] & 0xFF) << (i << 3); + } + return suffix; + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] bytes = input; 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 57372d529c..dc28215a67 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 @@ -1130,6 +1130,37 @@ private boolean tryReadNextRawToken3(long prefix, long prefixMask, int suffix, i return false; } + public boolean tryReadNextFieldNameToken8( + long prefix, long suffix, long suffixMask, int tokenLength) { + return tryReadNextRawToken8(prefix, suffix, suffixMask, tokenLength); + } + + private boolean tryReadNextRawToken8(long prefix, long suffix, long suffixMask, int tokenLength) { + byte[] bytes = input; + int mark = position; + int suffixOffset = mark + Long.BYTES; + if (mark + tokenLength <= bytes.length + && LittleEndian.getInt64(bytes, mark) == prefix + && readTokenSuffix(bytes, suffixOffset, tokenLength, suffixMask) == suffix) { + position = mark + tokenLength; + return true; + } + return false; + } + + private static long readTokenSuffix( + byte[] bytes, int suffixOffset, int tokenLength, long suffixMask) { + if (suffixOffset + Long.BYTES <= bytes.length) { + return LittleEndian.getInt64(bytes, suffixOffset) & suffixMask; + } + int suffixLength = tokenLength - Long.BYTES; + long suffix = 0; + for (int i = 0; i < suffixLength; i++) { + suffix |= (long) (bytes[suffixOffset + i] & 0xFF) << (i << 3); + } + return suffix; + } + private boolean tryReadFieldNameColonAt( int mark, long expectedHash, long expectedMask, int expectedLength) { byte[] bytes = input; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 5c58e516ed..45c15e9c97 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -20,6 +20,8 @@ package org.apache.fory.json; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -28,6 +30,10 @@ import org.apache.fory.json.data.RecursiveParent; import org.apache.fory.json.data.TokenGroup; import org.apache.fory.json.data.TokenValues; +import org.apache.fory.json.meta.JsonAsciiToken; +import org.apache.fory.json.meta.JsonFieldNameHash; +import org.apache.fory.json.reader.Latin1JsonReader; +import org.apache.fory.json.reader.Utf8JsonReader; import org.testng.annotations.Test; public class JsonGeneratedCodecTest extends ForyJsonTestModels { @@ -120,6 +126,57 @@ public void readGeneratedCollectionFields() { assertGeneratedWhenSupported(json, GeneratedCollectionFields.class); } + @Test + public void readLongAsciiFieldToken() { + String token = "\"favoriteFruit\":"; + long prefix = JsonAsciiToken.prefix(token); + long suffix = JsonAsciiToken.suffixLong(token); + long suffixMask = JsonAsciiToken.suffixMask(token.length()); + Utf8JsonReader utf8 = + new Utf8JsonReader((token + "\"apple\"").getBytes(StandardCharsets.UTF_8)); + assertTrue(utf8.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(utf8.readNullableStringToken(), "apple"); + + Latin1JsonReader latin1 = new Latin1JsonReader(token + "\"pear\""); + assertTrue(latin1.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(latin1.readNullableStringToken(), "pear"); + + String tailToken = "\"registered\":"; + long tailPrefix = JsonAsciiToken.prefix(tailToken); + long tailSuffix = JsonAsciiToken.suffixLong(tailToken); + long tailSuffixMask = JsonAsciiToken.suffixMask(tailToken.length()); + Utf8JsonReader tailUtf8 = + new Utf8JsonReader((tailToken + "1").getBytes(StandardCharsets.UTF_8)); + assertTrue( + tailUtf8.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailUtf8.readIntTokenValue(), 1); + Latin1JsonReader tailLatin1 = new Latin1JsonReader(tailToken + "2"); + assertTrue( + tailLatin1.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailLatin1.readIntTokenValue(), 2); + + Utf8JsonReader mismatch = + new Utf8JsonReader("\"favoriteSeed\":\"pit\"".getBytes(StandardCharsets.UTF_8)); + assertFalse(mismatch.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(mismatch.readFieldNameHash(), JsonFieldNameHash.hash("favoriteSeed")); + mismatch.expectNextToken(':'); + assertEquals(mismatch.readNextNullableString(), "pit"); + } + + @Test + public void readGeneratedLongAsciiFields() { + ForyJson json = ForyJson.builder().build(); + String input = + "{\"registered\":\"today\",\"longitude\":12.5,\"favoriteFruit\":\"apple\"," + + "\"shortName\":\"core\"}"; + assertLongAsciiFields(json.fromJson(input, LongAsciiFields.class)); + assertLongAsciiFields( + json.fromJson(input.getBytes(StandardCharsets.UTF_8), LongAsciiFields.class)); + assertGeneratedWhenSupported(json, LongAsciiFields.class); + } + @Test public void readSplitGeneratedFields() { ForyJson json = ForyJson.builder().build(); @@ -157,6 +214,20 @@ private static void assertWideFields(WideFields value) { assertEquals(value.f13, "thirteen"); } + private static void assertLongAsciiFields(LongAsciiFields value) { + assertEquals(value.registered, "today"); + assertEquals(value.longitude, 12.5d); + assertEquals(value.favoriteFruit, "apple"); + assertEquals(value.shortName, "core"); + } + + public static class LongAsciiFields { + public String registered; + public double longitude; + public String favoriteFruit; + public String shortName; + } + public static class WideFields { public int f0; public String f1; From 10cc77fc1313423376761d4fa8c6822bdf1498d5 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 06:16:51 +0800 Subject: [PATCH 24/40] perf(json): read utf8 doubles directly --- .../apache/fory/json/codec/ScalarCodecs.java | 6 +- .../fory/json/codegen/JsonReaderCodegen.java | 48 ++++++++ .../apache/fory/json/reader/JsonReader.java | 4 + .../fory/json/reader/Utf8JsonReader.java | 108 ++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 17 +++ 5 files changed, 180 insertions(+), 3 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index fbb00260b2..676904530d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -441,7 +441,7 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - return Double.parseDouble(reader.readNumber()); + return reader.readDouble(); } @Override @@ -454,9 +454,9 @@ public void readField( if (reader.peekNull()) { readFieldDefault(reader, object, accessor, typeInfo, resolver); } else if (typeInfo.primitive()) { - accessor.putDouble(object, Double.parseDouble(reader.readNumber())); + accessor.putDouble(object, reader.readDouble()); } else { - accessor.putObject(object, Double.parseDouble(reader.readNumber())); + accessor.putObject(object, reader.readDouble()); } } } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index a99ae52ea8..46f6af6028 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -1166,6 +1166,14 @@ private static Expression readLongExpr(int readerMode, boolean tokenValueRead) { .inline(); } + private static Expression readDoubleExpr(int readerMode, boolean tokenValueRead) { + return new Expression.Invoke( + readerRef(readerMode), + readDoubleMethod(readerMode, tokenValueRead), + TypeRef.of(double.class)) + .inline(); + } + private static Expression readStringExpr(int readerMode) { return readStringExpr(readerMode, false); } @@ -1200,6 +1208,13 @@ private static String readLongMethod(int readerMode, boolean tokenValueRead) { return tokenValueRead ? "readLongTokenValue" : "readNextLongValue"; } + private static String readDoubleMethod(int readerMode, boolean tokenValueRead) { + if (readerMode == UTF8_READER) { + return tokenValueRead ? "readDoubleTokenValue" : "readNextDoubleValue"; + } + return "readDouble"; + } + private static String readStringMethod(int readerMode, boolean tokenValueRead) { if (readerMode == GENERIC_READER) { return "readNullableString"; @@ -1444,6 +1459,8 @@ private Expression readField( return readInt(builder, property, rawType, readerMode, object, tokenValueRead); case LONG: return readLong(builder, property, rawType, readerMode, object, tokenValueRead); + case DOUBLE: + return readDouble(builder, property, rawType, readerMode, object, tokenValueRead); case STRING: return builder.setField(property, object, readStringExpr(readerMode, tokenValueRead)); case ENUM: @@ -1480,6 +1497,8 @@ private Expression readRecordField( return readRecordInt(rawType, id, readerMode, object, tokenValueRead); case LONG: return readRecordLong(rawType, id, readerMode, object, tokenValueRead); + case DOUBLE: + return readRecordDouble(rawType, id, readerMode, object, tokenValueRead); case STRING: return assignRecord(object, id, readStringExpr(readerMode, tokenValueRead)); case ENUM: @@ -1541,6 +1560,18 @@ private static Expression readRecordLong( assignRecord(object, id, value)); } + private static Expression readRecordDouble( + Class rawType, int id, int readerMode, Expression object, boolean tokenValueRead) { + Expression value = box(Double.class, readDoubleExpr(readerMode, tokenValueRead)); + if (rawType.isPrimitive()) { + return assignRecord(object, id, value); + } + return new Expression.If( + tryReadNullExpr(readerMode), + assignRecord(object, id, new Expression.Null(TypeRef.of(Double.class), false)), + assignRecord(object, id, value)); + } + private static Expression readRecordEnum( int id, int readerMode, Expression object, boolean tokenValueRead) { return new Expression.If( @@ -1623,6 +1654,23 @@ private static Expression readLong( property, object, box(Long.class, readLongExpr(readerMode, tokenValueRead)))); } + private static Expression readDouble( + JsonGeneratedCodecBuilder builder, + JsonFieldInfo property, + Class rawType, + int readerMode, + Expression object, + boolean tokenValueRead) { + if (rawType.isPrimitive()) { + return builder.setField(property, object, readDoubleExpr(readerMode, tokenValueRead)); + } + return new Expression.If( + tryReadNullExpr(readerMode), + builder.setNull(property, object), + builder.setField( + property, object, box(Double.class, readDoubleExpr(readerMode, tokenValueRead)))); + } + private static Expression readEnum( JsonGeneratedCodecBuilder builder, JsonFieldInfo property, 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 1cb3552200..b76d6f8e2c 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 @@ -281,6 +281,10 @@ public final long readLong() { return negative ? result : -result; } + public double readDouble() { + return Double.parseDouble(readNumber()); + } + public int readFieldNameInt() { try { return Integer.parseInt(readString()); 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 dc28215a67..569f196208 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 @@ -360,6 +360,23 @@ public BigDecimal readBigDecimal() { return readBigDecimalToken(); } + @Override + public double readDouble() { + skipWhitespaceFast(); + return readDoubleToken(); + } + + public double readNextDoubleValue() { + if (position < input.length && !isWhitespace(input[position])) { + return readDoubleToken(); + } + return readDouble(); + } + + public double readDoubleTokenValue() { + return readDoubleToken(); + } + private long readLongToken() { byte[] bytes = input; int offset = position; @@ -539,6 +556,97 @@ private BigDecimal readBigDecimalFallback(int start) { return new BigDecimal(readNumber()); } + private double readDoubleToken() { + // Keep the fast path exact: compact plain decimals convert through BigDecimal's long+scale + // path, while exponents, overflow, and longer precision stay on Java's full parser. + byte[] bytes = input; + int offset = position; + int start = offset; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + boolean negative = false; + int ch = bytes[offset]; + if (ch == '-') { + negative = true; + offset++; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + ch = bytes[offset]; + } + long unscaled = 0; + int scale = 0; + int digits = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + digits++; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { + offset++; + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + digits++; + offset++; + } + if (offset == fractionStart) { + return readDoubleFallback(start); + } + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } + } + if (digits > 18) { + return readDoubleFallback(start); + } + position = offset; + if (negative && unscaled == 0) { + return -0.0d; + } + long value = negative ? -unscaled : unscaled; + return BigDecimal.valueOf(value, scale).doubleValue(); + } + + private double readDoubleFallback(int start) { + position = start; + return Double.parseDouble(readNumber()); + } + @Override public int readFieldNameInt() { skipWhitespaceFast(); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 02fa6e7c10..4dae4b52cf 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -134,6 +134,23 @@ public void readNumericBoundaries() { "{\"intMax\":2147483648,\"text\":\"" + ZH_TEXT + "\"}", NumericBoundaries.class)); } + @Test + public void readUtf8DoubleTokens() { + assertEquals( + new Utf8JsonReader("12.375".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue(), + 12.375d); + assertEquals( + Double.doubleToRawLongBits( + new Utf8JsonReader("-0.0".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()), + Double.doubleToRawLongBits(-0.0d)); + assertEquals( + new Utf8JsonReader("1.25e2".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue(), + 125.0d); + assertThrows( + ForyJsonException.class, + () -> new Utf8JsonReader("01.5".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()); + } + @Test public void writeNumericBoundaries() { ForyJson json = ForyJson.builder().build(); From d7556fedf98de7a70a3e2924a04b64cfaa665fcd Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 07:03:50 +0800 Subject: [PATCH 25/40] perf(json): read small utf8 string arrays directly --- .../apache/fory/json/codec/ArrayCodec.java | 60 ++++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 19 ++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index de3ca843b0..5d550fe59e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -777,8 +777,64 @@ public Object readUtf8( reader.exitDepth(); return new String[0]; } - String[] values = new String[8]; - int size = 0; + String v0 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0}; + } + String v1 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1}; + } + String v2 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2}; + } + String v3 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3}; + } + String v4 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4}; + } + String v5 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5}; + } + String v6 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + String v7 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + String[] values = new String[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index b9c5007e90..7c010e7e23 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -264,6 +264,25 @@ public void readPrimitiveArrayRoots() { assertThrows(ForyJsonException.class, () -> json.fromJson("[1,null]", int[].class)); } + @Test + public void readUtf8StringArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[\"a\"]".getBytes(StandardCharsets.UTF_8), String[].class), + new String[] {"a"}); + assertEquals( + json.fromJson( + "[\"a\",null,\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]".getBytes(StandardCharsets.UTF_8), + String[].class), + new String[] {"a", null, "b", "c", "d", "e", "f", "g"}); + assertEquals( + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\",\"h\",\"i\"]" + .getBytes(StandardCharsets.UTF_8), + String[].class), + new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = ForyJson.builder().build(); From 37b08da9ce28753200a89dd4ebbc1ffceb22a348 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 07:33:37 +0800 Subject: [PATCH 26/40] perf(json): read utf8 uuids directly --- .../apache/fory/json/codec/ScalarCodecs.java | 15 ++++++ .../fory/json/reader/Utf8JsonReader.java | 54 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 6 +++ 3 files changed, 75 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java index 676904530d..7902bf7134 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ScalarCodecs.java @@ -766,6 +766,21 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso Object fromJsonString(String value) { return UUID.fromString(value); } + + @Override + public Object readUtf8( + Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (reader.tryReadNullToken()) { + return null; + } + try { + return reader.readUuid(); + } catch (ForyJsonException e) { + throw e; + } catch (RuntimeException e) { + throw new ForyJsonException("Invalid " + typeInfo.rawType().getName() + " JSON string", e); + } + } } public static final class LocaleCodec extends StringValueCodec { 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 569f196208..e402c5040e 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 @@ -23,6 +23,7 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.util.UUID; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.meta.JsonFieldTable; @@ -360,6 +361,17 @@ public BigDecimal readBigDecimal() { return readBigDecimalToken(); } + public UUID readUuid() { + skipWhitespaceFast(); + int mark = position; + try { + return readUuidToken(); + } catch (RuntimeException e) { + position = mark; + return UUID.fromString(readStringToken()); + } + } + @Override public double readDouble() { skipWhitespaceFast(); @@ -556,6 +568,48 @@ private BigDecimal readBigDecimalFallback(int start) { return new BigDecimal(readNumber()); } + private UUID readUuidToken() { + byte[] bytes = input; + int offset = position; + int start = offset + 1; + if (offset + 38 > bytes.length || bytes[offset] != '"') { + throw new IllegalArgumentException(); + } + if (bytes[start + 8] != '-' + || bytes[start + 13] != '-' + || bytes[start + 18] != '-' + || bytes[start + 23] != '-' + || bytes[start + 36] != '"') { + throw new IllegalArgumentException(); + } + long msb = parseHex(bytes, start, 8); + msb = (msb << 16) | parseHex(bytes, start + 9, 4); + msb = (msb << 16) | parseHex(bytes, start + 14, 4); + long lsb = parseHex(bytes, start + 19, 4); + lsb = (lsb << 48) | parseHex(bytes, start + 24, 12); + position = start + 37; + return new UUID(msb, lsb); + } + + private static long parseHex(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = 0; i < length; i++) { + value = (value << 4) | hexValue(bytes[offset + i]); + } + return value; + } + + private static int hexValue(int ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + int lower = ch | 0x20; + if (lower >= 'a' && lower <= 'f') { + return lower - 'a' + 10; + } + throw new IllegalArgumentException(); + } + private double readDoubleToken() { // Keep the fast path exact: compact plain decimals convert through BigDecimal's long+scale // path, while exponents, overflow, and longer precision stay on Java's full parser. diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 4dae4b52cf..99f69009ad 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -302,6 +302,11 @@ public void readScalarRoots() { json.fromJson( "12345678901234567890.123".getBytes(StandardCharsets.UTF_8), BigDecimal.class), new BigDecimal("12345678901234567890.123")); + assertEquals( + json.fromJson( + "\"123e4567-e89b-12d3-a456-426614174000\"".getBytes(StandardCharsets.UTF_8), + UUID.class), + UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); } @Test @@ -314,6 +319,7 @@ public void readGeneratedUtf8BigDecimal() { + "\"timestamp\":\"2024-02-03T04:05:06Z\"}") .getBytes(StandardCharsets.UTF_8); Utf8ScalarFields fields = json.fromJson(input, Utf8ScalarFields.class); + assertEquals(fields.uuid, UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); assertEquals(fields.decimal, new BigDecimal("0.12345678901234567")); } From d19e4102974ec15c3d08847d42ba9ac9d7229cf9 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 07:50:59 +0800 Subject: [PATCH 27/40] perf(json): read small utf8 long arrays directly --- .../apache/fory/json/codec/ArrayCodec.java | 68 ++++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 16 +++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 5d550fe59e..ca71456746 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -313,8 +313,72 @@ public Object readUtf8( reader.exitDepth(); return new long[0]; } - long[] values = new long[8]; - int size = 0; + rejectNull(reader); + long v0 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0}; + } + rejectNull(reader); + long v1 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1}; + } + rejectNull(reader); + long v2 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2}; + } + rejectNull(reader); + long v3 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3}; + } + rejectNull(reader); + long v4 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4}; + } + rejectNull(reader); + long v5 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5}; + } + rejectNull(reader); + long v6 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6}; + } + rejectNull(reader); + long v7 = reader.readLongValue(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } + long[] values = new long[16]; + values[0] = v0; + values[1] = v1; + values[2] = v2; + values[3] = v3; + values[4] = v4; + values[5] = v5; + values[6] = v6; + values[7] = v7; + int size = 8; do { rejectNull(reader); if (size == values.length) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 7c010e7e23..9ca1d9f6ad 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -283,6 +283,22 @@ public void readUtf8StringArrays() { new String[] {"a", "b", "c", "d", "e", "f", "g", "h", "i"}); } + @Test + public void readUtf8LongArrays() { + ForyJson json = ForyJson.builder().build(); + assertEquals( + json.fromJson("[7]".getBytes(StandardCharsets.UTF_8), long[].class), new long[] {7L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8]".getBytes(StandardCharsets.UTF_8), long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L}); + assertEquals( + json.fromJson("[1,2,3,4,5,6,7,8,9]".getBytes(StandardCharsets.UTF_8), long[].class), + new long[] {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L}); + assertThrows( + ForyJsonException.class, + () -> json.fromJson("[1,null]".getBytes(StandardCharsets.UTF_8), long[].class)); + } + @Test public void writeReadBoxedPrimitiveArrays() { ForyJson json = ForyJson.builder().build(); From ffa29c809fc1892000758a2b991e465324d89a39 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 09:28:08 +0800 Subject: [PATCH 28/40] perf(json): read small utf8 arraylists exactly --- .../fory/json/codec/CollectionCodec.java | 135 +++++++++++++++++- .../apache/fory/json/JsonContainerTest.java | 8 ++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index 66c4d78ac9..e62aeed201 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -50,10 +50,12 @@ public abstract class CollectionCodec extends AbstractJsonCodec { private final TypeRef typeRef; private final CollectionFactory factory; + private final boolean createsArrayList; CollectionCodec(TypeRef typeRef, CollectionFactory factory) { this.typeRef = typeRef; this.factory = factory; + this.createsArrayList = factory.createsArrayList(); } public static CollectionCodec create( @@ -118,6 +120,10 @@ final Collection newCollection() { return factory.newCollection(); } + final boolean createsArrayList() { + return createsArrayList; + } + public abstract Object readLatin1NonNull( Latin1JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver); @@ -166,7 +172,7 @@ private static CollectionFactory collectionFactory(Class rawType, Class el if (Queue.class.isAssignableFrom(rawType)) { return ArrayDeque::new; } - return () -> new ArrayList<>(0); + return CollectionFactory.ARRAY_LIST; } return () -> { try { @@ -178,7 +184,24 @@ private static CollectionFactory collectionFactory(Class rawType, Class el } private interface CollectionFactory { + CollectionFactory ARRAY_LIST = + new CollectionFactory() { + @Override + public Collection newCollection() { + return new ArrayList<>(0); + } + + @Override + public boolean createsArrayList() { + return true; + } + }; + Collection newCollection(); + + default boolean createsArrayList() { + return false; + } } public abstract static class DirectCollectionCodec extends CollectionCodec { @@ -260,6 +283,9 @@ public final Object readUtf8( @Override public final Object readUtf8NonNull( Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf8ArrayListNonNull(reader); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -272,6 +298,113 @@ public final Object readUtf8NonNull( return collection; } + private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + Object e4 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + Object e6 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf8Element(reader); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf8Element(reader)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + abstract Object readElement(JsonReader reader); Object readNullableElement(JsonReader reader) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 9ca1d9f6ad..942789ce88 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -122,6 +122,14 @@ public void parsedContainersStartSmall() { assertTrue(list instanceof ArrayList); assertEquals(arrayCapacity((ArrayList) list), 1); + List strings = + json.fromJson( + "[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\",\"g\"]".getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}); + assertTrue(strings instanceof ArrayList); + assertEquals(strings, Arrays.asList("a", "b", "c", "d", "e", "f", "g")); + assertEquals(arrayCapacity((ArrayList) strings), 7); + JSONObject object = json.fromJson("{\"x\":1}", JSONObject.class); assertEquals(mapCapacity(object), 2); assertEquals(mapCapacity(json.fromJson("{}", JSONObject.class)), 0); From 99a9b4084bfc63a1f93ba28c57adaadbafea3061 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 09:47:29 +0800 Subject: [PATCH 29/40] perf(json): read nine utf8 strings exactly --- .../main/java/org/apache/fory/json/codec/ArrayCodec.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index ca71456746..5151ff52ea 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -889,6 +889,12 @@ public Object readUtf8( reader.exitDepth(); return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; } + String v8 = reader.readNextNullableString(); + if (!reader.consumeNextToken(',')) { + reader.expectNextToken(']'); + reader.exitDepth(); + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; + } String[] values = new String[16]; values[0] = v0; values[1] = v1; @@ -898,7 +904,8 @@ public Object readUtf8( values[5] = v5; values[6] = v6; values[7] = v7; - int size = 8; + values[8] = v8; + int size = 9; do { if (size == values.length) { values = Arrays.copyOf(values, values.length << 1); From b34ac9a7e0093031f3f218447979660bc297118a Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 10:43:06 +0800 Subject: [PATCH 30/40] perf(json): split utf8 arraylist reads --- .../org/apache/fory/json/codec/CollectionCodec.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index e62aeed201..ed98da11dc 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -339,6 +339,11 @@ private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { list.add(e3); return list; } + return readUtf8ArrayListTail(reader, e0, e1, e2, e3); + } + + private ArrayList readUtf8ArrayListTail( + Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3) { Object e4 = readNullableUtf8Element(reader); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); @@ -362,6 +367,11 @@ private ArrayList readUtf8ArrayListNonNull(Utf8JsonReader reader) { list.add(e5); return list; } + return readUtf8ArrayListLongTail(reader, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf8ArrayListLongTail( + Utf8JsonReader reader, Object e0, Object e1, Object e2, Object e3, Object e4, Object e5) { Object e6 = readNullableUtf8Element(reader); if (!reader.consumeNextCommaOrEndArray()) { reader.exitDepth(); From a41c982a47b512dc03574851383e8339be4c2396 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 13:12:34 +0800 Subject: [PATCH 31/40] perf(json): speed up negative long reads --- .../fory/json/reader/Utf8JsonReader.java | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) 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 e402c5040e..b730fcb2a6 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 @@ -454,15 +454,15 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon } private long readNegativeLongToken(int start) { - position = start + 1; - long result = 0; - long limit = Long.MIN_VALUE; - if (position >= input.length) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { throw error("Expected digit"); } - int ch = input[position]; + int ch = bytes[offset]; if (ch == '0') { - position++; + position = offset + 1; rejectLeadingDigitFast(); rejectFractionOrExponentFast(); return 0; @@ -470,23 +470,52 @@ private long readNegativeLongToken(int start) { if (ch < '1' || ch > '9') { throw error("Expected digit"); } - long multmin = limit / 10; - while (position < input.length) { - ch = input[position]; + long result = '0' - ch; + offset++; + int safeEnd = offset + 17; + if (safeEnd > inputLength) { + safeEnd = inputLength; + } + while (offset < safeEnd) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + result = result * 10 - (ch - '0'); + offset++; + } + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readNegativeLongTail(bytes, offset, inputLength, result); + } + } + position = offset; + rejectFractionOrExponentFast(); + return result; + } + + private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, long result) { + long multmin = Long.MIN_VALUE / 10; + while (offset < inputLength) { + int ch = bytes[offset]; if (ch < '0' || ch > '9') { break; } int digit = ch - '0'; if (result < multmin) { + position = offset; throw error("Long overflow"); } result *= 10; if (result < Long.MIN_VALUE + digit) { + position = offset; throw error("Long overflow"); } result -= digit; - position++; + offset++; } + position = offset; rejectFractionOrExponentFast(); return result; } From 57fa99a770368fde93b6f4ce69745bda32baaf65 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 13:20:19 +0800 Subject: [PATCH 32/40] perf(json): avoid long tail division --- .../apache/fory/json/reader/Utf8JsonReader.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 b730fcb2a6..81cadae41d 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 @@ -42,6 +42,10 @@ public final class Utf8JsonReader extends JsonReader { private static final int INT_CONTROL_LIMIT_BYTES = 0x20202020; private static final long QUOTE_BYTES = 0x2222222222222222L; private static final int INT_QUOTE_BYTES = 0x22222222; + private static final long LONG_MAX_DIV_10 = Long.MAX_VALUE / 10; + private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); + private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; + private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. @@ -441,7 +445,7 @@ private long readPositiveLongTail(byte[] bytes, int offset, int inputLength, lon break; } int digit = ch - '0'; - if (result > (Long.MAX_VALUE - digit) / 10) { + if (result > LONG_MAX_DIV_10 || (result == LONG_MAX_DIV_10 && digit > LONG_MAX_MOD_10)) { position = offset; throw error("Long overflow"); } @@ -496,23 +500,17 @@ private long readNegativeLongToken(int start) { } private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, long result) { - long multmin = Long.MIN_VALUE / 10; while (offset < inputLength) { int ch = bytes[offset]; if (ch < '0' || ch > '9') { break; } int digit = ch - '0'; - if (result < multmin) { - position = offset; - throw error("Long overflow"); - } - result *= 10; - if (result < Long.MIN_VALUE + digit) { + if (result < LONG_MIN_DIV_10 || (result == LONG_MIN_DIV_10 && digit > LONG_MIN_LAST_DIGIT)) { position = offset; throw error("Long overflow"); } - result -= digit; + result = result * 10 - digit; offset++; } position = offset; From 277a3144cd93a0c673755e47975fb2eadef6a5a3 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 14:14:37 +0800 Subject: [PATCH 33/40] perf(json): cache object array reads --- .../apache/fory/json/codec/ArrayCodec.java | 62 +++++++++++++------ .../apache/fory/json/JsonContainerTest.java | 52 ++++++++++++++++ 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 5151ff52ea..54d6827329 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -20,9 +20,7 @@ package org.apache.fory.json.codec; import java.lang.reflect.Array; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import org.apache.fory.json.ForyJsonException; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; @@ -919,8 +917,13 @@ public Object readUtf8( } public static final class ObjectArrayCodec extends ArrayCodec { + private static final int INITIAL_VALUES_SIZE = 8; + private static final int MAX_CACHED_VALUES_SIZE = 1024; + private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; + // Null means an active read borrowed the scratch array; reentrant reads use temporary storage. + private Object[] valuesCache = new Object[INITIAL_VALUES_SIZE]; private ObjectArrayCodec(Class componentType, JsonTypeInfo elementTypeInfo) { super(componentType); @@ -963,25 +966,46 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - reader.enterDepth(); - List values = new ArrayList<>(0); - reader.expect('['); - if (!reader.consume(']')) { - do { - values.add(elementCodec.read(reader, elementTypeInfo, resolver)); - } while (reader.consume(',')); - reader.expect(']'); + Object[] values = valuesCache; + boolean restoreCache = values != null; + valuesCache = null; + if (values == null) { + values = new Object[INITIAL_VALUES_SIZE]; } - reader.exitDepth(); - return toArray(values); - } - - private Object toArray(List values) { - Object array = Array.newInstance(componentType, values.size()); - for (int i = 0; i < values.size(); i++) { - Array.set(array, i, values.get(i)); + int size = 0; + boolean entered = false; + try { + reader.enterDepth(); + entered = true; + reader.expect('['); + if (!reader.consume(']')) { + do { + if (size == values.length) { + values = Arrays.copyOf(values, values.length << 1); + } + values[size++] = elementCodec.read(reader, elementTypeInfo, resolver); + } while (reader.consume(',')); + reader.expect(']'); + } + Object[] array = (Object[]) Array.newInstance(componentType, size); + System.arraycopy(values, 0, array, 0, size); + return array; + } finally { + try { + if (entered) { + reader.exitDepth(); + } + } finally { + if (restoreCache) { + if (values.length <= MAX_CACHED_VALUES_SIZE) { + Arrays.fill(values, 0, size, null); + valuesCache = values; + } else { + valuesCache = new Object[INITIAL_VALUES_SIZE]; + } + } + } } - return array; } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 942789ce88..9d6c6a9646 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -341,6 +341,53 @@ public void writeReadBoxedPrimitiveArrays() { assertThrows(ForyJsonException.class, () -> json.fromJson("[\"ab\"]", Character[].class)); } + @Test + public void readReferenceObjectArrays() { + ForyJson json = ForyJson.builder().build(); + Note[] notes = json.fromJson("[{\"title\":\"one\"},null,{\"title\":\"two\"}]", Note[].class); + assertEquals(notes.getClass(), Note[].class); + assertEquals(notes.length, 3); + assertEquals(notes[0].title, "one"); + assertEquals(notes[1], null); + assertEquals(notes[2].title, "two"); + + Note[] empty = json.fromJson("[]".getBytes(StandardCharsets.UTF_8), Note[].class); + assertEquals(empty.getClass(), Note[].class); + assertEquals(empty.length, 0); + + Note[][] grid = + json.fromJson( + "[[{\"title\":\"left\"}],null,[{\"title\":\"right\"},null]]" + .getBytes(StandardCharsets.UTF_8), + Note[][].class); + assertEquals(grid.getClass(), Note[][].class); + assertEquals(grid.length, 3); + assertEquals(grid[0].getClass(), Note[].class); + assertEquals(grid[0][0].title, "left"); + assertEquals(grid[1], null); + assertEquals(grid[2].getClass(), Note[].class); + assertEquals(grid[2][0].title, "right"); + assertEquals(grid[2][1], null); + } + + @Test + public void readReentrantObjectArray() { + ForyJson json = ForyJson.builder().build(); + ArrayNode[] roots = + json.fromJson( + "[{\"name\":\"root\",\"children\":[{\"name\":\"leaf\",\"children\":[]}]}]" + .getBytes(StandardCharsets.UTF_8), + ArrayNode[].class); + assertEquals(roots.getClass(), ArrayNode[].class); + assertEquals(roots.length, 1); + assertEquals(roots[0].name, "root"); + assertEquals(roots[0].children.getClass(), ArrayNode[].class); + assertEquals(roots[0].children.length, 1); + assertEquals(roots[0].children[0].name, "leaf"); + assertEquals(roots[0].children[0].children.getClass(), ArrayNode[].class); + assertEquals(roots[0].children[0].children.length, 0); + } + @Test public void writeReadAtomicArrays() { ForyJson json = ForyJson.builder().build(); @@ -387,6 +434,11 @@ public static final class Note { public String title; } + public static final class ArrayNode { + public String name; + public ArrayNode[] children; + } + public static final class PaletteGroups extends HashMap {} public static final class PaletteCodes extends HashMap {} From 01bbb9a11076ffbf7ac63a63fe3f49963dc72caa Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 14:30:43 +0800 Subject: [PATCH 34/40] perf(json): stack object array scratch buffers --- .../apache/fory/json/codec/ArrayCodec.java | 46 +++++++++++-------- .../apache/fory/json/JsonContainerTest.java | 25 ++++++++++ 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 54d6827329..968b15f86c 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -917,13 +917,15 @@ public Object readUtf8( } public static final class ObjectArrayCodec extends ArrayCodec { + private static final int VALUES_CACHE_DEPTH = 8; private static final int INITIAL_VALUES_SIZE = 8; private static final int MAX_CACHED_VALUES_SIZE = 1024; private final JsonTypeInfo elementTypeInfo; private final JsonCodec elementCodec; - // Null means an active read borrowed the scratch array; reentrant reads use temporary storage. - private Object[] valuesCache = new Object[INITIAL_VALUES_SIZE]; + // Recursive object-array reads borrow one scratch slot per active depth. + private final Object[][] valuesCache = new Object[VALUES_CACHE_DEPTH][]; + private int valuesDepth; private ObjectArrayCodec(Class componentType, JsonTypeInfo elementTypeInfo) { super(componentType); @@ -966,17 +968,21 @@ void writeUtf8NonNull(Utf8JsonWriter writer, Object value, JsonTypeResolver reso @Override Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { - Object[] values = valuesCache; - boolean restoreCache = values != null; - valuesCache = null; + reader.enterDepth(); + int depth = valuesDepth; + boolean useCache = depth < VALUES_CACHE_DEPTH; + Object[] values = null; + if (useCache) { + values = valuesCache[depth]; + valuesCache[depth] = null; + } if (values == null) { values = new Object[INITIAL_VALUES_SIZE]; } int size = 0; - boolean entered = false; + boolean success = false; + valuesDepth = depth + 1; try { - reader.enterDepth(); - entered = true; reader.expect('['); if (!reader.consume(']')) { do { @@ -989,22 +995,22 @@ Object readNonNull(JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver re } Object[] array = (Object[]) Array.newInstance(componentType, size); System.arraycopy(values, 0, array, 0, size); + success = true; return array; } finally { - try { - if (entered) { - reader.exitDepth(); - } - } finally { - if (restoreCache) { - if (values.length <= MAX_CACHED_VALUES_SIZE) { - Arrays.fill(values, 0, size, null); - valuesCache = values; - } else { - valuesCache = new Object[INITIAL_VALUES_SIZE]; - } + reader.exitDepth(); + // Failed reads drop the scratch array, because it may contain partially parsed user values. + if (success && useCache) { + if (values.length <= MAX_CACHED_VALUES_SIZE) { + Arrays.fill(values, 0, size, null); + valuesCache[depth] = values; + } else { + // Keep the depth slot usable without retaining a grown array from one large value. + valuesCache[depth] = new Object[INITIAL_VALUES_SIZE]; } } + // Restore the codec recursion depth after the matching cache slot has been handled. + valuesDepth = depth; } } } diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 9d6c6a9646..00237c7e15 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -386,6 +386,22 @@ public void readReentrantObjectArray() { assertEquals(roots[0].children[0].name, "leaf"); assertEquals(roots[0].children[0].children.getClass(), ArrayNode[].class); assertEquals(roots[0].children[0].children.length, 0); + + ArrayNode[] deep = + json.fromJson(arrayNodeJson(9).getBytes(StandardCharsets.UTF_8), ArrayNode[].class); + assertEquals(deep.getClass(), ArrayNode[].class); + assertEquals(deep.length, 1); + ArrayNode node = deep[0]; + for (int i = 0; i < 9; i++) { + assertEquals(node.name, "node-" + i); + assertEquals(node.children.getClass(), ArrayNode[].class); + if (i == 8) { + assertEquals(node.children.length, 0); + } else { + assertEquals(node.children.length, 1); + node = node.children[0]; + } + } } @Test @@ -449,6 +465,15 @@ public static final class AtomicArrayFields { public AtomicReferenceArray refs = new AtomicReferenceArray<>(new String[] {"b", null}); } + private static String arrayNodeJson(int depth) { + return "[" + nodeJson(0, depth) + "]"; + } + + private static String nodeJson(int index, int depth) { + String children = index + 1 == depth ? "[]" : "[" + nodeJson(index + 1, depth) + "]"; + return "{\"name\":\"node-" + index + "\",\"children\":" + children + "}"; + } + private static void assertAtomicInts(AtomicIntegerArray array, int... expected) { assertEquals(array.length(), expected.length); for (int i = 0; i < expected.length; i++) { From f457f9fd7a1b4c18d5e4ee949b1263f0d0c89dae Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 15:03:52 +0800 Subject: [PATCH 35/40] perf(json): read object arraylists exactly --- .../fory/json/codec/CollectionCodec.java | 139 ++++++++++++++++++ .../apache/fory/json/JsonContainerTest.java | 13 ++ 2 files changed, 152 insertions(+) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java index ed98da11dc..a0af5b054a 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/CollectionCodec.java @@ -754,6 +754,9 @@ public Object readUtf8( @Override public Object readUtf8NonNull( Utf8JsonReader reader, JsonTypeInfo typeInfo, JsonTypeResolver resolver) { + if (createsArrayList()) { + return readUtf8ArrayListNonNull(reader, resolver); + } reader.enterDepth(); Collection collection = newCollection(); reader.expectNextToken('['); @@ -768,6 +771,142 @@ public Object readUtf8NonNull( reader.exitDepth(); return collection; } + + private ArrayList readUtf8ArrayListNonNull( + Utf8JsonReader reader, JsonTypeResolver resolver) { + reader.enterDepth(); + reader.expectNextToken('['); + if (reader.consumeNextToken(']')) { + reader.exitDepth(); + return new ArrayList<>(0); + } + Object e0 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(1); + list.add(e0); + return list; + } + Object e1 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(2); + list.add(e0); + list.add(e1); + return list; + } + Object e2 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(3); + list.add(e0); + list.add(e1); + list.add(e2); + return list; + } + Object e3 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(4); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + return list; + } + return readUtf8ArrayListTail(reader, resolver, e0, e1, e2, e3); + } + + private ArrayList readUtf8ArrayListTail( + Utf8JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3) { + Object e4 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(5); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + return list; + } + Object e5 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(6); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + return list; + } + return readUtf8ArrayListLongTail(reader, resolver, e0, e1, e2, e3, e4, e5); + } + + private ArrayList readUtf8ArrayListLongTail( + Utf8JsonReader reader, + JsonTypeResolver resolver, + Object e0, + Object e1, + Object e2, + Object e3, + Object e4, + Object e5) { + Object e6 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(7); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + return list; + } + Object e7 = readNullableUtf8Element(reader, resolver); + if (!reader.consumeNextCommaOrEndArray()) { + reader.exitDepth(); + ArrayList list = new ArrayList<>(8); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + return list; + } + ArrayList list = new ArrayList<>(9); + list.add(e0); + list.add(e1); + list.add(e2); + list.add(e3); + list.add(e4); + list.add(e5); + list.add(e6); + list.add(e7); + do { + list.add(readNullableUtf8Element(reader, resolver)); + } while (reader.consumeNextCommaOrEndArray()); + reader.exitDepth(); + return list; + } + + private Object readNullableUtf8Element(Utf8JsonReader reader, JsonTypeResolver resolver) { + return reader.tryReadNextNullToken() + ? null + : elementCodec.readUtf8NonNull(reader, elementTypeInfo, resolver); + } } public static final class StringCollectionCodec extends DirectCollectionCodec { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java index 00237c7e15..d441b9de3a 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonContainerTest.java @@ -130,6 +130,19 @@ public void parsedContainersStartSmall() { assertEquals(strings, Arrays.asList("a", "b", "c", "d", "e", "f", "g")); assertEquals(arrayCapacity((ArrayList) strings), 7); + List notes = + json.fromJson( + ("[{\"title\":\"a\"},{\"title\":\"b\"},null,{\"title\":\"d\"}," + + "{\"title\":\"e\"},{\"title\":\"f\"},{\"title\":\"g\"}]") + .getBytes(StandardCharsets.UTF_8), + new TypeRef>() {}); + assertTrue(notes instanceof ArrayList); + assertEquals(notes.size(), 7); + assertEquals(notes.get(0).title, "a"); + assertEquals(notes.get(2), null); + assertEquals(notes.get(6).title, "g"); + assertEquals(arrayCapacity((ArrayList) notes), 7); + JSONObject object = json.fromJson("{\"x\":1}", JSONObject.class); assertEquals(mapCapacity(object), 2); assertEquals(mapCapacity(json.fromJson("{}", JSONObject.class)), 0); From 306c134a6aa8917da77b5f0122f92bee6d55fc18 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 18:19:29 +0800 Subject: [PATCH 36/40] perf(java): compact Fory JSON reader hot paths --- .../apache/fory/json/codec/ArrayCodec.java | 67 ++++++- .../fory/json/codegen/JsonReaderCodegen.java | 163 +++++++++++++---- .../fory/json/reader/Utf8JsonReader.java | 170 ++++++++++++++++-- 3 files changed, 348 insertions(+), 52 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java index 968b15f86c..aff3817589 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codec/ArrayCodec.java @@ -339,6 +339,10 @@ public Object readUtf8( reader.exitDepth(); return new long[] {v0, v1, v2, v3}; } + return readUtf8Tail(reader, v0, v1, v2, v3); + } + + private long[] readUtf8Tail(Utf8JsonReader reader, long v0, long v1, long v2, long v3) { rejectNull(reader); long v4 = reader.readLongValue(); if (!reader.consumeNextToken(',')) { @@ -367,6 +371,21 @@ public Object readUtf8( reader.exitDepth(); return new long[] {v0, v1, v2, v3, v4, v5, v6, v7}; } + return readUtf8LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7); + } + + // Keep dynamic growth out of the exact small-array path so C2 can inline the common cases + // without pulling the uncommon grow/copy loop into the caller's inline budget. + private long[] readUtf8LongTail( + Utf8JsonReader reader, + long v0, + long v1, + long v2, + long v3, + long v4, + long v5, + long v6, + long v7) { long[] values = new long[16]; values[0] = v0; values[1] = v1; @@ -863,29 +882,34 @@ public Object readUtf8( reader.exitDepth(); return new String[] {v0, v1, v2, v3}; } + return readUtf8Tail(reader, v0, v1, v2, v3); + } + + private String[] readUtf8Tail( + Utf8JsonReader reader, String v0, String v1, String v2, String v3) { String v4 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4}; + return stringArray5(v0, v1, v2, v3, v4); } String v5 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4, v5}; + return stringArray6(v0, v1, v2, v3, v4, v5); } String v6 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4, v5, v6}; + return stringArray7(v0, v1, v2, v3, v4, v5, v6); } String v7 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { reader.expectNextToken(']'); reader.exitDepth(); - return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + return stringArray8(v0, v1, v2, v3, v4, v5, v6, v7); } String v8 = reader.readNextNullableString(); if (!reader.consumeNextToken(',')) { @@ -893,6 +917,22 @@ public Object readUtf8( reader.exitDepth(); return new String[] {v0, v1, v2, v3, v4, v5, v6, v7, v8}; } + return readUtf8LongTail(reader, v0, v1, v2, v3, v4, v5, v6, v7, v8); + } + + // Keep dynamic growth out of the exact small-array path so C2 can inline the common cases + // without pulling the uncommon grow/copy loop into the caller's inline budget. + private String[] readUtf8LongTail( + Utf8JsonReader reader, + String v0, + String v1, + String v2, + String v3, + String v4, + String v5, + String v6, + String v7, + String v8) { String[] values = new String[16]; values[0] = v0; values[1] = v1; @@ -914,6 +954,25 @@ public Object readUtf8( reader.exitDepth(); return Arrays.copyOf(values, size); } + + private static String[] stringArray5(String v0, String v1, String v2, String v3, String v4) { + return new String[] {v0, v1, v2, v3, v4}; + } + + private static String[] stringArray6( + String v0, String v1, String v2, String v3, String v4, String v5) { + return new String[] {v0, v1, v2, v3, v4, v5}; + } + + private static String[] stringArray7( + String v0, String v1, String v2, String v3, String v4, String v5, String v6) { + return new String[] {v0, v1, v2, v3, v4, v5, v6}; + } + + private static String[] stringArray8( + String v0, String v1, String v2, String v3, String v4, String v5, String v6, String v7) { + return new String[] {v0, v1, v2, v3, v4, v5, v6, v7}; + } } public static final class ObjectArrayCodec extends ArrayCodec { diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java index 46f6af6028..d1cae5ad1d 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonReaderCodegen.java @@ -29,6 +29,7 @@ import org.apache.fory.json.meta.JsonAsciiToken; import org.apache.fory.json.meta.JsonFieldInfo; import org.apache.fory.json.meta.JsonFieldKind; +import org.apache.fory.json.meta.JsonFieldTable; import org.apache.fory.json.reader.JsonReader; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Latin1ObjectReader; @@ -48,6 +49,7 @@ final class JsonReaderCodegen { private static final int UTF8_READER = JsonCodegen.UTF8_READER; private static final int MIN_SPLIT_READ_FIELDS = 12; private static final int READ_FIELD_GROUP_SIZE = 2; + private static final int READ_FIELD_SWITCH_SIZE = 8; private final JsonCodegen codegen; @@ -75,7 +77,8 @@ String genReaderCode( JsonReader.class, Latin1JsonReader.class, Utf16JsonReader.class, - Utf8JsonReader.class); + Utf8JsonReader.class, + JsonFieldTable.class); ctx.implementsInterfaces( ctx.type(ObjectReader.class), ctx.type(Latin1ObjectReader.class), @@ -103,14 +106,6 @@ String genReaderCode( "codecs", BaseObjectCodec[].class, "objectCodecs"); - addGeneratedMethod( - ctx, - "final", - "fieldIndex", - fieldIndexExpression(properties), - int.class, - long.class, - "fieldHash"); addGeneratedMethod( ctx, "public", @@ -123,6 +118,8 @@ String genReaderCode( "owner", JsonTypeResolver.class, "typeResolver"); + addReadFieldMethods( + ctx, builder, "read", JsonReader.class, type, properties, GENERIC_READER, record); addGeneratedMethod( ctx, "public", @@ -146,6 +143,15 @@ String genReaderCode( properties, LATIN1_READER, record); + addReadFieldMethods( + ctx, + builder, + "readLatin1", + Latin1JsonReader.class, + type, + properties, + LATIN1_READER, + record); addSlowReadMethods( ctx, builder, @@ -178,6 +184,8 @@ String genReaderCode( properties, UTF16_READER, record); + addReadFieldMethods( + ctx, builder, "readUtf16", Utf16JsonReader.class, type, properties, UTF16_READER, record); addSlowReadMethods( ctx, builder, @@ -210,6 +218,8 @@ String genReaderCode( properties, UTF8_READER, record); + addReadFieldMethods( + ctx, builder, "readUtf8", Utf8JsonReader.class, type, properties, UTF8_READER, record); addSlowReadMethods( ctx, builder, "readUtf8Slow", Utf8JsonReader.class, type, properties, UTF8_READER, record); return ctx.genCode(); @@ -252,6 +262,49 @@ private void addFastReadGroupMethods( } } + private void addReadFieldMethods( + CodegenContext ctx, + JsonGeneratedCodecBuilder builder, + String readMethod, + Class readerType, + Class type, + JsonFieldInfo[] properties, + int readerMode, + boolean record) { + if (!shouldSplitFieldSwitch(properties)) { + return; + } + Class objectType = record ? Object[].class : type; + for (int start = 0; start < properties.length; start += READ_FIELD_SWITCH_SIZE) { + int end = Math.min(start + READ_FIELD_SWITCH_SIZE, properties.length); + addGeneratedMethod( + ctx, + "final", + readFieldMethod(readMethod, start), + fieldSwitchRange( + builder, + type, + properties, + start, + end, + readerMode, + objectParam(type, record), + new Reference("fieldIndex", TypeRef.of(int.class)), + record), + void.class, + readerType, + "reader", + BaseObjectCodec.class, + "owner", + JsonTypeResolver.class, + "typeResolver", + objectType, + "object", + int.class, + "fieldIndex"); + } + } + private void addSlowReadMethods( CodegenContext ctx, JsonGeneratedCodecBuilder builder, @@ -367,19 +420,6 @@ private Expression readerConstructorExpression(Class type, JsonFieldInfo[] pr return expressions; } - private Expression fieldIndexExpression(JsonFieldInfo[] properties) { - Expression.ListExpression expressions = new Expression.ListExpression(); - Reference fieldHash = new Reference("fieldHash", TypeRef.of(long.class)); - for (int i = 0; i < properties.length; i++) { - expressions.add( - new Expression.If( - eq(fieldHash, Expression.Literal.ofLong(properties[i].nameHash())), - new Expression.Return(Expression.Literal.ofInt(i)))); - } - expressions.add(new Expression.Return(Expression.Literal.ofInt(-1))); - return expressions; - } - private Expression readExpression( JsonGeneratedCodecBuilder builder, Class type, @@ -822,6 +862,10 @@ private static String readGroupMethod(String readMethod, int start) { return readMethod + "Group" + start; } + private static String readFieldMethod(String readMethod, int start) { + return readMethod + "Field" + start; + } + private static int readGroupEnd(JsonFieldInfo[] properties, int start) { int weight = 0; int index = start; @@ -852,6 +896,25 @@ private static int readFieldWeight(JsonFieldInfo property) { } } + private static String readMethod(int readerMode) { + switch (readerMode) { + case GENERIC_READER: + return "read"; + case LATIN1_READER: + return "readLatin1"; + case UTF16_READER: + return "readUtf16"; + case UTF8_READER: + return "readUtf8"; + default: + throw new IllegalArgumentException(String.valueOf(readerMode)); + } + } + + private static boolean shouldSplitFieldSwitch(JsonFieldInfo[] properties) { + return properties.length > READ_FIELD_SWITCH_SIZE; + } + private Expression objectExpression(JsonGeneratedCodecBuilder builder, boolean record) { if (record) { return new Expression.Variable( @@ -1029,9 +1092,51 @@ private Expression fieldSwitch( Expression object, Expression fieldIndex, boolean record) { - Expression.Switch.Case[] cases = new Expression.Switch.Case[properties.length]; - for (int i = 0; i < properties.length; i++) { - cases[i] = + if (shouldSplitFieldSwitch(properties)) { + int chunks = (properties.length + READ_FIELD_SWITCH_SIZE - 1) / READ_FIELD_SWITCH_SIZE; + Expression.Switch.Case[] cases = new Expression.Switch.Case[chunks]; + for (int i = 0, start = 0; i < chunks; i++, start += READ_FIELD_SWITCH_SIZE) { + cases[i] = + new Expression.Switch.Case( + i, + new Expression.ListExpression( + new Expression.Invoke( + new Reference("this", TypeRef.of(Object.class)), + readFieldMethod(readMethod(readerMode), start), + "", + TypeRef.of(void.class), + false, + false, + readerRef(readerMode), + ownerRef(), + typeResolverRef(), + object, + fieldIndex), + new Expression.Break())); + } + Expression chunkIndex = + new Expression.Arithmetic( + true, "/", fieldIndex, Expression.Literal.ofInt(READ_FIELD_SWITCH_SIZE)); + return new Expression.Switch( + chunkIndex, cases, new Expression.Invoke(readerRef(readerMode), "skipValue")); + } + return fieldSwitchRange( + builder, type, properties, 0, properties.length, readerMode, object, fieldIndex, record); + } + + private Expression fieldSwitchRange( + JsonGeneratedCodecBuilder builder, + Class type, + JsonFieldInfo[] properties, + int start, + int end, + int readerMode, + Expression object, + Expression fieldIndex, + boolean record) { + Expression.Switch.Case[] cases = new Expression.Switch.Case[end - start]; + for (int i = start; i < end; i++) { + cases[i - start] = new Expression.Switch.Case( i, new Expression.ListExpression( @@ -1233,12 +1338,10 @@ private static Expression readFieldNameHash(int readerMode, String namePrefix) { private static Expression fieldIndexInvoke(Expression fieldHash) { return new Expression.Invoke( - new Reference("this", TypeRef.of(Object.class)), - "fieldIndex", - "", + new Expression.Invoke(ownerRef(), "readTable", TypeRef.of(JsonFieldTable.class)), + "index", TypeRef.of(int.class), - false, - false, + true, fieldHash) .inline(); } 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 81cadae41d..ed77b69883 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 @@ -526,16 +526,70 @@ private BigDecimal readBigDecimalToken() { if (offset >= inputLength) { return readBigDecimalFallback(start); } - boolean negative = false; int ch = bytes[offset]; if (ch == '-') { - negative = true; + return readSignedBigDecimalToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + position = offset; + rejectLeadingDigitFast(); + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readBigDecimalFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { offset++; - if (offset >= inputLength) { + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readBigDecimalFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { return readBigDecimalFallback(start); } + } + if (offset < inputLength) { ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readBigDecimalFallback(start); + } } + position = offset; + return BigDecimal.valueOf(unscaled, scale); + } + + private BigDecimal readSignedBigDecimalToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readBigDecimalFallback(start); + } + int ch = bytes[offset]; long unscaled = 0; int scale = 0; if (ch == '0') { @@ -585,7 +639,7 @@ private BigDecimal readBigDecimalToken() { } } position = offset; - return BigDecimal.valueOf(negative ? -unscaled : unscaled, scale); + return BigDecimal.valueOf(-unscaled, scale); } private BigDecimal readBigDecimalFallback(int start) { @@ -647,19 +701,69 @@ private double readDoubleToken() { if (offset >= inputLength) { return readDoubleFallback(start); } - boolean negative = false; int ch = bytes[offset]; if (ch == '-') { - negative = true; + return readSignedDoubleToken(start); + } + long unscaled = 0; + int scale = 0; + if (ch == '0') { + offset++; + if (offset < inputLength) { + ch = bytes[offset]; + if (ch >= '0' && ch <= '9') { + return readDoubleFallback(start); + } + } + } else if (ch >= '1' && ch <= '9') { + do { + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + offset++; + if (offset >= inputLength) { + break; + } + ch = bytes[offset]; + } while (ch >= '0' && ch <= '9'); + } else { + return readDoubleFallback(start); + } + if (offset < inputLength && bytes[offset] == '.') { offset++; - if (offset >= inputLength) { + int fractionStart = offset; + while (offset < inputLength) { + ch = bytes[offset]; + if (ch < '0' || ch > '9') { + break; + } + int digit = ch - '0'; + if (unscaled > (Long.MAX_VALUE - digit) / 10) { + return readDoubleFallback(start); + } + unscaled = unscaled * 10 + digit; + scale++; + offset++; + } + if (offset == fractionStart) { return readDoubleFallback(start); } - ch = bytes[offset]; } + return finishDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double readSignedDoubleToken(int start) { + byte[] bytes = input; + int offset = start + 1; + int inputLength = bytes.length; + if (offset >= inputLength) { + return readDoubleFallback(start); + } + int ch = bytes[offset]; long unscaled = 0; int scale = 0; - int digits = 0; if (ch == '0') { offset++; if (offset < inputLength) { @@ -675,7 +779,6 @@ private double readDoubleToken() { return readDoubleFallback(start); } unscaled = unscaled * 10 + digit; - digits++; offset++; if (offset >= inputLength) { break; @@ -699,28 +802,40 @@ private double readDoubleToken() { } unscaled = unscaled * 10 + digit; scale++; - digits++; offset++; } if (offset == fractionStart) { return readDoubleFallback(start); } } + return finishSignedDoubleToken(bytes, offset, inputLength, start, unscaled, scale); + } + + private double finishDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { if (offset < inputLength) { - ch = bytes[offset]; + int ch = bytes[offset]; if (ch == 'e' || ch == 'E') { return readDoubleFallback(start); } } - if (digits > 18) { - return readDoubleFallback(start); + position = offset; + return BigDecimal.valueOf(unscaled, scale).doubleValue(); + } + + private double finishSignedDoubleToken( + byte[] bytes, int offset, int inputLength, int start, long unscaled, int scale) { + if (offset < inputLength) { + int ch = bytes[offset]; + if (ch == 'e' || ch == 'E') { + return readDoubleFallback(start); + } } position = offset; - if (negative && unscaled == 0) { + if (unscaled == 0) { return -0.0d; } - long value = negative ? -unscaled : unscaled; - return BigDecimal.valueOf(value, scale).doubleValue(); + return BigDecimal.valueOf(-unscaled, scale).doubleValue(); } private double readDoubleFallback(int start) { @@ -1023,9 +1138,13 @@ private OffsetDateTime readIsoOffsetDateTimeToken() { int day = parse2(bytes, start + 8); int hour = parse2(bytes, start + 11); int minute = parse2(bytes, start + 14); + return readIsoOffsetDateTimeTail(bytes, start + 16, length, year, month, day, hour, minute); + } + + private OffsetDateTime readIsoOffsetDateTimeTail( + byte[] bytes, int index, int length, int year, int month, int day, int hour, int minute) { int second = 0; int nano = 0; - int index = start + 16; if (index < length && bytes[index] == ':') { second = parse2(bytes, index + 1); index += 3; @@ -1049,6 +1168,21 @@ private OffsetDateTime readIsoOffsetDateTimeToken() { position = index + 2; return OffsetDateTime.of(year, month, day, hour, minute, second, nano, ZoneOffset.UTC); } + return readIsoOffsetDateTimeOffsetTail( + bytes, index, length, year, month, day, hour, minute, second, nano); + } + + private OffsetDateTime readIsoOffsetDateTimeOffsetTail( + byte[] bytes, + int index, + int length, + int year, + int month, + int day, + int hour, + int minute, + int second, + int nano) { long offsetAndEnd = parseOffsetAndEnd(bytes, index, length); position = (int) offsetAndEnd; return OffsetDateTime.of( From 7a002cdb885b3d85ea0257a2eb6cf73f996f179f Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 18:40:03 +0800 Subject: [PATCH 37/40] perf(java): parse UTF-8 long digit blocks --- .../fory/json/reader/Utf8JsonReader.java | 40 +++++++++++++++++++ .../org/apache/fory/json/JsonScalarTest.java | 25 ++++++++++++ 2 files changed, 65 insertions(+) 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 ed77b69883..dc0c10c631 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 @@ -46,6 +46,10 @@ public final class Utf8JsonReader extends JsonReader { private static final int LONG_MAX_MOD_10 = (int) (Long.MAX_VALUE % 10); private static final long LONG_MIN_DIV_10 = Long.MIN_VALUE / 10; private static final int LONG_MIN_LAST_DIGIT = (int) -(Long.MIN_VALUE % 10); + private static final long EIGHT_DIGITS = 100_000_000L; + private static final long ASCII_ZEROES = 0x3030_3030_3030_3030L; + private static final long ASCII_NINES = 0x3939_3939_3939_3939L; + private static final long ASCII_HIGH_BITS = 0x8080_8080_8080_8080L; // JSON syntax bytes are ASCII, so hot token checks can compare signed bytes directly. // UTF-8 string decoding must keep unsigned byte conversion for non-ASCII content. @@ -419,6 +423,16 @@ private long readLongToken() { if (safeEnd > inputLength) { safeEnd = inputLength; } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS + block; + offset += 8; + } + } while (offset < safeEnd) { ch = bytes[offset]; if (ch < '0' || ch > '9') { @@ -480,6 +494,16 @@ private long readNegativeLongToken(int start) { if (safeEnd > inputLength) { safeEnd = inputLength; } + int block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + block = parseEightDigits(bytes, offset, safeEnd); + if (block >= 0) { + result = result * EIGHT_DIGITS - block; + offset += 8; + } + } while (offset < safeEnd) { ch = bytes[offset]; if (ch < '0' || ch > '9') { @@ -518,6 +542,22 @@ private long readNegativeLongTail(byte[] bytes, int offset, int inputLength, lon return result; } + private static int parseEightDigits(byte[] bytes, int offset, int safeEnd) { + if (offset + 8 > safeEnd) { + return -1; + } + // Keep this as one unaligned little-endian load. Eight separate byte loads made the helper too + // large for C2 to place well under generated readers, while the byte-lane math stays compact. + long chunk = LittleEndian.getInt64(bytes, offset); + long digits = chunk - ASCII_ZEROES; + if (((digits | (ASCII_NINES - chunk)) & ASCII_HIGH_BITS) != 0) { + return -1; + } + long pairs = (digits * 10 + (digits >>> 8)) & 0x00FF_00FF_00FF_00FFL; + long quads = (pairs * 100 + (pairs >>> 16)) & 0x0000_FFFF_0000_FFFFL; + return (int) ((quads & 0xFFFF) * 10_000 + (quads >>> 32)); + } + private BigDecimal readBigDecimalToken() { byte[] bytes = input; int offset = position; diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java index 99f69009ad..bcae107bb4 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonScalarTest.java @@ -151,6 +151,31 @@ public void readUtf8DoubleTokens() { () -> new Utf8JsonReader("01.5".getBytes(StandardCharsets.UTF_8)).readDoubleTokenValue()); } + @Test + public void readUtf8LongBlocks() { + assertEquals( + new Utf8JsonReader("123456789012345678".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + 123456789012345678L); + assertEquals( + new Utf8JsonReader("-123456789012345678".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + -123456789012345678L); + assertEquals( + new Utf8JsonReader("9223372036854775807".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + Long.MAX_VALUE); + assertEquals( + new Utf8JsonReader("-9223372036854775808".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue(), + Long.MIN_VALUE); + assertThrows( + ForyJsonException.class, + () -> + new Utf8JsonReader("9223372036854775808".getBytes(StandardCharsets.UTF_8)) + .readLongTokenValue()); + } + @Test public void writeNumericBoundaries() { ForyJson json = ForyJson.builder().build(); From 38261a74a78eedb796581e8f2e9be8abc07662b6 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 19:27:08 +0800 Subject: [PATCH 38/40] perf(java): compact Fory JSON UTF-8 string scan --- .../fory/json/reader/Utf8JsonReader.java | 22 +++++++------------ .../org/apache/fory/json/JsonStringTest.java | 2 +- 2 files changed, 9 insertions(+), 15 deletions(-) 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 dc0c10c631..b9328ee355 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 @@ -1337,29 +1337,23 @@ private StringBuilder newStringBuilder(int start, int stop) { } private static long stringStopMask(long word) { + // UTF-8 mode stops on every high-bit byte, so the quote/backslash equality mask can + // omit the usual `~match` term. Latin1JsonReader cannot use this shortcut because + // high-bit Latin-1 bytes are valid string payload. + long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); return (word & BYTE_HIGH_BITS) - | byteMatchMask(word, QUOTE_BYTES) - | byteMatchMask(word, BACKSLASH_BYTES) + | (syntaxStop & BYTE_HIGH_BITS) | ((word - CONTROL_LIMIT_BYTES) & ~word & BYTE_HIGH_BITS); } private static int stringStopMask(int word) { + int syntaxStop = + ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES); return (word & INT_BYTE_HIGH_BITS) - | byteMatchMask(word, INT_QUOTE_BYTES) - | byteMatchMask(word, INT_BACKSLASH_BYTES) + | (syntaxStop & INT_BYTE_HIGH_BITS) | ((word - INT_CONTROL_LIMIT_BYTES) & ~word & INT_BYTE_HIGH_BITS); } - private static long byteMatchMask(long word, long repeatedByte) { - long match = word ^ repeatedByte; - return (match - BYTE_ONES) & ~match & BYTE_HIGH_BITS; - } - - private static int byteMatchMask(int word, int repeatedByte) { - int match = word ^ repeatedByte; - return (match - INT_BYTE_ONES) & ~match & INT_BYTE_HIGH_BITS; - } - @Override public JsonFieldInfo readField(JsonFieldTable table) { return table.get(readFieldNameHash()); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java index c6beef60dc..d8e6143c3a 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonStringTest.java @@ -227,7 +227,7 @@ public void writeSurrogatePair() { @Test public void readStringScanBoundaries() { ForyJson json = ForyJson.builder().build(); - for (int length : new int[] {7, 8, 15, 16, 23, 24}) { + for (int length : new int[] {7, 8, 15, 16, 23, 24, 31, 32, 63, 64, 65}) { String value = repeat('a', length); String input = "\"" + value + "\""; assertEquals(json.fromJson(input, String.class), value); From 0294768953f6f79757267ed21be280cd0a01b0ac Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 19:37:07 +0800 Subject: [PATCH 39/40] perf(java): fold Fory JSON UTF-8 string mask --- .../apache/fory/json/reader/Utf8JsonReader.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 b9328ee355..5df2e24528 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 @@ -1337,21 +1337,18 @@ private StringBuilder newStringBuilder(int start, int stop) { } private static long stringStopMask(long word) { - // UTF-8 mode stops on every high-bit byte, so the quote/backslash equality mask can - // omit the usual `~match` term. Latin1JsonReader cannot use this shortcut because - // high-bit Latin-1 bytes are valid string payload. + // UTF-8 mode stops on every high-bit byte, and readStringToken only uses the first stop bit. + // Subtraction borrow may only create later high bits after an earlier real stop, so the + // compact syntax/range expression preserves the first-stop position. Latin1JsonReader cannot + // use this shortcut because high-bit Latin-1 bytes are valid string payload. long syntaxStop = ((word ^ QUOTE_BYTES) - BYTE_ONES) | ((word ^ BACKSLASH_BYTES) - BYTE_ONES); - return (word & BYTE_HIGH_BITS) - | (syntaxStop & BYTE_HIGH_BITS) - | ((word - CONTROL_LIMIT_BYTES) & ~word & BYTE_HIGH_BITS); + return (syntaxStop | word | (word - CONTROL_LIMIT_BYTES)) & BYTE_HIGH_BITS; } private static int stringStopMask(int word) { int syntaxStop = ((word ^ INT_QUOTE_BYTES) - INT_BYTE_ONES) | ((word ^ INT_BACKSLASH_BYTES) - INT_BYTE_ONES); - return (word & INT_BYTE_HIGH_BITS) - | (syntaxStop & INT_BYTE_HIGH_BITS) - | ((word - INT_CONTROL_LIMIT_BYTES) & ~word & INT_BYTE_HIGH_BITS); + return (syntaxStop | word | (word - INT_CONTROL_LIMIT_BYTES)) & INT_BYTE_HIGH_BITS; } @Override From 081b678f6fa632b8cd158a4301edcff2b71ae090 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Thu, 2 Jul 2026 22:32:13 +0800 Subject: [PATCH 40/40] fix(java): restore json ci --- .../apache/fory/json/meta/JsonFieldInfo.java | 16 +++++++------- .../fory/json/writer/Utf8JsonWriter.java | 6 +++--- .../fory/json/JsonGeneratedCodecTest.java | 21 +++++++++++-------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java index b1a4a148f8..d2e2afe3d1 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/meta/JsonFieldInfo.java @@ -113,10 +113,10 @@ public JsonFieldInfo( this.writeGetter = writeGetter; this.readField = readField; this.readSetter = readSetter; - this.writeType = writeType(writeField, writeGetter); - this.writeRawType = writeRawType(writeField, writeGetter); - this.readType = readType(readField, readSetter); - this.readRawType = readRawType(readField, readSetter); + this.writeType = resolveWriteType(writeField, writeGetter); + this.writeRawType = resolveWriteRawType(writeField, writeGetter); + this.readType = resolveReadType(readField, readSetter); + this.readRawType = resolveReadRawType(readField, readSetter); this.writeAccessor = writeAccessor; this.readAccessor = readAccessor; writeKind = writeRawType == null ? null : kind(writeRawType); @@ -266,19 +266,19 @@ private static Class fieldRawType(Field field) { return field == null ? null : field.getType(); } - private static Type writeType(Field field, Method getter) { + private static Type resolveWriteType(Field field, Method getter) { return getter == null ? fieldType(field) : getter.getGenericReturnType(); } - private static Class writeRawType(Field field, Method getter) { + private static Class resolveWriteRawType(Field field, Method getter) { return getter == null ? fieldRawType(field) : getter.getReturnType(); } - private static Type readType(Field field, Method setter) { + private static Type resolveReadType(Field field, Method setter) { return setter == null ? fieldType(field) : setter.getGenericParameterTypes()[0]; } - private static Class readRawType(Field field, Method setter) { + private static Class resolveReadRawType(Field field, Method setter) { return setter == null ? fieldRawType(field) : setter.getParameterTypes()[0]; } diff --git a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java index eb67d1e70a..218016b163 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/writer/Utf8JsonWriter.java @@ -221,7 +221,7 @@ public void writeLocalDate(LocalDate value) { byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; - pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + pos = writeDateParts(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) '"'; position = pos; } @@ -236,7 +236,7 @@ public void writeOffsetDateTime(OffsetDateTime value) { byte[] bytes = buffer; int pos = position; bytes[pos++] = (byte) '"'; - pos = writeLocalDate(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); + pos = writeDateParts(bytes, pos, year, value.getMonthValue(), value.getDayOfMonth()); bytes[pos++] = (byte) 'T'; pos = writeTime( @@ -1085,7 +1085,7 @@ private static int writeHex(byte[] bytes, int pos, long value, int shift, int co return pos; } - private static int writeLocalDate(byte[] bytes, int pos, int year, int month, int day) { + private static int writeDateParts(byte[] bytes, int pos, int year, int month, int day) { pos = writePadded4(bytes, pos, year); bytes[pos++] = (byte) '-'; pos = writeTwoDigits(bytes, pos, month); diff --git a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java index 45c15e9c97..bceb48e096 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/JsonGeneratedCodecTest.java @@ -34,6 +34,7 @@ import org.apache.fory.json.meta.JsonFieldNameHash; import org.apache.fory.json.reader.Latin1JsonReader; import org.apache.fory.json.reader.Utf8JsonReader; +import org.apache.fory.serializer.StringSerializer; import org.testng.annotations.Test; public class JsonGeneratedCodecTest extends ForyJsonTestModels { @@ -137,10 +138,6 @@ public void readLongAsciiFieldToken() { assertTrue(utf8.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); assertEquals(utf8.readNullableStringToken(), "apple"); - Latin1JsonReader latin1 = new Latin1JsonReader(token + "\"pear\""); - assertTrue(latin1.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); - assertEquals(latin1.readNullableStringToken(), "pear"); - String tailToken = "\"registered\":"; long tailPrefix = JsonAsciiToken.prefix(tailToken); long tailSuffix = JsonAsciiToken.suffixLong(tailToken); @@ -151,11 +148,17 @@ public void readLongAsciiFieldToken() { tailUtf8.tryReadNextFieldNameToken8( tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); assertEquals(tailUtf8.readIntTokenValue(), 1); - Latin1JsonReader tailLatin1 = new Latin1JsonReader(tailToken + "2"); - assertTrue( - tailLatin1.tryReadNextFieldNameToken8( - tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); - assertEquals(tailLatin1.readIntTokenValue(), 2); + if (StringSerializer.isBytesBackedString()) { + Latin1JsonReader latin1 = new Latin1JsonReader(token + "\"pear\""); + assertTrue(latin1.tryReadNextFieldNameToken8(prefix, suffix, suffixMask, token.length())); + assertEquals(latin1.readNullableStringToken(), "pear"); + + Latin1JsonReader tailLatin1 = new Latin1JsonReader(tailToken + "2"); + assertTrue( + tailLatin1.tryReadNextFieldNameToken8( + tailPrefix, tailSuffix, tailSuffixMask, tailToken.length())); + assertEquals(tailLatin1.readIntTokenValue(), 2); + } Utf8JsonReader mismatch = new Utf8JsonReader("\"favoriteSeed\":\"pit\"".getBytes(StandardCharsets.UTF_8));