From 840000b513ddb5b64bb7366ca85e3bdda2efeea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 12:09:15 +0800 Subject: [PATCH 01/20] perf(java): improve compatible mode performance --- .../fory/builder/BaseObjectCodecBuilder.java | 179 +++++++++++++++--- .../builder/LayerMarkerClassGenerator.java | 2 +- .../fory/builder/ObjectCodecBuilder.java | 5 +- .../apache/fory/context/MetaWriteContext.java | 92 ++++++++- .../org/apache/fory/context/WriteContext.java | 2 +- .../org/apache/fory/memory/MemoryBuffer.java | 4 +- .../apache/fory/resolver/TypeResolver.java | 103 +++++++++- .../CompatibleLayerSerializerBase.java | 6 +- .../fory/serializer/EnumSerializer.java | 16 +- .../serializer/UnknownClassSerializers.java | 6 +- .../collection/ChildContainerSerializers.java | 4 +- .../collection/CollectionLikeSerializer.java | 130 +++++++++++++ .../org/apache/fory/memory/MemoryBuffer.java | 4 +- 13 files changed, 491 insertions(+), 62 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index c431f7ec7c..e76176dc33 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -701,6 +701,18 @@ private Expression serializeForNotNullObjectForField( Expression inputObject, Expression buffer, Descriptor descriptor, Expression serializer) { TypeRef typeRef = descriptor.getTypeRef(); Class clz = getRawType(typeRef); + if (isEnumType(clz)) { + Expression enumSerializer = + cast( + serializer == null ? getSerializerForField(clz) : serializer, + TypeRef.of(EnumSerializer.class)); + return new Invoke( + enumSerializer, + "writeValue", + writeContextRef(), + buffer, + cast(inputObject, TypeRef.of(Enum.class))); + } if (serializer != null) { return new Invoke(serializer, writeMethodName, writeContextRef(), inputObject); } @@ -717,6 +729,10 @@ private Expression getSerializerForField(Class cls) { return getOrCreateSerializer(cls, true); } + private static boolean isEnumType(Class cls) { + return cls != Enum.class && Enum.class.isAssignableFrom(cls); + } + protected Expression serializeForNullable( Expression inputObject, Expression buffer, TypeRef typeRef, boolean nullable) { return serializeForNullable(inputObject, buffer, typeRef, null, false, nullable); @@ -885,6 +901,7 @@ protected Expression writeForNotNullNonFinalObject( Class clz = getRawType(typeRef); Expression clsExpr = new Invoke(inputObject, "getClass", "cls", CLASS_TYPE); ListExpression writeClassAndObject = new ListExpression(); + Expression exactClassWrite = exactClassWrite(inputObject, clz); Tuple2 classInfoRef = addTypeInfoField(clz); Expression classInfo = classInfoRef.f0; if (classInfoRef.f1) { @@ -904,12 +921,24 @@ protected Expression writeForNotNullNonFinalObject( PRIMITIVE_VOID_TYPE, writeContextRef(), inputObject)); + Expression write = + exactClassWrite == null + ? writeClassAndObject + : new If(eq(clsExpr, getClassExpr(clz)), exactClassWrite, writeClassAndObject, false); return invokeGenerated( - ctx, - writeCutPoints(buffer, inputObject), - writeClassAndObject, - "writeClassAndObject", - false); + ctx, writeCutPoints(buffer, inputObject), write, "writeClassAndObject", false); + } + + private Expression exactClassWrite(Expression inputObject, Class clz) { + if (clz.isInterface() || Modifier.isAbstract(clz.getModifiers())) { + return null; + } + Reference typeInfo = addExactTypeInfoField(clz); + Expression serializer = getOrCreateSerializer(clz); + return new ListExpression( + typeResolver(r -> r.writeClassExpr(typeResolverRef, writeContextRef(), typeInfo)), + new Invoke( + serializer, writeMethodName, PRIMITIVE_VOID_TYPE, writeContextRef(), inputObject)); } protected Expression writeTypeInfo( @@ -1128,6 +1157,21 @@ protected Tuple2 addTypeInfoField(Class cls) { return classInfoRef; } + protected Reference addExactTypeInfoField(Class cls) { + String key = "exactClassInfo:" + cls; + Reference reference = (Reference) sharedFieldMap.get(key); + if (reference != null) { + return reference; + } + Expression classInfoExpr = + inlineInvoke(typeResolverRef, "getTypeInfo", classInfoTypeRef, getClassExpr(cls)); + String name = ctx.newName(ctx.newName(cls) + "ExactTypeInfo"); + ctx.addField(true, ctx.type(TypeInfo.class), name, classInfoExpr); + reference = fieldRef(name, classInfoTypeRef); + sharedFieldMap.put(key, reference); + return reference; + } + protected Reference addTypeInfoHolderField(Class cls) { // Final type need to write classinfo when meta share enabled. String key; @@ -1325,7 +1369,7 @@ protected Expression writeCollectionData( Class elemClass = TypeUtils.getRawType(elementType); boolean trackingRef = needWriteRef(elementType); Tuple2 writeElementsHeader = - writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection); + writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection, size); Expression flags = writeElementsHeader.f0; builder.add(flags); boolean finalType = isMonomorphic(elemClass); @@ -1334,11 +1378,18 @@ protected Expression writeCollectionData( builder.add( writeContainerElements(elementType, true, null, null, buffer, collection, size)); } else { + Expression declSameNoNull = + eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL), "declSameNoNull"); Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL); Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag, "hasNull"); builder.add( + declSameNoNull, hasNull, - writeContainerElements(elementType, false, null, hasNull, buffer, collection, size)); + new If( + declSameNoNull, + writeContainerElements(elementType, false, null, null, buffer, collection, size), + writeContainerElements(elementType, false, null, hasNull, buffer, collection, size), + false)); } } else { Literal flag = ofInt(CollectionFlags.IS_SAME_TYPE); @@ -1398,9 +1449,28 @@ protected Expression writeCollectionData( differentTypeWrite); } else { // if declared elem type don't track ref, all elements must not write ref. + Expression declSameNoNull = + eq(flags, ofInt(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL), "declSameNoNull"); Literal hasNullFlag = ofInt(CollectionFlags.HAS_NULL); Expression hasNull = eq(new BitAnd(flags, hasNullFlag), hasNullFlag, "hasNull"); - builder.add(hasNull); + builder.add(declSameNoNull, hasNull); + Expression declaredNoNullWrite = null; + if (maybeDecl) { + declaredNoNullWrite = + invokeGenerated( + ctx, + writeCutPoints(buffer, collection, size), + writeContainerElements( + elementType, + false, + cast(getOrCreateSerializer(elemClass), serializerType), + null, + buffer, + collection, + size), + "declSameNoNullWrite", + false); + } ListExpression writeBuilder = new ListExpression(elemSerializer); writeBuilder.add( writeContainerElements( @@ -1415,6 +1485,9 @@ protected Expression writeCollectionData( invokeGenerated(ctx, cutPoint, writeBuilder, "sameElementClassWrite", false), writeContainerElements( elementType, false, null, hasNull, buffer, collection, size)); + if (declaredNoNullWrite != null) { + action = new If(declSameNoNull, declaredNoNullWrite, action, false); + } } builder.add(action); } @@ -1436,7 +1509,9 @@ private Tuple2 writeElementsHeader( boolean trackingRef, Expression collectionSerializer, Expression buffer, - Expression value) { + Expression value, + Expression size) { + boolean isList = List.class.isAssignableFrom(getRawType(value.type())); if (isMonomorphic(elementType)) { Expression bitmap; if (trackingRef) { @@ -1445,9 +1520,24 @@ private Tuple2 writeElementsHeader( new Invoke(buffer, "writeByte", ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)), ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)); } else { - bitmap = - new Invoke( - collectionSerializer, "writeNullabilityHeader", PRIMITIVE_INT_TYPE, buffer, value); + if (isList) { + bitmap = + new Invoke( + collectionSerializer, + "writeNullabilityHeader", + PRIMITIVE_INT_TYPE, + buffer, + value, + size); + } else { + bitmap = + new Invoke( + collectionSerializer, + "writeNullabilityHeader", + PRIMITIVE_INT_TYPE, + buffer, + value); + } } return Tuple2.of(bitmap, null); } else { @@ -1476,15 +1566,28 @@ private Tuple2 writeElementsHeader( classInfoHolder); } } else { - bitmap = - new Invoke( - collectionSerializer, - "writeTypeNullabilityHeader", - PRIMITIVE_INT_TYPE, - writeContextRef(), - value, - elementTypeExpr, - classInfoHolder); + if (isList) { + bitmap = + new Invoke( + collectionSerializer, + "writeTypeNullabilityHeader", + PRIMITIVE_INT_TYPE, + writeContextRef(), + value, + size, + elementTypeExpr, + classInfoHolder); + } else { + bitmap = + new Invoke( + collectionSerializer, + "writeTypeNullabilityHeader", + PRIMITIVE_INT_TYPE, + writeContextRef(), + value, + elementTypeExpr, + classInfoHolder); + } } Invoke serializer = new Invoke(classInfoHolder, "getSerializer", SERIALIZER_TYPE); return Tuple2.of(bitmap, serializer); @@ -2344,6 +2447,14 @@ private Expression deserializeForNotNullForField( return StringSerializer.readStringExpr( getOrCreateStringSerializer(), buffer, config.compressString()); } + if (isEnumType(cls)) { + Expression enumSerializer = + cast( + serializer == null ? getSerializerForField(cls) : serializer, + TypeRef.of(EnumSerializer.class)); + return new Invoke( + enumSerializer, "readValue", TypeRef.of(Enum.class), readContextRef(), buffer); + } Expression obj; if (usesPrimitiveListArrayProtocol(descriptor)) { serializer = getPrimitiveListArraySerializer(cls); @@ -2588,14 +2699,20 @@ protected Expression readForNotNullNonFinal( || typeInfo.getTypeId() == Types.NAMED_COMPATIBLE_STRUCT)) { String name = ctx.newName(StringUtils.uncapitalize(rawType.getSimpleName()) + "Class"); Expression clsExpr = staticClassFieldExpr(rawType, name); + Reference classInfoHolderRef = addTypeInfoHolderField(rawType); classInfo = inlineInvoke( - typeResolverRef, "readTypeInfo", classInfoTypeRef, readContextRef, clsExpr); + typeResolverRef, + "readTypeInfo", + classInfoTypeRef, + readContextRef, + clsExpr, + classInfoHolderRef); } else { classInfo = readTypeInfo(getRawType(typeRef), buffer); } } else { - classInfo = readTypeInfo(getRawType(typeRef), buffer); + classInfo = readTypeInfo(rawType, buffer); } serializer = inlineInvoke(classInfo, "getSerializer", SERIALIZER_TYPE); } @@ -2638,16 +2755,18 @@ protected Expression deserializeForCollection( Expression collection = new Invoke(serializer, "newCollection", COLLECTION_TYPE, readContextRef); Expression size = new Invoke(serializer, "getAndClearNumElements", "size", PRIMITIVE_INT_TYPE); - // if add branch by `ArrayList`, generated code will be > 325 bytes. - // and List#add is more likely be inlined if there is only one subclass. Expression hookRead = readCollectionCodegen(buffer, collection, size, elementType); hookRead = new Invoke(serializer, "onCollectionRead", OBJECT_TYPE, hookRead); - Expression action = - new If( - supportHook, - new ListExpression(collection, hookRead), - read(serializer, buffer, OBJECT_TYPE), + Expression fallbackAction = read(serializer, buffer, OBJECT_TYPE); + Expression fallbackRead = + invokeGenerated( + ctx, + readCutPoints(buffer, serializer), + new ListExpression(fallbackAction, new Return(fallbackAction)), + "readCollectionFallback", false); + Expression action = + new If(supportHook, new ListExpression(collection, hookRead), fallbackRead, false); if (invokeHint != null && invokeHint.genNewMethod) { invokeHint.add(buffer); invokeHint.add(readContextRef()); diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java index 3e761b55a2..c9e2e8c78d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java @@ -26,7 +26,7 @@ /** * Creates unique marker classes for each layer in a class hierarchy. These marker classes serve as - * unique keys in {@code metaContext.classMap} to distinguish different layers during serialization. + * unique keys in {@code MetaWriteContext} to distinguish different layers during serialization. * *

For a class hierarchy {@code C extends B extends A}, this generator creates unique marker * classes for: diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java index e22cd2b38c..b467361ac0 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecBuilder.java @@ -799,9 +799,10 @@ public Expression buildDecodeExpression() { Expression bean; if (!isRecord) { bean = newBean(); - Expression referenceObject = invokeReadContext("reference", bean); expressions.add(bean); - expressions.add(referenceObject); + if (typeResolver.trackingRef()) { + expressions.add(invokeReadContext("reference", bean)); + } } else { if (recordCtrAccessible) { bean = new FieldsCollector(); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java index a39d2ef0b2..84d164c2e0 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java @@ -19,8 +19,6 @@ package org.apache.fory.context; -import org.apache.fory.collection.IdentityObjectIntMap; - /** * Write-side state for meta-share serialization. * @@ -28,9 +26,93 @@ * already announced classes are not sent repeatedly. */ public class MetaWriteContext { + private static final int INITIAL_CAPACITY = 8; + private static final int MISSING_ID = -1; + + private Object[] keys = new Object[INITIAL_CAPACITY]; + private int[] idsBySlot = new int[INITIAL_CAPACITY]; + private int[] usedSlots = new int[INITIAL_CAPACITY]; + private int mask = INITIAL_CAPACITY - 1; + private int threshold = INITIAL_CAPACITY >> 1; + private int size; + + /** Returns the next protocol id that will be assigned to a new metadata key. */ + public int size() { + return size; + } + /** - * Classes whose definitions have already been announced to the peer, mapped to the protocol id - * used by the current or shared meta-share session. + * Returns the existing protocol id for {@code key}, or records it and returns {@code -1}. + * + *

Meta-share keys use identity semantics. Class objects and layer marker classes are already + * identity keys, and unknown-struct TypeDef ids historically used the same identity map path. */ - public final IdentityObjectIntMap> classMap = new IdentityObjectIntMap<>(1, 0.5f); + public int putOrGetMetaId(Object key) { + if (key == null) { + throw new NullPointerException("Meta key must not be null"); + } + int slot = locate(key, keys, mask); + if (slot >= 0) { + return idsBySlot[slot]; + } + if (size >= threshold) { + resize(keys.length << 1); + slot = locate(key, keys, mask); + } + slot = -(slot + 1); + int id = size; + keys[slot] = key; + idsBySlot[slot] = id; + usedSlots[id] = slot; + size = id + 1; + return MISSING_ID; + } + + /** Clears operation-local metadata ids while retaining reusable table storage. */ + public void reset() { + int size = this.size; + if (size == 0) { + return; + } + Object[] keys = this.keys; + int[] usedSlots = this.usedSlots; + for (int i = 0; i < size; i++) { + keys[usedSlots[i]] = null; + } + this.size = 0; + } + + private static int locate(Object key, Object[] keys, int mask) { + for (int slot = System.identityHashCode(key) & mask; ; slot = (slot + 1) & mask) { + Object other = keys[slot]; + if (other == null) { + return -(slot + 1); + } + if (other == key) { + return slot; + } + } + } + + private void resize(int newCapacity) { + Object[] oldKeys = keys; + int[] oldUsedSlots = usedSlots; + int oldSize = size; + Object[] newKeys = new Object[newCapacity]; + int[] newIdsBySlot = new int[newCapacity]; + int[] newUsedSlots = new int[newCapacity]; + int newMask = newCapacity - 1; + for (int id = 0; id < oldSize; id++) { + Object key = oldKeys[oldUsedSlots[id]]; + int slot = -(locate(key, newKeys, newMask) + 1); + newKeys[slot] = key; + newIdsBySlot[slot] = id; + newUsedSlots[id] = slot; + } + keys = newKeys; + idsBySlot = newIdsBySlot; + usedSlots = newUsedSlots; + mask = newMask; + threshold = newCapacity >> 1; + } } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index ebab5330b0..b2be9b7eb5 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -295,7 +295,7 @@ public void reset() { contextObjects.clear(); } if (scopedMetaShareEnabled) { - metaWriteContext.classMap.clear(); + metaWriteContext.reset(); } else { metaWriteContext = null; } diff --git a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java index 94dd961889..c0f002ca98 100644 --- a/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java/org/apache/fory/memory/MemoryBuffer.java @@ -962,7 +962,9 @@ public void writeByte(byte value) { } else { final int writerIdx = writerIndex; final int newIdx = writerIdx + 1; - ensure(newIdx); + if (newIdx > size) { + globalAllocator.grow(this, newIdx); + } final long pos = address + writerIdx; UNSAFE.putByte(heapMemory, pos, value); writerIndex = newIdx; diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index b104985527..a480f45325 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -55,7 +55,6 @@ import org.apache.fory.collection.BiMap; import org.apache.fory.collection.ConcurrentIdentityMap; import org.apache.fory.collection.IdentityMap; -import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.Tuple2; import org.apache.fory.config.Config; @@ -587,9 +586,8 @@ protected final void writeSharedClassMeta(WriteContext writeContext, TypeInfo ty MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG; - IdentityObjectIntMap> classMap = metaWriteContext.classMap; - int newId = classMap.size; - int id = classMap.putOrGet(typeInfo.type, newId); + int newId = metaWriteContext.size(); + int id = metaWriteContext.putOrGetMetaId(typeInfo.type); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); @@ -626,7 +624,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext) { case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -671,7 +669,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -703,6 +701,60 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass return typeInfo; } + /** + * Read class info from buffer using a target class and generated field-local cache. + * + *

The holder caches only checked metadata owned by this resolver. It never caches payload + * data. + */ + @CodegenInvoke + public final TypeInfo readTypeInfo( + ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { + MemoryBuffer buffer = readContext.getBuffer(); + int typeId = buffer.readUInt8(); + TypeInfo typeInfo; + boolean updateCache = false; + switch (typeId) { + case Types.ENUM: + case Types.STRUCT: + case Types.EXT: + case Types.TYPED_UNION: + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; + break; + case Types.COMPATIBLE_STRUCT: + case Types.NAMED_COMPATIBLE_STRUCT: + typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); + break; + case Types.NAMED_ENUM: + case Types.NAMED_STRUCT: + case Types.NAMED_EXT: + case Types.NAMED_UNION: + if (!metaContextShareEnabled) { + typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); + updateCache = true; + } else { + typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); + } + break; + case Types.LIST: + typeInfo = readListTypeInfo(readContext); + break; + case Types.TIMESTAMP: + typeInfo = readTimestampTypeInfo(readContext); + break; + default: + typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); + } + if (typeInfo.serializer == null) { + typeInfo = ensureSerializerForTypeInfo(typeInfo); + } + if (updateCache) { + classInfoHolder.typeInfo = typeInfo; + } + return typeInfo; + } + /** * Read class info from buffer with TypeInfo cache. This version is faster than {@link * #readTypeInfo(ReadContext)} because it uses the provided classInfoCache to reduce map lookups @@ -721,7 +773,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfo typeInfoCac case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -770,7 +822,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(buffer.readVarUInt32())); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: @@ -805,6 +858,14 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class return typeInfo; } + private TypeInfo readRegisteredTypeInfo(int typeId, int userTypeId, TypeInfo cachedTypeInfo) { + TypeInfo typeInfo = cachedTypeInfo; + if (typeInfo == null || typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) { + typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(userTypeId)); + } + return typeInfo; + } + /** * Read class info using the provided cache. Returns cached TypeInfo if the namespace and type * name bytes match. @@ -850,6 +911,18 @@ protected final TypeInfo readTypeInfoFromBytes( public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targetClass) { TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass); + return adaptSharedClassTarget(typeInfo, targetClass); + } + + private TypeInfo readSharedClassMeta( + ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { + TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass, classInfoHolder.typeInfo); + typeInfo = adaptSharedClassTarget(typeInfo, targetClass); + classInfoHolder.typeInfo = typeInfo; + return typeInfo; + } + + private TypeInfo adaptSharedClassTarget(TypeInfo typeInfo, Class targetClass) { Class readClass = typeInfo.getType(); if (targetClass != readClass) { return getTargetTypeInfo(typeInfo, targetClass); @@ -858,6 +931,11 @@ public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targ } private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class targetClass) { + return readSharedClassTypeInfo(readContext, targetClass, null); + } + + private TypeInfo readSharedClassTypeInfo( + ReadContext readContext, Class targetClass, TypeInfo cachedTypeInfo) { MemoryBuffer buffer = readContext.getBuffer(); MetaReadContext metaReadContext = readContext.getMetaReadContext(); assert metaReadContext != null : SET_META_READ_CONTEXT_MSG; @@ -875,7 +953,14 @@ private TypeInfo readSharedClassTypeInfo(ReadContext readContext, Class targe // body/hash/schema-limit/exact-local checks here; the header-miss path owns them before // cache publish. long id = buffer.readInt64(); - typeInfo = extRegistry.typeInfoByTypeDefId.get(id); + TypeDef cachedTypeDef = cachedTypeInfo == null ? null : cachedTypeInfo.getTypeDef(); + // A field-local cache hit is valid only when the cached TypeInfo carries the exact checked + // TypeDef id that was parsed and accepted earlier by this resolver. + if (cachedTypeDef != null && cachedTypeDef.getId() == id) { + typeInfo = cachedTypeInfo; + } else { + typeInfo = extRegistry.typeInfoByTypeDefId.get(id); + } if (typeInfo != null) { TypeDef.skipTypeDef(buffer, id); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java index e7040f16f4..e7ce30603e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java @@ -19,7 +19,6 @@ package org.apache.fory.serializer; -import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.ObjectIntMap; import org.apache.fory.context.CopyContext; import org.apache.fory.context.MetaWriteContext; @@ -87,9 +86,8 @@ public void writeLayerClassMeta(WriteContext writeContext) { if (metaWriteContext == null) { return; } - IdentityObjectIntMap> classMap = metaWriteContext.classMap; - int newId = classMap.size; - int id = classMap.putOrGet(layerMarkerClass, newId); + int newId = metaWriteContext.size(); + int id = metaWriteContext.putOrGetMetaId(layerMarkerClass); if (id >= 0) { buffer.writeVarUInt32((id << 1) | 1); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java index f07098d6c1..7e7e1d27bc 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/EnumSerializer.java @@ -26,11 +26,13 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.annotation.ForyEnumId; import org.apache.fory.collection.LongMap; import org.apache.fory.config.Config; import org.apache.fory.context.ReadContext; import org.apache.fory.context.WriteContext; +import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.util.Preconditions; @SuppressWarnings("rawtypes") @@ -65,15 +67,25 @@ public EnumSerializer(Config config, Class cls) { @Override public void write(WriteContext writeContext, Enum value) { + writeValue(writeContext, writeContext.getBuffer(), value); + } + + @CodegenInvoke + public final void writeValue(WriteContext writeContext, MemoryBuffer buffer, Enum value) { if (!config.isXlang() && config.serializeEnumByName()) { writeContext.writeString(value.name()); } else { - writeContext.getBuffer().writeVarUInt32Small7(tagByOrdinal[value.ordinal()]); + buffer.writeVarUInt32Small7(tagByOrdinal[value.ordinal()]); } } @Override public Enum read(ReadContext readContext) { + return readValue(readContext, readContext.getBuffer()); + } + + @CodegenInvoke + public final Enum readValue(ReadContext readContext, MemoryBuffer buffer) { if (!config.isXlang() && config.serializeEnumByName()) { String name = readContext.readString(); Enum e = stringToEnum.get(name); @@ -82,7 +94,7 @@ public Enum read(ReadContext readContext) { } return handleUnknownEnumValue(name); } else { - int tag = readContext.getBuffer().readVarUInt32Small7(); + int tag = buffer.readVarUInt32Small7(); Enum value = null; if (enumConstantByTagArray != null && tag < enumConstantByTagArray.length) { value = enumConstantByTagArray[tag]; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java index ef03486e11..5fdae10109 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.List; -import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.MapEntry; import org.apache.fory.config.Config; @@ -92,10 +91,9 @@ public UnknownStructSerializer(TypeResolver typeResolver, TypeDef typeDef) { private void writeTypeDef(WriteContext writeContext, UnknownClass.UnknownStruct value) { MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); - IdentityObjectIntMap classMap = metaWriteContext.classMap; - int newId = classMap.size; + int newId = metaWriteContext.size(); // class not exist, use class def id for identity. - int id = classMap.putOrGet(value.typeDef.getId(), newId); + int id = metaWriteContext.putOrGetMetaId(value.typeDef.getId()); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java index f7840349ef..acb1700311 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java @@ -710,8 +710,8 @@ private static void readAndSkipLayerClassMeta(ReadContext readContext) { // New type - need to read and skip the TypeDef bytes long id = buffer.readInt64(); TypeDef.skipTypeDef(buffer, id); - // Add a placeholder to keep readTypeInfos indices in sync with the write side's classMap. - // The write side (writeLayerClassMeta) adds layer marker classes to classMap which shares + // Add a placeholder to keep readTypeInfos indices in sync with the write side's meta ids. + // The write side (writeLayerClassMeta) adds layer marker classes to the metadata table, sharing // the same index space as writeSharedClassMeta. Without this placeholder, subsequent // readSharedClassMeta reference lookups would use wrong indices. metaReadContext.readTypeInfos.add(null); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java index 3915b5d888..221b104508 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java @@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.util.Collection; +import java.util.List; import org.apache.fory.Fory; import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.config.Config; @@ -178,6 +179,44 @@ public int writeNullabilityHeader(MemoryBuffer buffer, Collection value) { return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; } + /** Element type is final, write whether any elements is null. */ + @CodegenInvoke + public int writeNullabilityHeader(MemoryBuffer buffer, List value, int size) { + switch (size) { + case 1: + if (value.get(0) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + case 2: + if (value.get(0) == null || value.get(1) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + case 3: + if (value.get(0) == null || value.get(1) == null || value.get(2) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + default: + break; + } + for (int i = 0; i < size; i++) { + if (value.get(i) == null) { + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; + } + } + buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); + return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; + } + /** * Need to track elements ref, declared element type is not morphic, can't check elements * nullability. @@ -320,6 +359,97 @@ public int writeTypeNullabilityHeader( return bitmap; } + /** + * Element type is not final by {@link ClassResolver#isMonomorphic}, need to write element type. + * Elements ref tracking is disabled, write whether any elements is null. + */ + @CodegenInvoke + public int writeTypeNullabilityHeader( + WriteContext writeContext, + List value, + int size, + Class declareElementType, + TypeInfoHolder cache) { + MemoryBuffer buffer = writeContext.getBuffer(); + if (size > 0 && size <= 3) { + Object elem0 = value.get(0); + Object elem1 = size > 1 ? value.get(1) : null; + Object elem2 = size > 2 ? value.get(2) : null; + boolean containsNull = + elem0 == null || (size > 1 && elem1 == null) || (size > 2 && elem2 == null); + Class elemClass = + elem0 != null ? elem0.getClass() : elem1 != null ? elem1.getClass() : null; + if (elemClass == null && elem2 != null) { + elemClass = elem2.getClass(); + } + boolean hasDifferentClass = + elemClass != null + && ((elem0 != null && elem0.getClass() != elemClass) + || (elem1 != null && elem1.getClass() != elemClass) + || (elem2 != null && elem2.getClass() != elemClass)); + int bitmap = containsNull ? CollectionFlags.HAS_NULL : 0; + if (hasDifferentClass) { + buffer.writeByte(bitmap); + } else { + if (elemClass == null) { + elemClass = Object.class; + } + bitmap |= CollectionFlags.IS_SAME_TYPE; + // Write class in case peer doesn't have this class. + if (!config.isMetaShareEnabled() && elemClass == declareElementType) { + bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; + buffer.writeByte(bitmap); + } else { + buffer.writeByte(bitmap); + TypeResolver typeResolver = this.typeResolver; + TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); + typeResolver.writeTypeInfo(writeContext, typeInfo); + } + } + return bitmap; + } + int bitmap = 0; + boolean containsNull = false; + boolean hasDifferentClass = false; + Class elemClass = null; + for (int i = 0; i < size; i++) { + Object elem = value.get(i); + if (elem == null) { + containsNull = true; + } else if (elemClass == null) { + elemClass = elem.getClass(); + } else { + if (!hasDifferentClass && elem.getClass() != elemClass) { + hasDifferentClass = true; + } + } + } + if (containsNull) { + bitmap |= CollectionFlags.HAS_NULL; + } + if (hasDifferentClass) { + buffer.writeByte(bitmap); + } else { + // When serialize a collection with all elements null directly, the declare type + // will be equal to element type: null + if (elemClass == null) { + elemClass = Object.class; + } + bitmap |= CollectionFlags.IS_SAME_TYPE; + // Write class in case peer doesn't have this class. + if (!config.isMetaShareEnabled() && elemClass == declareElementType) { + bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; + buffer.writeByte(bitmap); + } else { + buffer.writeByte(bitmap); + TypeResolver typeResolver = this.typeResolver; + TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); + typeResolver.writeTypeInfo(writeContext, typeInfo); + } + } + return bitmap; + } + @Override public void write(WriteContext writeContext, T value) { Collection collection = onCollectionWrite(writeContext, value); diff --git a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java index 7fbf021ff5..52c4d0ce50 100644 --- a/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java +++ b/java/fory-core/src/main/java25/org/apache/fory/memory/MemoryBuffer.java @@ -1108,7 +1108,9 @@ public void writeUInt8(int value) { public void writeByte(byte value) { final int writerIdx = writerIndex; final int newIdx = writerIdx + 1; - ensure(newIdx); + if (newIdx > size) { + globalAllocator.grow(this, newIdx); + } final long pos = address + writerIdx; storeByte(pos, value); writerIndex = newIdx; From 157aba07dcbce0fed58fd034e2645543aad97d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 14:57:22 +0800 Subject: [PATCH 02/20] perf(java): improve compatible serialization paths --- .../src/main/java/org/apache/fory/Fory.java | 4 +- .../fory/builder/BaseObjectCodecBuilder.java | 19 ++++++-- .../org/apache/fory/context/ReadContext.java | 18 ++++++++ .../org/apache/fory/context/WriteContext.java | 28 ++++++++++++ .../apache/fory/resolver/TypeResolver.java | 6 ++- .../fory/serializer/StringSerializer.java | 43 ++++++++++++++++++- .../fory/serializer/StringSerializerTest.java | 41 ++++++++++++++++++ 7 files changed, 150 insertions(+), 9 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/Fory.java b/java/fory-core/src/main/java/org/apache/fory/Fory.java index 5ad083d69e..a7916400ae 100644 --- a/java/fory-core/src/main/java/org/apache/fory/Fory.java +++ b/java/fory-core/src/main/java/org/apache/fory/Fory.java @@ -352,7 +352,7 @@ public MemoryBuffer serialize(MemoryBuffer buffer, Object obj, BufferCallback ca if (writeContext.getDepth() > 0) { throwDepthSerializationException(); } - writeContext.writeRef(obj); + writeContext.writeRootRef(obj); return buffer; } catch (Throwable t) { throw processSerializationError(t); @@ -512,7 +512,7 @@ public Object deserialize(MemoryBuffer buffer, Iterable outOfBandB if (readContext.getDepth() > 0) { throwDepthDeserializationException(); } - return readContext.readRef(); + return readContext.readRootRef(); } finally { jitContext.unlock(); } diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index e76176dc33..8546b06e84 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1219,6 +1219,17 @@ protected Expression readTypeInfo(Class cls, Expression ignored, boolean inli } } + protected Expression readTypeInfoWithTarget(Class cls) { + Reference classInfoHolderRef = addTypeInfoHolderField(cls); + return inlineInvoke( + typeResolverRef, + "readTypeInfo", + classInfoTypeRef, + readContextRef, + getClassExpr(cls), + classInfoHolderRef); + } + protected TypeRef getSerializerType(TypeRef objType) { return getSerializerType(objType.getRawType()); } @@ -2783,8 +2794,10 @@ protected Expression deserializeForCollection( protected Expression readCollectionCodegen( Expression buffer, Expression collection, Expression size, TypeRef elementType) { ListExpression builder = new ListExpression(); - Invoke flags = new Invoke(buffer, "readByte", "flags", PRIMITIVE_INT_TYPE, false); - builder.add(flags); + Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); + Expression flags = + cast(new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), PRIMITIVE_INT_TYPE); + builder.add(readerIndex, flags, new Invoke(buffer, "_increaseReaderIndexUnsafe", ofInt(1))); Class elemClass = TypeUtils.getRawType(elementType); walkPath.add(elementType.toString()); boolean finalType = isMonomorphic(elemClass); @@ -2816,7 +2829,7 @@ protected Expression readCollectionCodegen( Literal isDeclTypeFlag = ofInt(CollectionFlags.IS_DECL_ELEMENT_TYPE); Expression isDeclType = eq(new BitAnd(flags, isDeclTypeFlag), isDeclTypeFlag); Invoke serializer = - inlineInvoke(readTypeInfo(elemClass, buffer), "getSerializer", SERIALIZER_TYPE); + inlineInvoke(readTypeInfoWithTarget(elemClass), "getSerializer", SERIALIZER_TYPE); TypeRef serializerType = getSerializerType(elementType); Expression elemSerializer; // make it in scope of `if(sameElementClass)` boolean maybeDecl = typeResolver(r -> r.isSerializable(elemClass)); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java index 6dca2e503a..dcc7553b59 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java @@ -55,9 +55,11 @@ public final class ReadContext { private final Generics generics; private final TypeResolver typeResolver; private final RefReader refReader; + private final TypeInfoHolder rootTypeInfoHolder; private final MetaStringReader metaStringReader; private final StringSerializer stringSerializer; private final boolean crossLanguage; + private final boolean trackingRef; private final boolean compressInt; private final Int64Encoding longEncoding; private final int maxDepth; @@ -86,9 +88,11 @@ public ReadContext( this.generics = generics; this.typeResolver = typeResolver; this.refReader = refReader; + rootTypeInfoHolder = typeResolver.nilTypeInfoHolder(); this.metaStringReader = metaStringReader; stringSerializer = (StringSerializer) typeResolver.getSerializer(String.class); crossLanguage = config.isXlang(); + trackingRef = config.trackingRef(); compressInt = config.compressInt(); longEncoding = config.longEncoding(); maxDepth = config.maxDepth(); @@ -537,6 +541,20 @@ public Object readRef() { return refReader.getReadRef(); } + /** Reads the root object for one deserialization operation. */ + public Object readRootRef() { + if (trackingRef) { + return readRef(rootTypeInfoHolder); + } + MemoryBuffer buffer = this.buffer; + int headFlag = buffer.readByte(); + if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) { + TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder); + return readNonRef(typeInfo); + } + return null; + } + /** Variant of {@link #readRef()} that uses already resolved {@link TypeInfo}. */ public Object readRef(TypeInfo typeInfo) { int nextReadRefId = refReader.tryPreserveRefId(buffer); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index b2be9b7eb5..745a992f77 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -56,9 +56,11 @@ public final class WriteContext { private final Generics generics; private final TypeResolver typeResolver; private final RefWriter refWriter; + private final TypeInfoHolder rootTypeInfoHolder; private final MetaStringWriter metaStringWriter; private final StringSerializer stringSerializer; private final boolean crossLanguage; + private final boolean trackingRef; private final boolean compressInt; private final Int64Encoding longEncoding; private final boolean forVirtualThread; @@ -85,9 +87,11 @@ public WriteContext( this.generics = generics; this.typeResolver = typeResolver; this.refWriter = refWriter; + rootTypeInfoHolder = typeResolver.nilTypeInfoHolder(); this.metaStringWriter = metaStringWriter; stringSerializer = (StringSerializer) typeResolver.getSerializer(String.class); crossLanguage = config.isXlang(); + trackingRef = config.trackingRef(); compressInt = config.compressInt(); longEncoding = config.longEncoding(); forVirtualThread = config.forVirtualThread(); @@ -460,6 +464,30 @@ public void writeRef(Object obj) { } } + /** Writes the root object for one serialization operation. */ + public void writeRootRef(Object obj) { + if (trackingRef) { + writeRef(obj, rootTypeInfoHolder); + return; + } + MemoryBuffer buffer = this.buffer; + if (obj == null) { + buffer.writeByte(Fory.NULL_FLAG); + return; + } + buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG); + TypeResolver resolver = typeResolver; + TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), rootTypeInfoHolder); + if (crossLanguage && typeInfo.getType() == UnknownStruct.class) { + depth++; + typeInfo.getSerializer().write(this, obj); + depth--; + return; + } + resolver.writeTypeInfo(this, typeInfo); + writeData(typeInfo, obj); + } + /** Variant of {@link #writeRef(Object)} that reuses a cached type-info holder. */ public void writeRef(Object obj, TypeInfoHolder classInfoHolder) { MemoryBuffer buffer = this.buffer; diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index a480f45325..e2e845c1dc 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -827,7 +827,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassTypeInfo(readContext, null); + typeInfo = readSharedClassTypeInfo(readContext, null, classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; break; case Types.NAMED_ENUM: case Types.NAMED_STRUCT: @@ -837,7 +838,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); updateCache = true; } else { - typeInfo = readSharedClassTypeInfo(readContext, null); + typeInfo = readSharedClassTypeInfo(readContext, null, classInfoHolder.typeInfo); + updateCache = typeInfo != classInfoHolder.typeInfo; } break; case Types.LIST: diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index bb3d93f22a..a331f85fdb 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java @@ -125,7 +125,7 @@ public static Expression writeStringExpr( if (compressString) { return new Invoke(strSerializer, "writeCompressedBytesString", buffer, str); } else { - return new StaticInvoke(StringSerializer.class, "writeBytesString", buffer, str); + return new StaticInvoke(StringSerializer.class, "writeBytesStringCodegen", buffer, str); } } else { if (!STRING_VALUE_FIELD_IS_CHARS) { @@ -529,6 +529,45 @@ public static void writeBytesString(MemoryBuffer buffer, String value) { writeBytesString(buffer, coder, bytes); } + @Internal + @CodegenInvoke + public static void writeBytesStringCodegen(MemoryBuffer buffer, String value) { + byte[] bytes = (byte[]) getStringValue(value); + byte coder = getStringCoder(value); + writeBytesStringCodegen(buffer, coder, bytes); + } + + private static void writeBytesStringCodegen(MemoryBuffer buffer, byte coder, byte[] bytes) { + if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { + writeBytesStringUTF16BE(buffer, bytes); + return; + } + int bytesLen = bytes.length; + long header = ((long) bytesLen << 2) | coder; + int writerIndex = buffer.writerIndex(); + buffer.ensure(writerIndex + 9 + bytesLen); + final byte[] targetArray = buffer.getHeapMemory(); + if (targetArray != null) { + final int targetIndex = buffer._unsafeHeapWriterIndex(); + int arrIndex = targetIndex; + if (header >>> 7 == 0) { + targetArray[arrIndex++] = (byte) header; + } else if (header >>> 14 == 0) { + targetArray[arrIndex++] = (byte) ((header & 0x7F) | 0x80); + targetArray[arrIndex++] = (byte) (header >>> 7); + } else { + arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); + } + writerIndex += arrIndex - targetIndex; + System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); + } else { + writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); + PlatformStringUtils.putBytes(buffer, writerIndex, bytes, bytesLen); + } + writerIndex += bytesLen; + buffer._unsafeWriterIndex(writerIndex); + } + public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { writeBytesStringUTF16BE(buffer, bytes); @@ -644,7 +683,7 @@ public byte[] readBytesUnCompressedUTF16(MemoryBuffer buffer, int numBytes) { byte[] heapMemory = buffer.getHeapMemory(); if (heapMemory != null) { final int arrIndex = buffer._unsafeHeapReaderIndex(); - buffer.increaseReaderIndex(numBytes); + buffer._increaseReaderIndexUnsafe(numBytes); bytes = new byte[numBytes]; System.arraycopy(heapMemory, arrIndex, bytes, 0, numBytes); } else { diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index 756c00e554..c7b10e1ad8 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java @@ -60,6 +60,47 @@ public void testRejectOddUtf16ByteSize() { Assert.assertThrows(IllegalArgumentException.class, () -> serializer.readString(buffer)); } + @Test + public void testCodegenByteStringWire() { + if (!stringValueIsBytes()) { + throw new SkipException("Skip when string value is not byte[]"); + } + String[] values = { + "", + "a", + StringUtils.random(31), + StringUtils.random(40), + StringUtils.random(5000), + "你好", + "abc你好" + }; + for (String value : values) { + int capacity = Math.max(64, value.length() * 8 + 64); + MemoryBuffer control = MemoryBuffer.newHeapBuffer(capacity); + MemoryBuffer codegen = MemoryBuffer.newHeapBuffer(capacity); + StringSerializer.writeBytesString(control, value); + StringSerializer.writeBytesStringCodegen(codegen, value); + Assert.assertEquals( + codegen.getBytes(0, codegen.writerIndex()), + control.getBytes(0, control.writerIndex()), + value); + Assert.assertEquals( + readJDK11String(MemoryBuffer.fromByteArray(codegen.getBytes(0, codegen.writerIndex()))), + value); + } + } + + private static boolean stringValueIsBytes() { + try { + Field valueIsBytesField = + StringSerializer.class.getDeclaredField("STRING_VALUE_FIELD_IS_BYTES"); + valueIsBytesField.setAccessible(true); + return (boolean) valueIsBytesField.get(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + @Test public void testJavaStringZeroCopy() { if (JdkVersion.MAJOR_VERSION >= 17) { From 232c6cef8a41a7d8227da3f9ad0b675f9cb57835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 16:36:53 +0800 Subject: [PATCH 03/20] perf(java): clean compatible mode optimizations --- .../fory/builder/BaseObjectCodecBuilder.java | 62 +++------ .../org/apache/fory/builder/CodecBuilder.java | 3 + .../builder/LayerMarkerClassGenerator.java | 2 +- .../apache/fory/context/MetaWriteContext.java | 92 +------------ .../org/apache/fory/context/WriteContext.java | 2 +- .../apache/fory/resolver/TypeResolver.java | 6 +- .../CompatibleLayerSerializerBase.java | 6 +- .../serializer/UnknownClassSerializers.java | 6 +- .../collection/ChildContainerSerializers.java | 4 +- .../collection/CollectionLikeSerializer.java | 130 ------------------ 10 files changed, 41 insertions(+), 272 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 8546b06e84..70f0ff757d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1380,7 +1380,7 @@ protected Expression writeCollectionData( Class elemClass = TypeUtils.getRawType(elementType); boolean trackingRef = needWriteRef(elementType); Tuple2 writeElementsHeader = - writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection, size); + writeElementsHeader(elemClass, trackingRef, serializer, buffer, collection); Expression flags = writeElementsHeader.f0; builder.add(flags); boolean finalType = isMonomorphic(elemClass); @@ -1520,9 +1520,7 @@ private Tuple2 writeElementsHeader( boolean trackingRef, Expression collectionSerializer, Expression buffer, - Expression value, - Expression size) { - boolean isList = List.class.isAssignableFrom(getRawType(value.type())); + Expression value) { if (isMonomorphic(elementType)) { Expression bitmap; if (trackingRef) { @@ -1531,24 +1529,9 @@ private Tuple2 writeElementsHeader( new Invoke(buffer, "writeByte", ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)), ofInt(CollectionFlags.DECL_SAME_TYPE_TRACKING_REF)); } else { - if (isList) { - bitmap = - new Invoke( - collectionSerializer, - "writeNullabilityHeader", - PRIMITIVE_INT_TYPE, - buffer, - value, - size); - } else { - bitmap = - new Invoke( - collectionSerializer, - "writeNullabilityHeader", - PRIMITIVE_INT_TYPE, - buffer, - value); - } + bitmap = + new Invoke( + collectionSerializer, "writeNullabilityHeader", PRIMITIVE_INT_TYPE, buffer, value); } return Tuple2.of(bitmap, null); } else { @@ -1577,28 +1560,15 @@ private Tuple2 writeElementsHeader( classInfoHolder); } } else { - if (isList) { - bitmap = - new Invoke( - collectionSerializer, - "writeTypeNullabilityHeader", - PRIMITIVE_INT_TYPE, - writeContextRef(), - value, - size, - elementTypeExpr, - classInfoHolder); - } else { - bitmap = - new Invoke( - collectionSerializer, - "writeTypeNullabilityHeader", - PRIMITIVE_INT_TYPE, - writeContextRef(), - value, - elementTypeExpr, - classInfoHolder); - } + bitmap = + new Invoke( + collectionSerializer, + "writeTypeNullabilityHeader", + PRIMITIVE_INT_TYPE, + writeContextRef(), + value, + elementTypeExpr, + classInfoHolder); } Invoke serializer = new Invoke(classInfoHolder, "getSerializer", SERIALIZER_TYPE); return Tuple2.of(bitmap, serializer); @@ -2796,7 +2766,9 @@ protected Expression readCollectionCodegen( ListExpression builder = new ListExpression(); Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); Expression flags = - cast(new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), PRIMITIVE_INT_TYPE); + cast( + new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), + PRIMITIVE_INT_TYPE); builder.add(readerIndex, flags, new Invoke(buffer, "_increaseReaderIndexUnsafe", ofInt(1))); Class elemClass = TypeUtils.getRawType(elementType); walkPath.add(elementType.toString()); diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java index 7bfd4898c9..8e5083b6ca 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java @@ -518,6 +518,8 @@ private Expression varHandleSetField(Expression bean, Descriptor descriptor, Exp if (descriptor.getTypeRef().isPrimitive()) { Preconditions.checkArgument(getRawType(value.type()) == getRawType(fieldType)); } + // Janino cannot compile VarHandle's signature-polymorphic access modes directly. Keep generated + // serializers on typed helper methods even though the VarHandle itself is a static final field. return new StaticInvoke( varHandleSupportClass(), varHandleSetMethod(fieldType), @@ -609,6 +611,7 @@ protected Reference getOrCreateField( private Expression varHandleGetField(Expression inputObject, Descriptor descriptor) { TypeRef returnType = descriptor.getTypeRef().isPrimitive() ? descriptor.getTypeRef() : OBJECT_TYPE; + // See varHandleSetField: direct VarHandle access-mode calls are not valid Janino output. Expression getValue = new StaticInvoke( varHandleSupportClass(), diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java index c9e2e8c78d..3e761b55a2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/LayerMarkerClassGenerator.java @@ -26,7 +26,7 @@ /** * Creates unique marker classes for each layer in a class hierarchy. These marker classes serve as - * unique keys in {@code MetaWriteContext} to distinguish different layers during serialization. + * unique keys in {@code metaContext.classMap} to distinguish different layers during serialization. * *

For a class hierarchy {@code C extends B extends A}, this generator creates unique marker * classes for: diff --git a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java index 84d164c2e0..a39d2ef0b2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/MetaWriteContext.java @@ -19,6 +19,8 @@ package org.apache.fory.context; +import org.apache.fory.collection.IdentityObjectIntMap; + /** * Write-side state for meta-share serialization. * @@ -26,93 +28,9 @@ * already announced classes are not sent repeatedly. */ public class MetaWriteContext { - private static final int INITIAL_CAPACITY = 8; - private static final int MISSING_ID = -1; - - private Object[] keys = new Object[INITIAL_CAPACITY]; - private int[] idsBySlot = new int[INITIAL_CAPACITY]; - private int[] usedSlots = new int[INITIAL_CAPACITY]; - private int mask = INITIAL_CAPACITY - 1; - private int threshold = INITIAL_CAPACITY >> 1; - private int size; - - /** Returns the next protocol id that will be assigned to a new metadata key. */ - public int size() { - return size; - } - /** - * Returns the existing protocol id for {@code key}, or records it and returns {@code -1}. - * - *

Meta-share keys use identity semantics. Class objects and layer marker classes are already - * identity keys, and unknown-struct TypeDef ids historically used the same identity map path. + * Classes whose definitions have already been announced to the peer, mapped to the protocol id + * used by the current or shared meta-share session. */ - public int putOrGetMetaId(Object key) { - if (key == null) { - throw new NullPointerException("Meta key must not be null"); - } - int slot = locate(key, keys, mask); - if (slot >= 0) { - return idsBySlot[slot]; - } - if (size >= threshold) { - resize(keys.length << 1); - slot = locate(key, keys, mask); - } - slot = -(slot + 1); - int id = size; - keys[slot] = key; - idsBySlot[slot] = id; - usedSlots[id] = slot; - size = id + 1; - return MISSING_ID; - } - - /** Clears operation-local metadata ids while retaining reusable table storage. */ - public void reset() { - int size = this.size; - if (size == 0) { - return; - } - Object[] keys = this.keys; - int[] usedSlots = this.usedSlots; - for (int i = 0; i < size; i++) { - keys[usedSlots[i]] = null; - } - this.size = 0; - } - - private static int locate(Object key, Object[] keys, int mask) { - for (int slot = System.identityHashCode(key) & mask; ; slot = (slot + 1) & mask) { - Object other = keys[slot]; - if (other == null) { - return -(slot + 1); - } - if (other == key) { - return slot; - } - } - } - - private void resize(int newCapacity) { - Object[] oldKeys = keys; - int[] oldUsedSlots = usedSlots; - int oldSize = size; - Object[] newKeys = new Object[newCapacity]; - int[] newIdsBySlot = new int[newCapacity]; - int[] newUsedSlots = new int[newCapacity]; - int newMask = newCapacity - 1; - for (int id = 0; id < oldSize; id++) { - Object key = oldKeys[oldUsedSlots[id]]; - int slot = -(locate(key, newKeys, newMask) + 1); - newKeys[slot] = key; - newIdsBySlot[slot] = id; - newUsedSlots[id] = slot; - } - keys = newKeys; - idsBySlot = newIdsBySlot; - usedSlots = newUsedSlots; - mask = newMask; - threshold = newCapacity >> 1; - } + public final IdentityObjectIntMap> classMap = new IdentityObjectIntMap<>(1, 0.5f); } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index 745a992f77..257afb991a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -299,7 +299,7 @@ public void reset() { contextObjects.clear(); } if (scopedMetaShareEnabled) { - metaWriteContext.reset(); + metaWriteContext.classMap.clear(); } else { metaWriteContext = null; } diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index e2e845c1dc..e22bed10c9 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -55,6 +55,7 @@ import org.apache.fory.collection.BiMap; import org.apache.fory.collection.ConcurrentIdentityMap; import org.apache.fory.collection.IdentityMap; +import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.Tuple2; import org.apache.fory.config.Config; @@ -586,8 +587,9 @@ protected final void writeSharedClassMeta(WriteContext writeContext, TypeInfo ty MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG; - int newId = metaWriteContext.size(); - int id = metaWriteContext.putOrGetMetaId(typeInfo.type); + IdentityObjectIntMap> classMap = metaWriteContext.classMap; + int newId = classMap.size; + int id = classMap.putOrGet(typeInfo.type, newId); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java index e7ce30603e..e7040f16f4 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java @@ -19,6 +19,7 @@ package org.apache.fory.serializer; +import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.ObjectIntMap; import org.apache.fory.context.CopyContext; import org.apache.fory.context.MetaWriteContext; @@ -86,8 +87,9 @@ public void writeLayerClassMeta(WriteContext writeContext) { if (metaWriteContext == null) { return; } - int newId = metaWriteContext.size(); - int id = metaWriteContext.putOrGetMetaId(layerMarkerClass); + IdentityObjectIntMap> classMap = metaWriteContext.classMap; + int newId = classMap.size; + int id = classMap.putOrGet(layerMarkerClass, newId); if (id >= 0) { buffer.writeVarUInt32((id << 1) | 1); } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java index 5fdae10109..ef03486e11 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.MapEntry; import org.apache.fory.config.Config; @@ -91,9 +92,10 @@ public UnknownStructSerializer(TypeResolver typeResolver, TypeDef typeDef) { private void writeTypeDef(WriteContext writeContext, UnknownClass.UnknownStruct value) { MemoryBuffer buffer = writeContext.getBuffer(); MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); - int newId = metaWriteContext.size(); + IdentityObjectIntMap classMap = metaWriteContext.classMap; + int newId = classMap.size; // class not exist, use class def id for identity. - int id = metaWriteContext.putOrGetMetaId(value.typeDef.getId()); + int id = classMap.putOrGet(value.typeDef.getId(), newId); if (id >= 0) { // Reference to previously written type: (index << 1) | 1, LSB=1 buffer.writeVarUInt32((id << 1) | 1); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java index acb1700311..f7840349ef 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java @@ -710,8 +710,8 @@ private static void readAndSkipLayerClassMeta(ReadContext readContext) { // New type - need to read and skip the TypeDef bytes long id = buffer.readInt64(); TypeDef.skipTypeDef(buffer, id); - // Add a placeholder to keep readTypeInfos indices in sync with the write side's meta ids. - // The write side (writeLayerClassMeta) adds layer marker classes to the metadata table, sharing + // Add a placeholder to keep readTypeInfos indices in sync with the write side's classMap. + // The write side (writeLayerClassMeta) adds layer marker classes to classMap which shares // the same index space as writeSharedClassMeta. Without this placeholder, subsequent // readSharedClassMeta reference lookups would use wrong indices. metaReadContext.readTypeInfos.add(null); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java index 221b104508..3915b5d888 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java @@ -22,7 +22,6 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.util.Collection; -import java.util.List; import org.apache.fory.Fory; import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.config.Config; @@ -179,44 +178,6 @@ public int writeNullabilityHeader(MemoryBuffer buffer, Collection value) { return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; } - /** Element type is final, write whether any elements is null. */ - @CodegenInvoke - public int writeNullabilityHeader(MemoryBuffer buffer, List value, int size) { - switch (size) { - case 1: - if (value.get(0) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - case 2: - if (value.get(0) == null || value.get(1) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - case 3: - if (value.get(0) == null || value.get(1) == null || value.get(2) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - default: - break; - } - for (int i = 0; i < size; i++) { - if (value.get(i) == null) { - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_HAS_NULL; - } - } - buffer.writeByte(CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL); - return CollectionFlags.DECL_SAME_TYPE_NOT_HAS_NULL; - } - /** * Need to track elements ref, declared element type is not morphic, can't check elements * nullability. @@ -359,97 +320,6 @@ public int writeTypeNullabilityHeader( return bitmap; } - /** - * Element type is not final by {@link ClassResolver#isMonomorphic}, need to write element type. - * Elements ref tracking is disabled, write whether any elements is null. - */ - @CodegenInvoke - public int writeTypeNullabilityHeader( - WriteContext writeContext, - List value, - int size, - Class declareElementType, - TypeInfoHolder cache) { - MemoryBuffer buffer = writeContext.getBuffer(); - if (size > 0 && size <= 3) { - Object elem0 = value.get(0); - Object elem1 = size > 1 ? value.get(1) : null; - Object elem2 = size > 2 ? value.get(2) : null; - boolean containsNull = - elem0 == null || (size > 1 && elem1 == null) || (size > 2 && elem2 == null); - Class elemClass = - elem0 != null ? elem0.getClass() : elem1 != null ? elem1.getClass() : null; - if (elemClass == null && elem2 != null) { - elemClass = elem2.getClass(); - } - boolean hasDifferentClass = - elemClass != null - && ((elem0 != null && elem0.getClass() != elemClass) - || (elem1 != null && elem1.getClass() != elemClass) - || (elem2 != null && elem2.getClass() != elemClass)); - int bitmap = containsNull ? CollectionFlags.HAS_NULL : 0; - if (hasDifferentClass) { - buffer.writeByte(bitmap); - } else { - if (elemClass == null) { - elemClass = Object.class; - } - bitmap |= CollectionFlags.IS_SAME_TYPE; - // Write class in case peer doesn't have this class. - if (!config.isMetaShareEnabled() && elemClass == declareElementType) { - bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; - buffer.writeByte(bitmap); - } else { - buffer.writeByte(bitmap); - TypeResolver typeResolver = this.typeResolver; - TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); - typeResolver.writeTypeInfo(writeContext, typeInfo); - } - } - return bitmap; - } - int bitmap = 0; - boolean containsNull = false; - boolean hasDifferentClass = false; - Class elemClass = null; - for (int i = 0; i < size; i++) { - Object elem = value.get(i); - if (elem == null) { - containsNull = true; - } else if (elemClass == null) { - elemClass = elem.getClass(); - } else { - if (!hasDifferentClass && elem.getClass() != elemClass) { - hasDifferentClass = true; - } - } - } - if (containsNull) { - bitmap |= CollectionFlags.HAS_NULL; - } - if (hasDifferentClass) { - buffer.writeByte(bitmap); - } else { - // When serialize a collection with all elements null directly, the declare type - // will be equal to element type: null - if (elemClass == null) { - elemClass = Object.class; - } - bitmap |= CollectionFlags.IS_SAME_TYPE; - // Write class in case peer doesn't have this class. - if (!config.isMetaShareEnabled() && elemClass == declareElementType) { - bitmap |= CollectionFlags.IS_DECL_ELEMENT_TYPE; - buffer.writeByte(bitmap); - } else { - buffer.writeByte(bitmap); - TypeResolver typeResolver = this.typeResolver; - TypeInfo typeInfo = typeResolver.getTypeInfo(elemClass, cache); - typeResolver.writeTypeInfo(writeContext, typeInfo); - } - } - return bitmap; - } - @Override public void write(WriteContext writeContext, T value) { Collection collection = onCollectionWrite(writeContext, value); From 31e048cb870d71f45b000ac56dc107545efb1f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Thu, 25 Jun 2026 19:05:37 +0800 Subject: [PATCH 04/20] perf(java): speed compatible type info writes --- .../fory/builder/BaseObjectCodecBuilder.java | 7 ++-- .../apache/fory/resolver/TypeResolver.java | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 70f0ff757d..930385a79e 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -901,7 +901,7 @@ protected Expression writeForNotNullNonFinalObject( Class clz = getRawType(typeRef); Expression clsExpr = new Invoke(inputObject, "getClass", "cls", CLASS_TYPE); ListExpression writeClassAndObject = new ListExpression(); - Expression exactClassWrite = exactClassWrite(inputObject, clz); + Expression exactClassWrite = exactClassWrite(inputObject, buffer, clz); Tuple2 classInfoRef = addTypeInfoField(clz); Expression classInfo = classInfoRef.f0; if (classInfoRef.f1) { @@ -929,14 +929,15 @@ protected Expression writeForNotNullNonFinalObject( ctx, writeCutPoints(buffer, inputObject), write, "writeClassAndObject", false); } - private Expression exactClassWrite(Expression inputObject, Class clz) { + private Expression exactClassWrite(Expression inputObject, Expression buffer, Class clz) { if (clz.isInterface() || Modifier.isAbstract(clz.getModifiers())) { return null; } Reference typeInfo = addExactTypeInfoField(clz); Expression serializer = getOrCreateSerializer(clz); return new ListExpression( - typeResolver(r -> r.writeClassExpr(typeResolverRef, writeContextRef(), typeInfo)), + typeResolver( + r -> r.writeExactClassExpr(typeResolverRef, writeContextRef(), buffer, typeInfo, clz)), new Invoke( serializer, writeMethodName, PRIMITIVE_VOID_TYPE, writeContextRef(), inputObject)); } diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index e22bed10c9..7b1a635969 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -557,6 +557,27 @@ public final void writeTypeInfo(WriteContext writeContext, TypeInfo typeInfo) { } } + // Generated exact-class branches call this only after code generation resolves the TypeInfo to a + // compatible struct id. The wire body stays in writeSharedClassMeta; this helper only skips the + // generic type-id switch. + @Internal + public final void writeCompatibleTypeInfo( + WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) { + assert isCompatibleStructTypeId(typeInfo.typeId); + buffer.writeUInt8(typeInfo.typeId); + writeSharedClassMeta(writeContext, buffer, typeInfo); + } + + private static boolean isCompatibleStructTypeId(int typeId) { + switch (typeId) { + case Types.COMPATIBLE_STRUCT: + case Types.NAMED_COMPATIBLE_STRUCT: + return true; + default: + return false; + } + } + static int toUserTypeId(long userTypeId) { Preconditions.checkArgument( userTypeId >= 0 && userTypeId <= MAX_USER_TYPE_ID, @@ -576,6 +597,21 @@ public Expression writeClassExpr( return new Invoke(classResolverRef, "writeTypeInfo", buffer, classInfo); } + // Note: Thread safe for jit thread to call. + public Expression writeExactClassExpr( + Expression classResolverRef, + Expression writeContext, + Expression buffer, + Expression classInfo, + Class cls) { + TypeInfo typeInfo = getTypeInfo(cls); + if (isCompatibleStructTypeId(typeInfo.typeId)) { + return new Invoke( + classResolverRef, "writeCompatibleTypeInfo", writeContext, buffer, classInfo); + } + return writeClassExpr(classResolverRef, writeContext, classInfo); + } + /** * Writes shared class metadata using the meta-share protocol. Protocol: If class already written, * writes {@code (index << 1) | 1} (reference). If new class, writes {@code (index << 1)} followed @@ -585,6 +621,11 @@ public Expression writeClassExpr( */ protected final void writeSharedClassMeta(WriteContext writeContext, TypeInfo typeInfo) { MemoryBuffer buffer = writeContext.getBuffer(); + writeSharedClassMeta(writeContext, buffer, typeInfo); + } + + private void writeSharedClassMeta( + WriteContext writeContext, MemoryBuffer buffer, TypeInfo typeInfo) { MetaWriteContext metaWriteContext = writeContext.getMetaWriteContext(); assert metaWriteContext != null : SET_META_WRITE_CONTEXT_MSG; IdentityObjectIntMap> classMap = metaWriteContext.classMap; From 496da59e76c5a42ac20337231aa1e3500b1b83b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 26 Jun 2026 15:30:20 +0800 Subject: [PATCH 05/20] perf(java): unify compatible string writes --- .../fory/builder/BaseObjectCodecBuilder.java | 21 +------ .../apache/fory/resolver/TypeResolver.java | 62 ------------------- .../fory/serializer/StringSerializer.java | 40 +----------- .../fory/serializer/StringSerializerTest.java | 14 ++--- 4 files changed, 8 insertions(+), 129 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 930385a79e..64bdba0394 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1220,17 +1220,6 @@ protected Expression readTypeInfo(Class cls, Expression ignored, boolean inli } } - protected Expression readTypeInfoWithTarget(Class cls) { - Reference classInfoHolderRef = addTypeInfoHolderField(cls); - return inlineInvoke( - typeResolverRef, - "readTypeInfo", - classInfoTypeRef, - readContextRef, - getClassExpr(cls), - classInfoHolderRef); - } - protected TypeRef getSerializerType(TypeRef objType) { return getSerializerType(objType.getRawType()); } @@ -2681,15 +2670,9 @@ protected Expression readForNotNullNonFinal( || typeInfo.getTypeId() == Types.NAMED_COMPATIBLE_STRUCT)) { String name = ctx.newName(StringUtils.uncapitalize(rawType.getSimpleName()) + "Class"); Expression clsExpr = staticClassFieldExpr(rawType, name); - Reference classInfoHolderRef = addTypeInfoHolderField(rawType); classInfo = inlineInvoke( - typeResolverRef, - "readTypeInfo", - classInfoTypeRef, - readContextRef, - clsExpr, - classInfoHolderRef); + typeResolverRef, "readTypeInfo", classInfoTypeRef, readContextRef, clsExpr); } else { classInfo = readTypeInfo(getRawType(typeRef), buffer); } @@ -2802,7 +2785,7 @@ protected Expression readCollectionCodegen( Literal isDeclTypeFlag = ofInt(CollectionFlags.IS_DECL_ELEMENT_TYPE); Expression isDeclType = eq(new BitAnd(flags, isDeclTypeFlag), isDeclTypeFlag); Invoke serializer = - inlineInvoke(readTypeInfoWithTarget(elemClass), "getSerializer", SERIALIZER_TYPE); + inlineInvoke(readTypeInfo(elemClass, buffer), "getSerializer", SERIALIZER_TYPE); TypeRef serializerType = getSerializerType(elementType); Expression elemSerializer; // make it in scope of `if(sameElementClass)` boolean maybeDecl = typeResolver(r -> r.isSerializable(elemClass)); diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index 7b1a635969..35417977f0 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -744,60 +744,6 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass return typeInfo; } - /** - * Read class info from buffer using a target class and generated field-local cache. - * - *

The holder caches only checked metadata owned by this resolver. It never caches payload - * data. - */ - @CodegenInvoke - public final TypeInfo readTypeInfo( - ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { - MemoryBuffer buffer = readContext.getBuffer(); - int typeId = buffer.readUInt8(); - TypeInfo typeInfo; - boolean updateCache = false; - switch (typeId) { - case Types.ENUM: - case Types.STRUCT: - case Types.EXT: - case Types.TYPED_UNION: - typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); - updateCache = typeInfo != classInfoHolder.typeInfo; - break; - case Types.COMPATIBLE_STRUCT: - case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); - break; - case Types.NAMED_ENUM: - case Types.NAMED_STRUCT: - case Types.NAMED_EXT: - case Types.NAMED_UNION: - if (!metaContextShareEnabled) { - typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); - updateCache = true; - } else { - typeInfo = readSharedClassMeta(readContext, targetClass, classInfoHolder); - } - break; - case Types.LIST: - typeInfo = readListTypeInfo(readContext); - break; - case Types.TIMESTAMP: - typeInfo = readTimestampTypeInfo(readContext); - break; - default: - typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); - } - if (typeInfo.serializer == null) { - typeInfo = ensureSerializerForTypeInfo(typeInfo); - } - if (updateCache) { - classInfoHolder.typeInfo = typeInfo; - } - return typeInfo; - } - /** * Read class info from buffer with TypeInfo cache. This version is faster than {@link * #readTypeInfo(ReadContext)} because it uses the provided classInfoCache to reduce map lookups @@ -959,14 +905,6 @@ public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targ return adaptSharedClassTarget(typeInfo, targetClass); } - private TypeInfo readSharedClassMeta( - ReadContext readContext, Class targetClass, TypeInfoHolder classInfoHolder) { - TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass, classInfoHolder.typeInfo); - typeInfo = adaptSharedClassTarget(typeInfo, targetClass); - classInfoHolder.typeInfo = typeInfo; - return typeInfo; - } - private TypeInfo adaptSharedClassTarget(TypeInfo typeInfo, Class targetClass) { Class readClass = typeInfo.getType(); if (targetClass != readClass) { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java index a331f85fdb..2ad935101f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/StringSerializer.java @@ -125,7 +125,7 @@ public static Expression writeStringExpr( if (compressString) { return new Invoke(strSerializer, "writeCompressedBytesString", buffer, str); } else { - return new StaticInvoke(StringSerializer.class, "writeBytesStringCodegen", buffer, str); + return new StaticInvoke(StringSerializer.class, "writeBytesString", buffer, str); } } else { if (!STRING_VALUE_FIELD_IS_CHARS) { @@ -529,15 +529,7 @@ public static void writeBytesString(MemoryBuffer buffer, String value) { writeBytesString(buffer, coder, bytes); } - @Internal - @CodegenInvoke - public static void writeBytesStringCodegen(MemoryBuffer buffer, String value) { - byte[] bytes = (byte[]) getStringValue(value); - byte coder = getStringCoder(value); - writeBytesStringCodegen(buffer, coder, bytes); - } - - private static void writeBytesStringCodegen(MemoryBuffer buffer, byte coder, byte[] bytes) { + public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { writeBytesStringUTF16BE(buffer, bytes); return; @@ -568,34 +560,6 @@ private static void writeBytesStringCodegen(MemoryBuffer buffer, byte coder, byt buffer._unsafeWriterIndex(writerIndex); } - public static void writeBytesString(MemoryBuffer buffer, byte coder, byte[] bytes) { - if (!NativeByteOrder.IS_LITTLE_ENDIAN && coder == UTF16) { - writeBytesStringUTF16BE(buffer, bytes); - return; - } - int bytesLen = bytes.length; - long header = ((long) bytesLen << 2) | coder; - int writerIndex = buffer.writerIndex(); - // The `ensure` ensure next operations are safe without bound checks, - // and inner heap buffer doesn't change. - buffer.ensure(writerIndex + 9 + bytesLen); // 1 byte coder + varint max 8 bytes - final byte[] targetArray = buffer.getHeapMemory(); - if (targetArray != null) { - // Some JDK11 Unsafe.copyMemory will `copyMemoryChecks`, and - // jvm doesn't eliminate well in some jdk. - final int targetIndex = buffer._unsafeHeapWriterIndex(); - int arrIndex = targetIndex; - arrIndex += LittleEndian.putVarUint36Small(targetArray, arrIndex, header); - writerIndex += arrIndex - targetIndex; - System.arraycopy(bytes, 0, targetArray, arrIndex, bytesLen); - } else { - writerIndex += buffer._unsafePutVarUint36Small(writerIndex, header); - PlatformStringUtils.putBytes(buffer, writerIndex, bytes, bytesLen); - } - writerIndex += bytesLen; - buffer._unsafeWriterIndex(writerIndex); - } - @CodegenInvoke public void writeCharsString(MemoryBuffer buffer, String value) { final char[] chars = (char[]) getStringValue(value); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java index c7b10e1ad8..c910414959 100644 --- a/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/StringSerializerTest.java @@ -61,7 +61,7 @@ public void testRejectOddUtf16ByteSize() { } @Test - public void testCodegenByteStringWire() { + public void testBytesStringWire() { if (!stringValueIsBytes()) { throw new SkipException("Skip when string value is not byte[]"); } @@ -76,16 +76,10 @@ public void testCodegenByteStringWire() { }; for (String value : values) { int capacity = Math.max(64, value.length() * 8 + 64); - MemoryBuffer control = MemoryBuffer.newHeapBuffer(capacity); - MemoryBuffer codegen = MemoryBuffer.newHeapBuffer(capacity); - StringSerializer.writeBytesString(control, value); - StringSerializer.writeBytesStringCodegen(codegen, value); + MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(capacity); + StringSerializer.writeBytesString(buffer, value); Assert.assertEquals( - codegen.getBytes(0, codegen.writerIndex()), - control.getBytes(0, control.writerIndex()), - value); - Assert.assertEquals( - readJDK11String(MemoryBuffer.fromByteArray(codegen.getBytes(0, codegen.writerIndex()))), + readJDK11String(MemoryBuffer.fromByteArray(buffer.getBytes(0, buffer.writerIndex()))), value); } } From 573cd3814ee269089665c3441f2e277b725e095f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Fri, 26 Jun 2026 16:42:53 +0800 Subject: [PATCH 06/20] fix(java): handle unknown enum codegen --- .../fory/builder/BaseObjectCodecBuilder.java | 38 +++++++++------ .../org/apache/fory/context/ReadContext.java | 28 +++++------ .../org/apache/fory/context/WriteContext.java | 48 +++++++++---------- .../serializer/UnknownClassSerializers.java | 16 ++++++- 4 files changed, 76 insertions(+), 54 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 64bdba0394..c1f281ed3b 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -138,6 +138,7 @@ import org.apache.fory.serializer.ReplaceResolveSerializer; import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.StringSerializer; +import org.apache.fory.serializer.UnknownClassSerializers; import org.apache.fory.serializer.collection.CollectionFlags; import org.apache.fory.serializer.collection.CollectionLikeSerializer; import org.apache.fory.serializer.collection.CollectionSerializer; @@ -702,16 +703,16 @@ private Expression serializeForNotNullObjectForField( TypeRef typeRef = descriptor.getTypeRef(); Class clz = getRawType(typeRef); if (isEnumType(clz)) { - Expression enumSerializer = - cast( - serializer == null ? getSerializerForField(clz) : serializer, - TypeRef.of(EnumSerializer.class)); - return new Invoke( - enumSerializer, - "writeValue", - writeContextRef(), - buffer, - cast(inputObject, TypeRef.of(Enum.class))); + Expression enumSerializer = serializer == null ? getSerializerForField(clz) : serializer; + if (hasEnumValueMethods(enumSerializer)) { + Class serializerClass = getRawType(enumSerializer.type()); + Expression value = + EnumSerializer.class.isAssignableFrom(serializerClass) + ? cast(inputObject, TypeRef.of(Enum.class)) + : inputObject; + return new Invoke(enumSerializer, "writeValue", writeContextRef(), buffer, value); + } + return new Invoke(enumSerializer, writeMethodName, writeContextRef(), inputObject); } if (serializer != null) { return new Invoke(serializer, writeMethodName, writeContextRef(), inputObject); @@ -733,6 +734,13 @@ private static boolean isEnumType(Class cls) { return cls != Enum.class && Enum.class.isAssignableFrom(cls); } + private static boolean hasEnumValueMethods(Expression serializer) { + Class serializerClass = getRawType(serializer.type()); + // UnknownEnumSerializer is enum-shaped but does not extend EnumSerializer. + return EnumSerializer.class.isAssignableFrom(serializerClass) + || UnknownClassSerializers.UnknownEnumSerializer.class.isAssignableFrom(serializerClass); + } + protected Expression serializeForNullable( Expression inputObject, Expression buffer, TypeRef typeRef, boolean nullable) { return serializeForNullable(inputObject, buffer, typeRef, null, false, nullable); @@ -2419,10 +2427,12 @@ private Expression deserializeForNotNullForField( getOrCreateStringSerializer(), buffer, config.compressString()); } if (isEnumType(cls)) { - Expression enumSerializer = - cast( - serializer == null ? getSerializerForField(cls) : serializer, - TypeRef.of(EnumSerializer.class)); + Expression enumSerializer = serializer == null ? getSerializerForField(cls) : serializer; + if (!hasEnumValueMethods(enumSerializer)) { + return cast( + new Invoke(enumSerializer, readMethodName, OBJECT_TYPE, readContextRef()), + TypeRef.of(Enum.class)); + } return new Invoke( enumSerializer, "readValue", TypeRef.of(Enum.class), readContextRef(), buffer); } diff --git a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java index dcc7553b59..b1000161b3 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/ReadContext.java @@ -541,20 +541,6 @@ public Object readRef() { return refReader.getReadRef(); } - /** Reads the root object for one deserialization operation. */ - public Object readRootRef() { - if (trackingRef) { - return readRef(rootTypeInfoHolder); - } - MemoryBuffer buffer = this.buffer; - int headFlag = buffer.readByte(); - if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) { - TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder); - return readNonRef(typeInfo); - } - return null; - } - /** Variant of {@link #readRef()} that uses already resolved {@link TypeInfo}. */ public Object readRef(TypeInfo typeInfo) { int nextReadRefId = refReader.tryPreserveRefId(buffer); @@ -596,6 +582,20 @@ public T readRef(Serializer serializer) { return (T) readNonRef(serializer); } + /** Reads the root object for one deserialization operation. */ + public Object readRootRef() { + if (trackingRef) { + return readRef(rootTypeInfoHolder); + } + MemoryBuffer buffer = this.buffer; + int headFlag = buffer.readByte(); + if (headFlag >= Fory.NOT_NULL_VALUE_FLAG) { + TypeInfo typeInfo = typeResolver.readTypeInfo(this, rootTypeInfoHolder); + return readNonRef(typeInfo); + } + return null; + } + /** Reads a non-null, first-seen object together with its type metadata. */ public Object readNonRef() { TypeInfo typeInfo = typeResolver.readTypeInfo(this); diff --git a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java index 257afb991a..bf74c0a67a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/context/WriteContext.java @@ -464,30 +464,6 @@ public void writeRef(Object obj) { } } - /** Writes the root object for one serialization operation. */ - public void writeRootRef(Object obj) { - if (trackingRef) { - writeRef(obj, rootTypeInfoHolder); - return; - } - MemoryBuffer buffer = this.buffer; - if (obj == null) { - buffer.writeByte(Fory.NULL_FLAG); - return; - } - buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG); - TypeResolver resolver = typeResolver; - TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), rootTypeInfoHolder); - if (crossLanguage && typeInfo.getType() == UnknownStruct.class) { - depth++; - typeInfo.getSerializer().write(this, obj); - depth--; - return; - } - resolver.writeTypeInfo(this, typeInfo); - writeData(typeInfo, obj); - } - /** Variant of {@link #writeRef(Object)} that reuses a cached type-info holder. */ public void writeRef(Object obj, TypeInfoHolder classInfoHolder) { MemoryBuffer buffer = this.buffer; @@ -555,6 +531,30 @@ public void writeRef(T obj, Serializer serializer) { } } + /** Writes the root object for one serialization operation. */ + public void writeRootRef(Object obj) { + if (trackingRef) { + writeRef(obj, rootTypeInfoHolder); + return; + } + MemoryBuffer buffer = this.buffer; + if (obj == null) { + buffer.writeByte(Fory.NULL_FLAG); + return; + } + buffer.writeByte(Fory.NOT_NULL_VALUE_FLAG); + TypeResolver resolver = typeResolver; + TypeInfo typeInfo = resolver.getTypeInfo(obj.getClass(), rootTypeInfoHolder); + if (crossLanguage && typeInfo.getType() == UnknownStruct.class) { + depth++; + typeInfo.getSerializer().write(this, obj); + depth--; + return; + } + resolver.writeTypeInfo(this, typeInfo); + writeData(typeInfo, obj); + } + /** * Writes a non-null, first-seen object together with its type metadata. * diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java index ef03486e11..1356d80a35 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnknownClassSerializers.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.fory.annotation.CodegenInvoke; import org.apache.fory.collection.IdentityObjectIntMap; import org.apache.fory.collection.LongMap; import org.apache.fory.collection.MapEntry; @@ -294,21 +295,32 @@ public UnknownEnumSerializer(TypeResolver typeResolver) { @Override public void write(WriteContext writeContext, UnknownEnum value) { + writeValue(writeContext, writeContext.getBuffer(), value); + } + + @CodegenInvoke + public final void writeValue( + WriteContext writeContext, MemoryBuffer buffer, UnknownEnum value) { if (!config.isXlang() && config.serializeEnumByName()) { writeContext.writeString(value.name()); } else { - writeContext.getBuffer().writeVarUInt32Small7(value.ordinal()); + buffer.writeVarUInt32Small7(value.ordinal()); } } @Override public UnknownEnum read(ReadContext readContext) { + return readValue(readContext, readContext.getBuffer()); + } + + @CodegenInvoke + public final UnknownEnum readValue(ReadContext readContext, MemoryBuffer buffer) { if (!config.isXlang() && config.serializeEnumByName()) { readContext.readString(); return UnknownEnum.UNKNOWN; } - int ordinal = readContext.getBuffer().readVarUInt32Small7(); + int ordinal = buffer.readVarUInt32Small7(); if (ordinal >= enumConstants.length) { return UnknownEnum.UNKNOWN; } From b3117d0129ce5a6b4e7ef7ab2e7e7cfd9f32a7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 11:06:01 +0800 Subject: [PATCH 07/20] docs: format generated code example --- docs/compiler/generated-code.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md index b3e5ebc91f..68fddf234c 100644 --- a/docs/compiler/generated-code.md +++ b/docs/compiler/generated-code.md @@ -1062,8 +1062,7 @@ export enum AnimalCase { } export type Animal = - | { case: AnimalCase.DOG; value: Dog } - | { case: AnimalCase.CAT; value: Cat }; + { case: AnimalCase.DOG; value: Dog } | { case: AnimalCase.CAT; value: Cat }; ``` ### Schema Helpers From 20dd260aaa99376cfaccfc67d41bb1f6fb3aff2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 11:09:47 +0800 Subject: [PATCH 08/20] fix(java): check generated collection flag reads --- .../apache/fory/builder/BaseObjectCodecBuilder.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index c1f281ed3b..3a9b2451bb 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -2730,6 +2730,8 @@ protected Expression deserializeForCollection( Expression collection = new Invoke(serializer, "newCollection", COLLECTION_TYPE, readContextRef); Expression size = new Invoke(serializer, "getAndClearNumElements", "size", PRIMITIVE_INT_TYPE); + // Do not add an ArrayList-specific branch here: it pushes generated code over 325 bytes, and + // List#add is more likely to inline when the call site has only one receiver subclass. Expression hookRead = readCollectionCodegen(buffer, collection, size, elementType); hookRead = new Invoke(serializer, "onCollectionRead", OBJECT_TYPE, hookRead); Expression fallbackAction = read(serializer, buffer, OBJECT_TYPE); @@ -2758,12 +2760,11 @@ protected Expression deserializeForCollection( protected Expression readCollectionCodegen( Expression buffer, Expression collection, Expression size, TypeRef elementType) { ListExpression builder = new ListExpression(); - Expression readerIndex = new Invoke(buffer, "readerIndex", "readerIndex", PRIMITIVE_INT_TYPE); + // The flags byte is the next encoded byte. Use the checked read path so truncated + // buffer-backed or stream-backed input fails before advancing the reader index. Expression flags = - cast( - new Invoke(buffer, "_unsafeGetByte", PRIMITIVE_BYTE_TYPE, readerIndex), - PRIMITIVE_INT_TYPE); - builder.add(readerIndex, flags, new Invoke(buffer, "_increaseReaderIndexUnsafe", ofInt(1))); + cast(new Invoke(buffer, "readByte", PRIMITIVE_BYTE_TYPE), PRIMITIVE_INT_TYPE); + builder.add(flags); Class elemClass = TypeUtils.getRawType(elementType); walkPath.add(elementType.toString()); boolean finalType = isMonomorphic(elemClass); From 5c909ff7c294e71d3bb1e727b38d2a1a2aa64683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 11:30:30 +0800 Subject: [PATCH 09/20] perf(java): remove nullable type info cache check --- .../src/main/java/org/apache/fory/resolver/TypeResolver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index 35417977f0..4d9239c187 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -151,7 +151,7 @@ private static final class TransformedTypeInfo { final LongMap userTypeIdToTypeInfo = new LongMap<>(4, TYPE_ID_MAP_LOAD_FACTOR); // Cache for readTypeInfo(MemoryBuffer) - persists between calls to avoid reloading // dynamically created classes that can't be found by Class.forName - private TypeInfo typeInfoCache; + private TypeInfo typeInfoCache = NIL_TYPE_INFO; private boolean registrationFinished; protected TypeResolver( @@ -851,7 +851,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class private TypeInfo readRegisteredTypeInfo(int typeId, int userTypeId, TypeInfo cachedTypeInfo) { TypeInfo typeInfo = cachedTypeInfo; - if (typeInfo == null || typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) { + if (typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) { typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(userTypeId)); } return typeInfo; From 88535b1d150c9a2dcb5ca5f58faa24f0cedc8907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 11:32:35 +0800 Subject: [PATCH 10/20] refactor(java): inline shared class target adaptation --- .../src/main/java/org/apache/fory/resolver/TypeResolver.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index 4d9239c187..e84abbe270 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -902,10 +902,6 @@ protected final TypeInfo readTypeInfoFromBytes( public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targetClass) { TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass); - return adaptSharedClassTarget(typeInfo, targetClass); - } - - private TypeInfo adaptSharedClassTarget(TypeInfo typeInfo, Class targetClass) { Class readClass = typeInfo.getType(); if (targetClass != readClass) { return getTargetTypeInfo(typeInfo, targetClass); From 503499456e8b944e8c2d4c696ce1165a846ec62d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 12:30:57 +0800 Subject: [PATCH 11/20] perf(java): update type info holder after resolution --- .../apache/fory/resolver/TypeResolver.java | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index e84abbe270..2e89ed479f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -804,46 +804,40 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfo typeInfoCac public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder classInfoHolder) { MemoryBuffer buffer = readContext.getBuffer(); int typeId = buffer.readUInt8(); + TypeInfo cachedTypeInfo = classInfoHolder.typeInfo; TypeInfo typeInfo; - boolean updateCache = false; switch (typeId) { case Types.ENUM: case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), classInfoHolder.typeInfo); - updateCache = typeInfo != classInfoHolder.typeInfo; + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), cachedTypeInfo); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassTypeInfo(readContext, null, classInfoHolder.typeInfo); - updateCache = typeInfo != classInfoHolder.typeInfo; + typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo); break; case Types.NAMED_ENUM: case Types.NAMED_STRUCT: case Types.NAMED_EXT: case Types.NAMED_UNION: if (!metaContextShareEnabled) { - typeInfo = readTypeInfoFromBytes(readContext, classInfoHolder.typeInfo, typeId); - updateCache = true; + typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo, typeId); } else { - typeInfo = readSharedClassTypeInfo(readContext, null, classInfoHolder.typeInfo); - updateCache = typeInfo != classInfoHolder.typeInfo; + typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo); } break; case Types.LIST: - typeInfo = readListTypeInfo(readContext); - break; + return readListTypeInfo(readContext); case Types.TIMESTAMP: - typeInfo = readTimestampTypeInfo(readContext); - break; + return readTimestampTypeInfo(readContext); default: - typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); + return Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); } if (typeInfo.serializer == null) { typeInfo = ensureSerializerForTypeInfo(typeInfo); } - if (updateCache) { + if (typeInfo != cachedTypeInfo) { classInfoHolder.typeInfo = typeInfo; } return typeInfo; @@ -852,20 +846,12 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class private TypeInfo readRegisteredTypeInfo(int typeId, int userTypeId, TypeInfo cachedTypeInfo) { TypeInfo typeInfo = cachedTypeInfo; if (typeInfo.typeId != typeId || typeInfo.userTypeId != userTypeId) { - typeInfo = Objects.requireNonNull(userTypeIdToTypeInfo.get(userTypeId)); + typeInfo = userTypeIdToTypeInfo.get(userTypeId); + assert typeInfo != null; } return typeInfo; } - /** - * Read class info using the provided cache. Returns cached TypeInfo if the namespace and type - * name bytes match. - */ - protected final TypeInfo readTypeInfoByCache( - ReadContext readContext, TypeInfo typeInfoCache, int header) { - return readTypeInfoFromBytes(readContext, typeInfoCache, header); - } - /** * Read class info from bytes with cache optimization. Uses the cached namespace and type name * bytes to avoid map lookups when the class is the same as the cached one. From 9ea646858d7c2c1ce289a69f01d080f55b18eca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 13:08:34 +0800 Subject: [PATCH 12/20] fix(java): avoid null type info cache for union reads --- .../org/apache/fory/serializer/UnionSerializer.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java index 15b53b575e..aadd270719 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java @@ -175,12 +175,13 @@ public Union read(ReadContext readContext) { if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) { // ref value or not-null value TypeInfo declared = getFinalCaseTypeInfo(caseId); - TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared); if (declared != null) { + TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared); Serializer serializer = getCaseSerializer(caseId, readTypeInfo.getTypeId(), declared); GenericType genericType = getCaseGenericType(caseId, readTypeInfo.getTypeId()); caseValue = readCaseValue(readContext, serializer, genericType); } else { + TypeInfo readTypeInfo = resolver.readTypeInfo(readContext); caseValue = Serializers.read(readContext, readTypeInfo.getSerializer()); } readContext.setReadRef(nextReadRefId, caseValue); @@ -333,7 +334,12 @@ public static Object readCaseValue( int nextReadRefId = readContext.tryPreserveRefId(); if (nextReadRefId >= Fory.NOT_NULL_VALUE_FLAG) { TypeInfo declared = getDeclaredCaseTypeInfo(fieldInfo, typeId); - TypeInfo readTypeInfo = resolver.readTypeInfo(readContext, declared); + TypeInfo readTypeInfo; + if (declared != null) { + readTypeInfo = resolver.readTypeInfo(readContext, declared); + } else { + readTypeInfo = resolver.readTypeInfo(readContext); + } Serializer serializer = getCaseSerializer(fieldInfo, readTypeInfo.getTypeId(), readTypeInfo); Object caseValue = readCaseValue( From 6219f2e5040670fc90b6c6fc69135e5fa674b5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 13:21:05 +0800 Subject: [PATCH 13/20] perf(java): index type info cache by depth --- .../apache/fory/resolver/TypeResolver.java | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index 2e89ed479f..cf3d85b73f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -123,6 +123,7 @@ public abstract class TypeResolver { static final int INTERNAL_NATIVE_ID_LIMIT = 250; private static final GenericType OBJECT_GENERIC_TYPE = GenericType.build(Object.class); private static final float TYPE_ID_MAP_LOAD_FACTOR = 0.5f; + private static final int TYPE_INFO_CACHE_DEPTH_SLACK = 10; static final long MAX_USER_TYPE_ID = 0xffff_fffEL; private static final class TransformedTypeInfo { @@ -149,9 +150,9 @@ private static final class TransformedTypeInfo { final TypeInfo[] typeIdToTypeInfo; // Map for user-registered type ids, keyed by user id. final LongMap userTypeIdToTypeInfo = new LongMap<>(4, TYPE_ID_MAP_LOAD_FACTOR); - // Cache for readTypeInfo(MemoryBuffer) - persists between calls to avoid reloading + // Caches for readTypeInfo(ReadContext) - persist between calls to avoid reloading // dynamically created classes that can't be found by Class.forName - private TypeInfo typeInfoCache = NIL_TYPE_INFO; + private final TypeInfo[] typeInfoCache; private boolean registrationFinished; protected TypeResolver( @@ -167,6 +168,8 @@ protected TypeResolver( typeDefMap = sharedRegistry.typeDefMap; int length = isCrossLanguage() ? Types.BOUND : INTERNAL_NATIVE_ID_LIMIT; typeIdToTypeInfo = new TypeInfo[length]; + typeInfoCache = new TypeInfo[config.maxDepth() + TYPE_INFO_CACHE_DEPTH_SLACK]; + Arrays.fill(typeInfoCache, NIL_TYPE_INFO); } public final Config getConfig() { @@ -660,6 +663,8 @@ private void writeSharedClassMeta( */ public final TypeInfo readTypeInfo(ReadContext readContext) { MemoryBuffer buffer = readContext.getBuffer(); + int depth = readContext.getDepth(); + TypeInfo cachedTypeInfo = typeInfoCache[depth]; int typeId = buffer.readUInt8(); TypeInfo typeInfo; switch (typeId) { @@ -667,20 +672,20 @@ public final TypeInfo readTypeInfo(ReadContext readContext) { case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), cachedTypeInfo); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassTypeInfo(readContext, null); + typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo); break; case Types.NAMED_ENUM: case Types.NAMED_STRUCT: case Types.NAMED_EXT: case Types.NAMED_UNION: if (!metaContextShareEnabled) { - typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId); + typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo, typeId); } else { - typeInfo = readSharedClassTypeInfo(readContext, null); + typeInfo = readSharedClassTypeInfo(readContext, null, cachedTypeInfo); } break; case Types.LIST: @@ -695,7 +700,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext) { if (typeInfo.serializer == null) { typeInfo = ensureSerializerForTypeInfo(typeInfo); } - typeInfoCache = typeInfo; + typeInfoCache[depth] = typeInfo; return typeInfo; } @@ -705,6 +710,8 @@ public final TypeInfo readTypeInfo(ReadContext readContext) { */ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass) { MemoryBuffer buffer = readContext.getBuffer(); + int depth = readContext.getDepth(); + TypeInfo cachedTypeInfo = typeInfoCache[depth]; int typeId = buffer.readUInt8(); TypeInfo typeInfo; switch (typeId) { @@ -712,20 +719,20 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass case Types.STRUCT: case Types.EXT: case Types.TYPED_UNION: - typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), typeInfoCache); + typeInfo = readRegisteredTypeInfo(typeId, buffer.readVarUInt32(), cachedTypeInfo); break; case Types.COMPATIBLE_STRUCT: case Types.NAMED_COMPATIBLE_STRUCT: - typeInfo = readSharedClassMeta(readContext, targetClass); + typeInfo = readSharedClassMeta(readContext, targetClass, cachedTypeInfo); break; case Types.NAMED_ENUM: case Types.NAMED_STRUCT: case Types.NAMED_EXT: case Types.NAMED_UNION: if (!metaContextShareEnabled) { - typeInfo = readTypeInfoFromBytes(readContext, typeInfoCache, typeId); + typeInfo = readTypeInfoFromBytes(readContext, cachedTypeInfo, typeId); } else { - typeInfo = readSharedClassMeta(readContext, targetClass); + typeInfo = readSharedClassMeta(readContext, targetClass, cachedTypeInfo); } break; case Types.LIST: @@ -740,7 +747,7 @@ public final TypeInfo readTypeInfo(ReadContext readContext, Class targetClass if (typeInfo.serializer == null) { typeInfo = ensureSerializerForTypeInfo(typeInfo); } - typeInfoCache = typeInfo; + typeInfoCache[depth] = typeInfo; return typeInfo; } @@ -887,7 +894,12 @@ protected final TypeInfo readTypeInfoFromBytes( } public final TypeInfo readSharedClassMeta(ReadContext readContext, Class targetClass) { - TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass); + return readSharedClassMeta(readContext, targetClass, null); + } + + private TypeInfo readSharedClassMeta( + ReadContext readContext, Class targetClass, TypeInfo cachedTypeInfo) { + TypeInfo typeInfo = readSharedClassTypeInfo(readContext, targetClass, cachedTypeInfo); Class readClass = typeInfo.getType(); if (targetClass != readClass) { return getTargetTypeInfo(typeInfo, targetClass); From a98ba648f5efe504c8da6c669d6d0c79fa64e148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 13:34:17 +0800 Subject: [PATCH 14/20] perf(java): index writer type info cache by depth --- .../apache/fory/resolver/ClassResolver.java | 52 ++++++++----------- .../apache/fory/resolver/TypeResolver.java | 2 +- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java index 253c14b507..b0c8caf7c8 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java @@ -42,6 +42,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; @@ -245,7 +246,7 @@ public class ClassResolver extends TypeResolver { public static final short REPLACE_STUB_ID = INTERNAL_TYPE_START_ID + 28; public static final int NONEXISTENT_META_SHARED_ID = REPLACE_STUB_ID + 1; - private TypeInfo typeInfoCache; + private final TypeInfo[] typeInfoCache; // Every deserialization for unregistered class will query it, performance is important. private final ObjectMap compositeNameBytes2TypeInfo = new ObjectMap<>(16, foryMapLoadFactor); @@ -257,12 +258,17 @@ public ClassResolver( SharedRegistry sharedRegistry, JITContext jitContext) { super(config, classLoader, sharedRegistry, jitContext); - typeInfoCache = NIL_TYPE_INFO; + typeInfoCache = new TypeInfo[config.maxDepth() + TYPE_INFO_CACHE_DEPTH_SLACK]; + clearTypeInfoCache(); extRegistry.classIdGenerator = NONEXISTENT_META_SHARED_ID + 1; shimDispatcher = new ShimDispatcher(this); _addGraalvmClassRegistry(config.getConfigHash(), this); } + private void clearTypeInfoCache() { + Arrays.fill(typeInfoCache, NIL_TYPE_INFO); + } + @Override public void initialize() { extRegistry.objectGenericType = buildGenericType(OBJECT_TYPE); @@ -1245,9 +1251,7 @@ private void registerSerializerImpl(Class type, Serializer serializer) { TypeNameBytes typeNameBytes = new TypeNameBytes(typeInfo.namespace, typeInfo.typeName); compositeNameBytes2TypeInfo.put(typeNameBytes, typeInfo); } - if (typeInfoCache.type == type) { - typeInfoCache = NIL_TYPE_INFO; - } + clearTypeInfoCache(); } } // in order to support customized serializer for abstract or interface. @@ -1306,7 +1310,7 @@ public void setSerializer(String className, Class serializ if (cls.getName().equals(className)) { LOG.info("Clear serializer for class {}.", className); entry.getValue().setSerializer(this, Serializers.newSerializer(this, cls, serializer)); - typeInfoCache = NIL_TYPE_INFO; + clearTypeInfoCache(); return; } } @@ -1323,7 +1327,7 @@ public void setSerializers(String classNamePrefix, Class s if (className.startsWith(classNamePrefix)) { LOG.info("Clear serializer for class {}.", className); entry.getValue().setSerializer(this, Serializers.newSerializer(this, cls, serializer)); - typeInfoCache = NIL_TYPE_INFO; + clearTypeInfoCache(); } } } @@ -1570,9 +1574,7 @@ public Class getSerializerClass(Class cls, boolean code @Override public void onSuccess(Class result) { setSerializer(clz, Serializers.newSerializer(ClassResolver.this, clz, result)); - if (typeInfoCache.type == clz) { - typeInfoCache = NIL_TYPE_INFO; // clear class info cache - } + clearTypeInfoCache(); Preconditions.checkState(getSerializer(clz).getClass() == result); } @@ -1707,30 +1709,18 @@ public TypeInfo getTypeInfo(Class cls, boolean createTypeInfoIfNotFound) { @Internal public TypeInfo getOrUpdateTypeInfo(Class cls) { - TypeInfo typeInfo = typeInfoCache; + return getOrUpdateTypeInfo(cls, 0); + } + + private TypeInfo getOrUpdateTypeInfo(Class cls, int depth) { + TypeInfo typeInfo = typeInfoCache[depth]; if (typeInfo.type != cls) { typeInfo = classInfoMap.get(cls); if (typeInfo == null || typeInfo.serializer == null) { addSerializer(cls, createSerializer(cls)); typeInfo = classInfoMap.get(cls); } - typeInfoCache = typeInfo; - } - return typeInfo; - } - - private TypeInfo getOrUpdateTypeInfo(short classId) { - TypeInfo typeInfo = typeInfoCache; - TypeInfo internalInfo = classId < typeIdToTypeInfo.length ? typeIdToTypeInfo[classId] : null; - Preconditions.checkArgument( - internalInfo != null, "Internal class id %s is not registered", classId); - if (typeInfo != internalInfo) { - typeInfo = internalInfo; - if (typeInfo.serializer == null) { - addSerializer(typeInfo.type, createSerializer(typeInfo.type)); - typeInfo = classInfoMap.get(typeInfo.type); - } - typeInfoCache = typeInfo; + typeInfoCache[depth] = typeInfo; } return typeInfo; } @@ -1881,7 +1871,7 @@ private void registerGraalvmSerializerClass(Class cls) { .putCompatibleDeserializerClass( cls, CodecUtils.loadOrGenStaticCompatibleCodecClass(this, cls, typeDef)); } - typeInfoCache = NIL_TYPE_INFO; + clearTypeInfoCache(); if (RecordUtils.isRecord(cls)) { RecordUtils.getRecordConstructor(cls); RecordUtils.getRecordComponents(cls); @@ -1995,7 +1985,7 @@ public void writeClassAndUpdateCache(WriteContext writeContext, Class cls) { } else if (cls == Long.class) { buffer.writeVarUInt32Small7(Types.INT64); } else { - writeTypeInfo(writeContext, getOrUpdateTypeInfo(cls)); + writeTypeInfo(writeContext, getOrUpdateTypeInfo(cls, writeContext.getDepth())); } } @@ -2474,7 +2464,7 @@ public void ensureSerializersCompiled() { } }); if (GraalvmSupport.isGraalBuildTime()) { - typeInfoCache = NIL_TYPE_INFO; + clearTypeInfoCache(); clearGraalvmGeneratedTypeInfoSerializers(); compositeNameBytes2TypeInfo.forEach( (typeNameBytes, typeInfo) -> clearGraalvmTypeInfoSerializer(typeInfo)); diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index cf3d85b73f..ddb9091b4a 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -123,7 +123,7 @@ public abstract class TypeResolver { static final int INTERNAL_NATIVE_ID_LIMIT = 250; private static final GenericType OBJECT_GENERIC_TYPE = GenericType.build(Object.class); private static final float TYPE_ID_MAP_LOAD_FACTOR = 0.5f; - private static final int TYPE_INFO_CACHE_DEPTH_SLACK = 10; + static final int TYPE_INFO_CACHE_DEPTH_SLACK = 10; static final long MAX_USER_TYPE_ID = 0xffff_fffEL; private static final class TransformedTypeInfo { From 7b0e285f8f76c019b960d99a61d94d4e150f61fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 13:41:52 +0800 Subject: [PATCH 15/20] revert(java): remove hidden nestmate codegen path --- .agents/languages/java.md | 15 ++-- .../fory/builder/BaseObjectCodecBuilder.java | 4 - .../org/apache/fory/builder/CodecUtils.java | 47 +++-------- .../apache/fory/codegen/CodeGenerator.java | 80 +------------------ .../apache/fory/codegen/CodegenContext.java | 19 +---- .../org/apache/fory/codegen/CompileUnit.java | 18 ----- .../fory/codegen/ExpressionOptimizer.java | 12 +-- .../apache/fory/builder/CodecUtilsTest.java | 15 ---- 8 files changed, 31 insertions(+), 179 deletions(-) diff --git a/.agents/languages/java.md b/.agents/languages/java.md index 3f4b165126..41b19b206d 100644 --- a/.agents/languages/java.md +++ b/.agents/languages/java.md @@ -205,15 +205,14 @@ Load this file when changing anything under `java/` or when Java drives a cross- overlay only to call `Lookup#defineHiddenClass` directly, and do not move it to `java9` because `Lookup#defineClass` defines normal package classes, not hidden nestmates. Root code must avoid direct `Lookup.ClassOption` linkage and cache the method-handle/option-array setup off the hot - path. -- Hidden generated serializers are Java25+ only. Do not broaden serializer hidden-class definition - to Java15-24, because those runtimes still use the unsafe-backed field/object path. Keep - `AccessorHelper` as the source-generated same-package helper; do not turn it into a bytecode + path. Keep this method available for future Java25+ designs even when runtime codegen does not + call it. +- Runtime generated serializers use the normal `CodeGenerator` classloader path, not hidden + nestmate class definition. Janino-generated source still cannot directly access target bean + private fields through hidden nestmate definition, so do not reintroduce hidden serializer loading + or same-package source-access plumbing without a separate Java25 design and measured proof. + `AccessorHelper` remains the source-generated same-package helper; do not turn it into a bytecode hidden-field owner unless a separate Java25-only design explicitly requires that. -- JDK25 hidden generated serializers must not emit private split helper methods. Janino lowers - private instance helpers to static bridge methods whose receiver parameter uses the original - binary class name, which fails hidden-class verification. Use non-private final split helpers on - that path. - Runtime codegen must not emit Janino source that names bootstrap JDK implementation classes in concealed or non-source-public packages. Generated source in the unnamed module cannot access those classes even when Fory's trusted field-access path can read/write their fields; use diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 3a9b2451bb..a5cbb9c6e2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -221,10 +221,6 @@ public BaseObjectCodecBuilder(TypeRef beanType, Fory fory, Class parentSer descriptorDispatchId = new HashMap<>(); } - void setSamePackageAccess(boolean samePackageAccess) { - ctx.setSamePackageAccess(samePackageAccess); - } - // Must be static to be shared across the whole process life. private static final Map> idGenerator = new ConcurrentHashMap<>(); diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java index 2361179ddb..c9092f6723 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java @@ -19,6 +19,7 @@ package org.apache.fory.builder; +import java.util.Collections; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import org.apache.fory.Fory; @@ -26,10 +27,7 @@ import org.apache.fory.codegen.CompileUnit; import org.apache.fory.collection.Tuple3; import org.apache.fory.meta.TypeDef; -import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.JdkVersion; -import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.reflect.TypeRef; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.Serializer; @@ -120,6 +118,12 @@ public static Class loadOrGenCompatibleLayerCodecClass @SuppressWarnings("unchecked") static Class loadOrGenCodecClass( Class beanClass, Fory fory, BaseObjectCodecBuilder codecBuilder) { + // use genCodeFunc to avoid gen code repeatedly + CompileUnit compileUnit = + new CompileUnit( + CodeGenerator.getPackage(beanClass), + codecBuilder.codecClassName(beanClass), + codecBuilder::genCode); CodeGenerator codeGenerator; ClassLoader beanClassClassLoader = beanClass.getClassLoader() == null @@ -130,40 +134,15 @@ static Class loadOrGenCodecClass( } TypeResolver typeResolver = fory.getTypeResolver(); codeGenerator = getCodeGenerator(beanClassClassLoader, typeResolver); - Class neighborClass = codecNeighbor(beanClass, beanClassClassLoader); - codecBuilder.setSamePackageAccess(neighborClass != null); - // use genCodeFunc to avoid gen code repeatedly - CompileUnit compileUnit = - new CompileUnit( - CodeGenerator.getPackage(beanClass), - codecBuilder.codecClassName(beanClass), - codecBuilder::genCode, - neighborClass); - return (Class) - codeGenerator.compileAndLoad(compileUnit, compileState -> compileState.lock.lock()); - } - - private static Class codecNeighbor(Class beanClass, ClassLoader beanClassClassLoader) { - // Hidden generated serializers are only a JDK25+ path. JDK8-24 keeps the existing unsafe-backed - // field/object access strategy, so broadening this to Java15+ adds complexity without removing - // unsafe from those runtimes. - if (AndroidSupport.IS_ANDROID - || JdkVersion.MAJOR_VERSION < 25 - || beanClass.getClassLoader() == null) { - return null; - } - if (!CodeGenerator.getPackage(beanClass).equals(ReflectionUtils.getPackage(beanClass))) { - return null; - } + ClassLoader classLoader = + codeGenerator.compile( + Collections.singletonList(compileUnit), compileState -> compileState.lock.lock()); + String className = codecBuilder.codecQualifiedClassName(beanClass); try { - // A generated serializer defined in the bean loader must resolve Fory runtime classes there. - if (beanClassClassLoader.loadClass(Fory.class.getName()) == Fory.class) { - return beanClass; - } + return (Class) classLoader.loadClass(className); } catch (ClassNotFoundException e) { - // The composed-loader path remains the owner when the bean loader cannot see Fory directly. + throw new IllegalStateException("Impossible because we just compiled class", e); } - return null; } private static CodeGenerator getCodeGenerator( diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java index f63e4151c9..826029a4c2 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodeGenerator.java @@ -40,7 +40,6 @@ import org.apache.fory.logging.LoggerFactory; import org.apache.fory.platform.AndroidSupport; import org.apache.fory.platform.GraalvmSupport; -import org.apache.fory.platform.internal.DefineClass; import org.apache.fory.reflect.ReflectionUtils; import org.apache.fory.util.ClassLoaderUtils; import org.apache.fory.util.ClassLoaderUtils.ByteArrayClassLoader; @@ -136,46 +135,6 @@ public ClassLoader compile(List units, CompileCallback callback) { } parentClassLoader = classLoader; } - Map classes = compileToBytecode(compileUnits, parentClassLoader, callback); - return defineClasses(classes); - } - - public Class compileAndLoad(CompileUnit unit, CompileCallback callback) { - Class neighborClass = unit.getNeighborClass(); - if (neighborClass == null) { - ClassLoader loader = compile(java.util.Collections.singletonList(unit), callback); - try { - return loader.loadClass(unit.getQualifiedClassName()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Impossible because we just compiled class", e); - } - } - checkRuntimeCodegenSupported(); - DefineState defineState = getDefineState(unit.getQualifiedClassName()); - if (defineState.definedClass != null) { - return defineState.definedClass; - } - synchronized (defineState.lock) { - if (defineState.definedClass != null) { - return defineState.definedClass; - } - ClassLoader parentClassLoader = getClassLoader(); - Map classes = - compileToBytecode(java.util.Collections.singletonList(unit), parentClassLoader, callback); - byte[] bytecodes = classes.get(classFilepath(unit)); - if (bytecodes == null) { - throw new IllegalStateException( - "Compiler did not produce bytecode for " + unit.getQualifiedClassName()); - } - Class definedClass = DefineClass.defineHiddenNestmate(neighborClass, bytecodes); - defineState.definedClass = definedClass; - defineState.defined = true; - return definedClass; - } - } - - private Map compileToBytecode( - List compileUnits, ClassLoader parentClassLoader, CompileCallback callback) { CompileState compileState = getCompileState(compileUnits); callback.lock(compileState); Map classes; @@ -192,7 +151,7 @@ private Map compileToBytecode( compileState.lock.unlock(); } } - return classes; + return defineClasses(classes); } /** @@ -342,7 +301,6 @@ private String getCompileLockKey(List toCompile) { private static class DefineState { final Object lock; volatile boolean defined; - volatile Class definedClass; private DefineState() { this.lock = new Object(); @@ -449,7 +407,7 @@ public static String classFilepath(String fullClassName) { } public static String fullClassName(CompileUnit unit) { - return unit.getQualifiedClassName(); + return unit.pkg + "." + unit.mainClassName; } public static String fullClassNameFromClassFilePath(String classFilePath) { @@ -564,40 +522,10 @@ private static boolean classSourcePublicAccessible(Class clz) { if (clz.isPrimitive()) { return true; } - if (!sourcePkgLevelAccessible(clz)) { + if (!ReflectionUtils.isPublic(clz)) { return false; } - Class current = clz; - while (current != null) { - if (!ReflectionUtils.isPublic(current)) { - return false; - } - current = current.getEnclosingClass(); - } - return true; - } - - public static boolean sourceAccessibleFrom(Class clz, String pkg) { - if (sourcePublicAccessible(clz)) { - return true; - } - if (clz.isArray()) { - return sourceAccessibleFrom(clz.getComponentType(), pkg); - } - if (!sourcePkgLevelAccessible(clz)) { - return false; - } - if (!ReflectionUtils.getPackage(clz).equals(pkg)) { - return false; - } - Class current = clz; - while (current != null) { - if (ReflectionUtils.isPrivate(current)) { - return false; - } - current = current.getEnclosingClass(); - } - return true; + return sourcePkgLevelAccessible(clz); } private static final Map, Boolean> sourcePkgLevelAccessible = diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java index 779d7d9cf3..ccfed32371 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CodegenContext.java @@ -140,7 +140,6 @@ public class CodegenContext { String pkg; LinkedHashSet imports = new LinkedHashSet<>(); String className; - private boolean samePackageAccess; String classModifiers = "public final"; String[] superClasses; String[] interfaces; @@ -356,17 +355,6 @@ public String getPackage() { return pkg; } - public void setSamePackageAccess(boolean samePackageAccess) { - if (this.samePackageAccess != samePackageAccess) { - sourcePublicAccessibleCache.clear(); - } - this.samePackageAccess = samePackageAccess; - } - - public boolean hasSamePackageAccess() { - return samePackageAccess; - } - public Set getValNames() { return valNames; } @@ -741,12 +729,7 @@ public String optimizeMethodCode(String code) { /** Returns true if class is public accessible from source. */ public boolean sourcePublicAccessible(Class clz) { - return sourcePublicAccessibleCache.computeIfAbsent( - clz, - c -> - samePackageAccess - ? CodeGenerator.sourceAccessibleFrom(c, pkg) - : CodeGenerator.sourcePublicAccessible(c)); + return sourcePublicAccessibleCache.computeIfAbsent(clz, CodeGenerator::sourcePublicAccessible); } /** Returns true if class is package level accessible from source. */ diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java index d90b6fdae4..f442c71c52 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/CompileUnit.java @@ -31,33 +31,19 @@ public class CompileUnit { String pkg; String mainClassName; - // Non-null only when the compiled class must be defined as a JDK25+ hidden nestmate of this - // neighbor. Ordinary generated classes still use the CodeGenerator classloader path. - private final Class neighborClass; private String code; private Supplier genCodeFunc; public CompileUnit(String pkg, String mainClassName, String code) { - this(pkg, mainClassName, code, null); - } - - public CompileUnit(String pkg, String mainClassName, String code, Class neighborClass) { this.pkg = pkg; this.mainClassName = mainClassName; this.code = code; - this.neighborClass = neighborClass; } public CompileUnit(String pkg, String mainClassName, Supplier genCodeFunc) { - this(pkg, mainClassName, genCodeFunc, null); - } - - public CompileUnit( - String pkg, String mainClassName, Supplier genCodeFunc, Class neighborClass) { this.pkg = pkg; this.mainClassName = mainClassName; this.genCodeFunc = genCodeFunc; - this.neighborClass = neighborClass; } public String getCode() { @@ -79,10 +65,6 @@ public String getQualifiedClassName() { } } - public Class getNeighborClass() { - return neighborClass; - } - @Override public String toString() { return "CompileUnit{" + "pkg='" + pkg + '\'' + ", mainClassName='" + mainClassName + '\'' + '}'; diff --git a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java index 68eae904bd..a9c3568e6f 100644 --- a/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java +++ b/java/fory-core/src/main/java/org/apache/fory/codegen/ExpressionOptimizer.java @@ -68,12 +68,13 @@ public static Expression invokeGenerated( Expression groupExpressions, String methodPrefix, boolean inlineInvoke) { - // Janino lowers private instance helpers to static bridge methods with the original binary - // class as receiver. Hidden classes have a different runtime identity, so JDK25 hidden - // generated serializers must keep split helpers non-private. - String modifier = ctx.hasSamePackageAccess() ? "final" : "private"; return invokeGenerated( - ctx, new LinkedHashSet<>(cutPoint), groupExpressions, modifier, methodPrefix, inlineInvoke); + ctx, + new LinkedHashSet<>(cutPoint), + groupExpressions, + "private", + methodPrefix, + inlineInvoke); } /** @@ -132,7 +133,6 @@ public static Expression invokeGenerated( // instance field name. CodegenContext codegenContext = new CodegenContext(ctx.getPackage(), ctx.getValNames(), ctx.getImports()); - codegenContext.setSamePackageAccess(ctx.hasSamePackageAccess()); for (Reference reference : cutExprMap.values()) { Preconditions.checkArgument(codegenContext.containName(reference.name())); } diff --git a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java index 41702a4425..c347b628a2 100644 --- a/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/builder/CodecUtilsTest.java @@ -20,14 +20,11 @@ package org.apache.fory.builder; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertSame; -import static org.testng.Assert.assertTrue; import org.apache.fory.Fory; import org.apache.fory.ForyTestBase; import org.apache.fory.memory.MemoryBuffer; import org.apache.fory.memory.MemoryUtils; -import org.apache.fory.platform.JdkVersion; import org.apache.fory.resolver.TypeResolver; import org.apache.fory.test.bean.BeanA; import org.testng.annotations.Test; @@ -45,7 +42,6 @@ public void loadOrGenObjectCodecClass() throws Exception { .withCompatible(false) .build(); Class seqCodecClass = fory.getTypeResolver().getSerializerClass(BeanA.class); - assertGeneratedClassShape(seqCodecClass, BeanA.class); Generated.GeneratedSerializer serializer = seqCodecClass .asSubclass(Generated.GeneratedSerializer.class) @@ -58,15 +54,4 @@ public void loadOrGenObjectCodecClass() throws Exception { Object obj = ForyTestBase.readSerializer(fory, serializer, MemoryUtils.wrap(bytes)); assertEquals(obj, beanA); } - - private static void assertGeneratedClassShape(Class serializerClass, Class beanClass) - throws Exception { - if (JdkVersion.MAJOR_VERSION >= 25) { - assertTrue((Boolean) Class.class.getMethod("isHidden").invoke(serializerClass)); - assertSame(Class.class.getMethod("getNestHost").invoke(serializerClass), beanClass); - } else { - assertSame( - serializerClass.getClassLoader().loadClass(serializerClass.getName()), serializerClass); - } - } } From 3779459c6d6a871056cf07fc5b5ca1888b29dda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 13:59:40 +0800 Subject: [PATCH 16/20] ci: use allowlisted graalvm setup action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c46905dc20..c921239be6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -535,7 +535,7 @@ jobs: java-version: ["17", "21", "25"] steps: - uses: actions/checkout@v5 - - uses: graalvm/setup-graalvm@f744c72a42b1995d7b0cbc314bde4bace7ac1fe1 # 1.5.0 + - uses: graalvm/setup-graalvm@6f3fa030c4b8f77c1f554a860f593a654538fa38 # 1.5.6 with: java-version: ${{ matrix.java-version }} distribution: "graalvm" From 698c77d29b15f979f050742c733558d67a0dcb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 14:01:51 +0800 Subject: [PATCH 17/20] ci: document apache action allowlist reference --- .agents/ci-and-pr.md | 6 +++++- .github/workflows/build-containerized-pr.yml | 3 +++ .github/workflows/build-containerized-release.yml | 3 +++ .github/workflows/build-native-pr.yml | 3 +++ .github/workflows/build-native-release.yml | 3 +++ .github/workflows/ci.yml | 3 +++ .github/workflows/clean-pr-body.yml | 3 +++ .github/workflows/lint.yml | 3 +++ .github/workflows/pr-lint.yml | 3 +++ .github/workflows/release-compiler.yaml | 3 +++ .github/workflows/release-csharp.yaml | 3 +++ .github/workflows/release-dart.yaml | 3 +++ .github/workflows/release-java-snapshot.yaml | 3 +++ .github/workflows/release-javascript.yaml | 3 +++ .github/workflows/release-python.yaml | 3 +++ .github/workflows/release-rust.yaml | 3 +++ .github/workflows/sync.yml | 3 +++ 17 files changed, 53 insertions(+), 1 deletion(-) diff --git a/.agents/ci-and-pr.md b/.agents/ci-and-pr.md index 6f7171f095..6bf0672ac7 100644 --- a/.agents/ci-and-pr.md +++ b/.agents/ci-and-pr.md @@ -132,7 +132,11 @@ implementation, CI-fix, or verification task, or when the user explicitly asks f ## Workflow Changes -- In ASF GitHub Actions, verify new `uses:` entries against approved Apache infrastructure action patterns; replace unapproved actions with approved actions plus shell or Python logic when needed. +- In ASF GitHub Actions, verify new or updated `uses:` entries against the approved Apache + infrastructure action reference at + `https://github.com/apache/infrastructure-actions/blob/main/actions.yml`; use an allowlisted pin + from that file or replace unapproved actions with approved actions plus shell or Python logic when + needed. - Do not add workflow dependencies on new repository variables or secrets when GitHub Actions context, constants, or checked-in configuration can provide stable non-secret values. Coordinate required repo config before landing if no safe default exists. - If a setup action fails because of a runtime deprecation, verify the current official major and upgrade to the compatible major rather than adding temporary runner flags. - Reusable release automation belongs under `ci/` unless the user explicitly requests a language-local script. diff --git a/.github/workflows/build-containerized-pr.yml b/.github/workflows/build-containerized-pr.yml index 55d516324c..6434f2cf54 100644 --- a/.github/workflows/build-containerized-pr.yml +++ b/.github/workflows/build-containerized-pr.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Build Containerized PR Wheels on: pull_request: diff --git a/.github/workflows/build-containerized-release.yml b/.github/workflows/build-containerized-release.yml index 6e68991441..e8b1bc7c72 100644 --- a/.github/workflows/build-containerized-release.yml +++ b/.github/workflows/build-containerized-release.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Build Containerized Release Wheels on: push: diff --git a/.github/workflows/build-native-pr.yml b/.github/workflows/build-native-pr.yml index 6c7227a75b..e45cd26d49 100644 --- a/.github/workflows/build-native-pr.yml +++ b/.github/workflows/build-native-pr.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Build Native PR Wheels on: pull_request: diff --git a/.github/workflows/build-native-release.yml b/.github/workflows/build-native-release.yml index fac625cd21..9098ade6a7 100644 --- a/.github/workflows/build-native-release.yml +++ b/.github/workflows/build-native-release.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Build Native Release Wheels on: push: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c921239be6..c6b3bf04cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Fory CI concurrency: diff --git a/.github/workflows/clean-pr-body.yml b/.github/workflows/clean-pr-body.yml index a90312f730..5961c93233 100644 --- a/.github/workflows/clean-pr-body.yml +++ b/.github/workflows/clean-pr-body.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: "Clean PR Body" on: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 67a573f5db..b64a3ad4ff 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: ❄️ Lint on: diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index bbd4c57fad..c330f6ac0e 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: "Lint PR" on: diff --git a/.github/workflows/release-compiler.yaml b/.github/workflows/release-compiler.yaml index d155c180a0..096e4c1c14 100644 --- a/.github/workflows/release-compiler.yaml +++ b/.github/workflows/release-compiler.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish Compiler run-name: "Compiler Release: ${{ github.ref_name }}" diff --git a/.github/workflows/release-csharp.yaml b/.github/workflows/release-csharp.yaml index 1f58e544c1..8f458f46ea 100644 --- a/.github/workflows/release-csharp.yaml +++ b/.github/workflows/release-csharp.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish C# run-name: "C# Release: ${{ github.ref_name }}" diff --git a/.github/workflows/release-dart.yaml b/.github/workflows/release-dart.yaml index 8525237afd..1191d8544b 100644 --- a/.github/workflows/release-dart.yaml +++ b/.github/workflows/release-dart.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish Dart run-name: "Dart Release: ${{ github.ref_name }}" diff --git a/.github/workflows/release-java-snapshot.yaml b/.github/workflows/release-java-snapshot.yaml index af869e9ebf..d82dcc6a06 100644 --- a/.github/workflows/release-java-snapshot.yaml +++ b/.github/workflows/release-java-snapshot.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish Fory Java Snapshot on: diff --git a/.github/workflows/release-javascript.yaml b/.github/workflows/release-javascript.yaml index a2883ebcfb..f0c43db2eb 100644 --- a/.github/workflows/release-javascript.yaml +++ b/.github/workflows/release-javascript.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish JavaScript run-name: "JavaScript Release: ${{ github.ref_name }}" diff --git a/.github/workflows/release-python.yaml b/.github/workflows/release-python.yaml index f09ac43d22..5c480eeac6 100644 --- a/.github/workflows/release-python.yaml +++ b/.github/workflows/release-python.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish Python run-name: "Python Release: ${{ contains(github.event.workflow_run.name, 'Containerized') && 'Containerized' || 'Native' }} (Run ID: ${{ github.event.workflow_run.id }})" diff --git a/.github/workflows/release-rust.yaml b/.github/workflows/release-rust.yaml index 9f1153bb4f..9ce88548f8 100644 --- a/.github/workflows/release-rust.yaml +++ b/.github/workflows/release-rust.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Publish Rust run-name: "Rust Release: ${{ github.ref_name }}" diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index a920ef65da..543b0ed13d 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +# All `uses:` action pins in this workflow must come from the Apache action allowlist: +# https://github.com/apache/infrastructure-actions/blob/main/actions.yml + name: Sync Files on: From ba97b11232245d76e48742d13e345530b523bd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 14:09:55 +0800 Subject: [PATCH 18/20] fix(java): update json codegen class loading --- .../apache/fory/json/codegen/JsonCodegen.java | 23 ++++++++----------- .../apache/fory/json/ForyJsonTestModels.java | 3 +-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java index a98c7e6d58..df786e028e 100644 --- a/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java +++ b/java/fory-json/src/main/java/org/apache/fory/json/codegen/JsonCodegen.java @@ -40,7 +40,6 @@ import org.apache.fory.json.resolver.JsonTypeResolver; import org.apache.fory.json.writer.StringObjectWriter; import org.apache.fory.json.writer.Utf8ObjectWriter; -import org.apache.fory.platform.JdkVersion; import org.apache.fory.util.record.RecordUtils; public final class JsonCodegen { @@ -121,8 +120,7 @@ private Object compileWriter( new JsonGeneratedCodecBuilder(this, className, type, properties, utf8, true, false) .genCode(); try { - CompileUnit unit = new CompileUnit(PACKAGE, className, code, JsonCodegen.class); - Class writerClass = codeGenerator.compileAndLoad(unit, state -> state.lock.lock()); + Class writerClass = compileClass(className, code); Constructor constructor = writerClass.getDeclaredConstructor(JsonFieldInfo[].class, JsonCodec[].class); constructor.setAccessible(true); @@ -143,8 +141,7 @@ private Object compileReader( new JsonGeneratedCodecBuilder(this, className, type, properties, false, false, record) .genCode(); try { - CompileUnit unit = new CompileUnit(PACKAGE, className, code, JsonCodegen.class); - Class readerClass = codeGenerator.compileAndLoad(unit, state -> state.lock.lock()); + Class readerClass = compileClass(className, code); Constructor constructor = readerClass.getDeclaredConstructor( JsonFieldInfo[].class, JsonCodec[].class, BaseObjectCodec[].class); @@ -155,6 +152,12 @@ private Object compileReader( } } + private Class compileClass(String className, String code) throws ClassNotFoundException { + CompileUnit unit = new CompileUnit(PACKAGE, className, code); + ClassLoader classLoader = codeGenerator.compile(unit); + return classLoader.loadClass(PACKAGE + "." + className); + } + private static JsonCodec[] writeCodecs(JsonFieldInfo[] properties) { JsonCodec[] codecs = new JsonCodec[properties.length]; for (int i = 0; i < properties.length; i++) { @@ -230,15 +233,7 @@ private boolean canCompileRead(JsonFieldInfo property, boolean record) { } private boolean canCompile(Class type) { - return supportsHiddenNestmateLoading() - && CodeGenerator.sourcePublicAccessible(type) - && isVisible(type); - } - - private static boolean supportsHiddenNestmateLoading() { - // Generated JSON codecs are defined as hidden nestmates of JsonCodegen; JDK 8 must keep using - // the interpreter path because hidden classes are unavailable there. - return JdkVersion.MAJOR_VERSION >= 15; + return CodeGenerator.sourcePublicAccessible(type) && isVisible(type); } private boolean isVisible(Class type) { diff --git a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java index cdbd4b4d19..390eca4913 100644 --- a/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java +++ b/java/fory-json/src/test/java/org/apache/fory/json/ForyJsonTestModels.java @@ -50,7 +50,6 @@ import org.apache.fory.json.data.PublicFields; import org.apache.fory.json.data.TokenValues; import org.apache.fory.json.data.UnicodeMatrix; -import org.apache.fory.platform.JdkVersion; import org.apache.fory.reflect.FieldAccessor; import org.testng.SkipException; @@ -270,7 +269,7 @@ protected static void assertTextRoundTrip(ForyJson json, String text) { } protected static void assertGeneratedWhenSupported(ForyJson json, Class type) { - assertEquals(json.hasGeneratedWriter(type), JdkVersion.MAJOR_VERSION >= 15); + assertTrue(json.hasGeneratedWriter(type)); } protected static String repeat(char ch, int length) { From c87fe40f155eafe4edfdefcfaba6b813937f3f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 14:29:36 +0800 Subject: [PATCH 19/20] fix(java): keep generated serializer paths resolved --- .../apache/fory/resolver/TypeResolver.java | 10 ++-- java/fory-json/pom.xml | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java index ddb9091b4a..275ccf1d05 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java @@ -835,12 +835,16 @@ public final TypeInfo readTypeInfo(ReadContext readContext, TypeInfoHolder class } break; case Types.LIST: - return readListTypeInfo(readContext); + typeInfo = readListTypeInfo(readContext); + break; case Types.TIMESTAMP: - return readTimestampTypeInfo(readContext); + typeInfo = readTimestampTypeInfo(readContext); + break; default: - return Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); + typeInfo = Objects.requireNonNull(getInternalTypeInfoByTypeId(typeId)); } + // Do not return internal TypeInfo above before this check: lazy codegen can briefly clear a + // cached TypeInfo serializer while resolving recursive serializers. if (typeInfo.serializer == null) { typeInfo = ensureSerializerForTypeInfo(typeInfo); } diff --git a/java/fory-json/pom.xml b/java/fory-json/pom.xml index 1425c3e3f7..ebd6e8ba10 100644 --- a/java/fory-json/pom.xml +++ b/java/fory-json/pom.xml @@ -39,6 +39,8 @@ 8 8 ${basedir}/.. + ${project.build.directory}/jdk25-test-classes + ${project.basedir}/../fory-core/target/jdk25-test-classes @@ -153,5 +155,57 @@ + + jdk25-core-test-overlay + + [25,) + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + prepare-jdk25-test-classes + process-test-classes + + run + + + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${argLine} --add-opens=java.base/java.lang.invoke=ALL-UNNAMED + ${fory.json.jdk25.test.classes} + + + + + From 603b7fc6df0bd0e61e2ed57703ee9e15c88602cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=85=95=E7=99=BD?= Date: Mon, 29 Jun 2026 14:37:36 +0800 Subject: [PATCH 20/20] test(java): align jpms codegen final-field assertion --- .../apache/fory/integration_tests/JpmsFieldAccessorTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java index 5abb72c254..3f81ea9c70 100644 --- a/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java +++ b/integration_tests/jpms_tests/src/test/java/org/apache/fory/integration_tests/JpmsFieldAccessorTest.java @@ -130,9 +130,8 @@ public void testCodegenFinalFieldAccess() throws Exception { Assert.assertEquals(result.value(), 17); Class serializerClass = serializerClass(fory, PrivateFieldBean.class); - Assert.assertTrue((Boolean) Class.class.getMethod("isHidden").invoke(serializerClass)); - Assert.assertSame( - Class.class.getMethod("getNestHost").invoke(serializerClass), PrivateFieldBean.class); + // Runtime codegen uses the normal CodeGenerator classloader path. JDK25+ final-field access is + // guaranteed by the generated VarHandle field, not by hidden-nestmate source access. assertVarHandleField(serializerClass, "value"); }