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 fc9aee8cb5..ab56612e66 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 @@ -1712,6 +1712,10 @@ public boolean isCompatible() { return config.getCompatibleMode() == CompatibleMode.COMPATIBLE; } + public boolean isShareMeta() { + return shareMeta; + } + public boolean trackingRef() { return refTracking; } 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 992dfee285..bdc90cf0fe 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 @@ -69,6 +69,7 @@ import static org.apache.fory.type.TypeUtils.isPrimitive; import static org.apache.fory.util.Preconditions.checkArgument; +import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -113,13 +114,16 @@ import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.CompatibleSerializer; import org.apache.fory.serializer.EnumSerializer; +import org.apache.fory.serializer.FinalFieldReplaceResolveSerializer; import org.apache.fory.serializer.ObjectSerializer; import org.apache.fory.serializer.PrimitiveSerializers.LongSerializer; +import org.apache.fory.serializer.ReplaceResolveSerializer; import org.apache.fory.serializer.Serializer; import org.apache.fory.serializer.StringSerializer; import org.apache.fory.serializer.collection.CollectionFlags; import org.apache.fory.serializer.collection.CollectionLikeSerializer; import org.apache.fory.serializer.collection.MapLikeSerializer; +import org.apache.fory.type.Descriptor; import org.apache.fory.type.GenericType; import org.apache.fory.type.TypeUtils; import org.apache.fory.util.GraalvmSupport; @@ -144,6 +148,8 @@ public abstract class BaseObjectCodecBuilder extends CodecBuilder { TypeRef.of(CollectionLikeSerializer.class); private static final TypeRef MAP_SERIALIZER_TYPE = TypeRef.of(MapLikeSerializer.class); private static final TypeRef GENERIC_TYPE = TypeRef.of(GenericType.class); + private static final TypeRef FINAL_FIELD_SERIALIZER_TYPE = + TypeRef.of(FinalFieldReplaceResolveSerializer.class); protected final Fory fory; protected final Reference refResolverRef; @@ -394,6 +400,104 @@ protected Expression serializeFor( } } + protected Expression serializeField( + Expression fieldValue, Expression buffer, Descriptor descriptor) { + TypeRef typeRef = descriptor.getTypeRef(); + boolean nullable = descriptor.isNullable(); + + if (needWriteRef(typeRef)) { + return new If( + not(writeRefOrNull(buffer, fieldValue)), + serializeForNotNullForField(fieldValue, buffer, typeRef, null, false)); + } else { + // if typeToken is not final, ref tracking of subclass will be ignored too. + if (typeRef.isPrimitive()) { + return serializeForNotNullForField(fieldValue, buffer, typeRef, null, false); + } + if (nullable) { + Expression action = + new ListExpression( + new Invoke(buffer, "writeByte", Literal.ofByte(Fory.NOT_NULL_VALUE_FLAG)), + serializeForNotNullForField(fieldValue, buffer, typeRef, null, false)); + return new If( + eqNull(fieldValue), + new Invoke(buffer, "writeByte", Literal.ofByte(Fory.NULL_FLAG)), + action); + } else { + return serializeForNotNullForField(fieldValue, buffer, typeRef, null, false); + } + } + } + + private Expression serializeForNotNullForField( + Expression inputObject, + Expression buffer, + TypeRef typeRef, + Expression serializer, + boolean generateNewMethod) { + Class clz = getRawType(typeRef); + if (isPrimitive(clz) || isBoxed(clz)) { + return serializePrimitive(inputObject, buffer, clz); + } else { + if (clz == String.class) { + return fory.getStringSerializer().writeStringExpr(stringSerializerRef, buffer, inputObject); + } + Expression action; + if (useCollectionSerialization(typeRef)) { + action = + serializeForCollection(buffer, inputObject, typeRef, serializer, generateNewMethod); + } else if (useMapSerialization(typeRef)) { + action = serializeForMap(buffer, inputObject, typeRef, serializer, generateNewMethod); + } else { + action = serializeForNotNullObjectForField(inputObject, buffer, typeRef, serializer); + } + return action; + } + } + + private Expression serializePrimitive(Expression inputObject, Expression buffer, Class clz) { + // for primitive, inline call here to avoid java boxing, rather call corresponding serializer. + if (clz == byte.class || clz == Byte.class) { + return new Invoke(buffer, "writeByte", inputObject); + } else if (clz == boolean.class || clz == Boolean.class) { + return new Invoke(buffer, "writeBoolean", inputObject); + } else if (clz == char.class || clz == Character.class) { + return new Invoke(buffer, "writeChar", inputObject); + } else if (clz == short.class || clz == Short.class) { + return new Invoke(buffer, "writeInt16", inputObject); + } else if (clz == int.class || clz == Integer.class) { + String func = fory.compressInt() ? "writeVarInt32" : "writeInt32"; + return new Invoke(buffer, func, inputObject); + } else if (clz == long.class || clz == Long.class) { + return LongSerializer.writeInt64(buffer, inputObject, fory.longEncoding(), true); + } else if (clz == float.class || clz == Float.class) { + return new Invoke(buffer, "writeFloat32", inputObject); + } else if (clz == double.class || clz == Double.class) { + return new Invoke(buffer, "writeFloat64", inputObject); + } else { + throw new IllegalStateException("impossible"); + } + } + + private Expression serializeForNotNullObjectForField( + Expression inputObject, Expression buffer, TypeRef typeRef, Expression serializer) { + Class clz = getRawType(typeRef); + if (serializer != null) { + return new Invoke(serializer, writeMethodName, buffer, inputObject); + } + if (isMonomorphic(clz)) { + // Use descriptor to get the appropriate serializer + serializer = getSerializerForField(clz); + return new Invoke(serializer, writeMethodName, buffer, inputObject); + } else { + return writeForNotNullNonFinalObject(inputObject, buffer, typeRef); + } + } + + private Expression getSerializerForField(Class cls) { + return getOrCreateSerializer(cls, true); + } + protected Expression serializeForNullable( Expression inputObject, Expression buffer, TypeRef typeRef, boolean nullable) { return serializeForNullable(inputObject, buffer, typeRef, null, false, nullable); @@ -463,27 +567,7 @@ private Expression serializeForNotNull( boolean generateNewMethod) { Class clz = getRawType(typeRef); if (isPrimitive(clz) || isBoxed(clz)) { - // for primitive, inline call here to avoid java boxing, rather call corresponding serializer. - if (clz == byte.class || clz == Byte.class) { - return new Invoke(buffer, "writeByte", inputObject); - } else if (clz == boolean.class || clz == Boolean.class) { - return new Invoke(buffer, "writeBoolean", inputObject); - } else if (clz == char.class || clz == Character.class) { - return new Invoke(buffer, "writeChar", inputObject); - } else if (clz == short.class || clz == Short.class) { - return new Invoke(buffer, "writeInt16", inputObject); - } else if (clz == int.class || clz == Integer.class) { - String func = fory.compressInt() ? "writeVarInt32" : "writeInt32"; - return new Invoke(buffer, func, inputObject); - } else if (clz == long.class || clz == Long.class) { - return LongSerializer.writeInt64(buffer, inputObject, fory.longEncoding(), true); - } else if (clz == float.class || clz == Float.class) { - return new Invoke(buffer, "writeFloat32", inputObject); - } else if (clz == double.class || clz == Double.class) { - return new Invoke(buffer, "writeFloat64", inputObject); - } else { - throw new IllegalStateException("impossible"); - } + return serializePrimitive(inputObject, buffer, clz); } else { if (clz == String.class) { return fory.getStringSerializer().writeStringExpr(stringSerializerRef, buffer, inputObject); @@ -599,12 +683,26 @@ protected Expression writeClassInfo( * methods calls in most situations. */ protected Expression getOrCreateSerializer(Class cls) { + return getOrCreateSerializer(cls, false); + } + + private Expression getOrCreateSerializer(Class cls, boolean isField) { // Not need to check cls final, take collection writeSameTypeElements as an example. // Preconditions.checkArgument(isMonomorphic(cls), cls); Reference serializerRef = serializerMap.get(cls); if (serializerRef == null) { // potential recursive call for seq codec generation is handled in `getSerializerClass`. Class serializerClass = typeResolver(r -> r.getSerializerClass(cls)); + boolean finalClassAsFieldCondition = + !fory.isShareMeta() + && !fory.isCompatible() + && isField + && Modifier.isFinal(cls.getModifiers()) + && serializerClass == ReplaceResolveSerializer.class; + if (finalClassAsFieldCondition) { + serializerClass = FinalFieldReplaceResolveSerializer.class; + } + Preconditions.checkNotNull(serializerClass, "Unsupported for class " + cls); if (!ReflectionUtils.isPublic(serializerClass)) { // TODO(chaokunyang) add jdk17+ unexported class check. @@ -640,12 +738,18 @@ protected Expression getOrCreateSerializer(Class cls) { && !MapLikeSerializer.class.isAssignableFrom(serializerClass)) { serializerClass = MapLikeSerializer.class; } - TypeRef serializerTypeRef = TypeRef.of(serializerClass); Expression fieldTypeExpr = getClassExpr(cls); // Don't invoke `Serializer.newSerializer` here, since it(ex. ObjectSerializer) may set itself // as global serializer, which overwrite serializer updates in jit callback. - Expression newSerializerExpr = - inlineInvoke(typeResolverRef, "getRawSerializer", SERIALIZER_TYPE, fieldTypeExpr); + Expression newSerializerExpr; + if (finalClassAsFieldCondition) { + // Create serializer directly via static factory method + newSerializerExpr = + new Expression.NewInstance(FINAL_FIELD_SERIALIZER_TYPE, foryRef, fieldTypeExpr); + } else { + newSerializerExpr = + inlineInvoke(typeResolverRef, "getRawSerializer", SERIALIZER_TYPE, fieldTypeExpr); + } String name = ctx.newName(StringUtils.uncapitalize(serializerClass.getSimpleName())); // It's ok it jit already finished and this method return false, in such cases // `serializerClass` is already jit generated class. @@ -656,6 +760,7 @@ protected Expression getOrCreateSerializer(Class cls) { false, ctx.type(Serializer.class), name, cast(newSerializerExpr, SERIALIZER_TYPE)); serializerRef = new Reference(name, SERIALIZER_TYPE, false); } else { + TypeRef serializerTypeRef = TypeRef.of(serializerClass); ctx.addField( true, ctx.type(serializerClass), name, cast(newSerializerExpr, serializerTypeRef)); serializerRef = fieldRef(name, serializerTypeRef); @@ -1631,26 +1736,7 @@ protected Expression deserializeForNotNull( Expression buffer, TypeRef typeRef, Expression serializer, InvokeHint invokeHint) { Class cls = getRawType(typeRef); if (isPrimitive(cls) || isBoxed(cls)) { - // for primitive, inline call here to avoid java boxing, rather call corresponding serializer. - if (cls == byte.class || cls == Byte.class) { - return new Invoke(buffer, "readByte", PRIMITIVE_BYTE_TYPE); - } else if (cls == boolean.class || cls == Boolean.class) { - return new Invoke(buffer, "readBoolean", PRIMITIVE_BOOLEAN_TYPE); - } else if (cls == char.class || cls == Character.class) { - return readChar(buffer); - } else if (cls == short.class || cls == Short.class) { - return readInt16(buffer); - } else if (cls == int.class || cls == Integer.class) { - return fory.compressInt() ? readVarInt32(buffer) : readInt32(buffer); - } else if (cls == long.class || cls == Long.class) { - return LongSerializer.readInt64(buffer, fory.longEncoding()); - } else if (cls == float.class || cls == Float.class) { - return readFloat32(buffer); - } else if (cls == double.class || cls == Double.class) { - return readFloat64(buffer); - } else { - throw new IllegalStateException("impossible"); - } + return deserializePrimitive(buffer, cls); } else { if (cls == String.class) { return fory.getStringSerializer().readStringExpr(stringSerializerRef, buffer); @@ -1677,6 +1763,83 @@ protected Expression deserializeForNotNull( } } + protected Expression deserializeField( + Expression buffer, Descriptor descriptor, Function callback) { + TypeRef typeRef = descriptor.getTypeRef(); + boolean nullable = descriptor.isNullable(); + + if (needWriteRef(typeRef)) { + return readRef(buffer, callback, () -> deserializeForNotNullForField(buffer, typeRef, null)); + } else { + if (!nullable) { + Expression value = deserializeForNotNullForField(buffer, typeRef, null); + // Should put value expr ahead to avoid generated code in wrong scope. + return new ListExpression(value, callback.apply(value)); + } + return readNullable( + buffer, + typeRef, + callback, + () -> deserializeForNotNullForField(buffer, typeRef, null), + true); + } + } + + private Expression deserializeForNotNullForField( + Expression buffer, TypeRef typeRef, Expression serializer) { + Class cls = getRawType(typeRef); + if (isPrimitive(cls) || isBoxed(cls)) { + return deserializePrimitive(buffer, cls); + } else { + if (cls == String.class) { + return fory.getStringSerializer().readStringExpr(stringSerializerRef, buffer); + } + Expression obj; + if (useCollectionSerialization(typeRef)) { + obj = deserializeForCollection(buffer, typeRef, serializer, null); + } else if (useMapSerialization(typeRef)) { + obj = deserializeForMap(buffer, typeRef, serializer, null); + } else { + if (serializer != null) { + return read(serializer, buffer, OBJECT_TYPE); + } + if (isMonomorphic(cls)) { + // Use descriptor to get the appropriate serializer + serializer = getSerializerForField(cls); + Class returnType = + ReflectionUtils.getReturnType(getRawType(serializer.type()), readMethodName); + obj = read(serializer, buffer, TypeRef.of(returnType)); + } else { + obj = readForNotNullNonFinal(buffer, typeRef, serializer); + } + } + return obj; + } + } + + private Expression deserializePrimitive(Expression buffer, Class cls) { + // for primitive, inline call here to avoid java boxing + if (cls == byte.class || cls == Byte.class) { + return new Invoke(buffer, "readByte", PRIMITIVE_BYTE_TYPE); + } else if (cls == boolean.class || cls == Boolean.class) { + return new Invoke(buffer, "readBoolean", PRIMITIVE_BOOLEAN_TYPE); + } else if (cls == char.class || cls == Character.class) { + return readChar(buffer); + } else if (cls == short.class || cls == Short.class) { + return readInt16(buffer); + } else if (cls == int.class || cls == Integer.class) { + return fory.compressInt() ? readVarInt32(buffer) : readInt32(buffer); + } else if (cls == long.class || cls == Long.class) { + return LongSerializer.readInt64(buffer, fory.longEncoding()); + } else if (cls == float.class || cls == Float.class) { + return readFloat32(buffer); + } else if (cls == double.class || cls == Double.class) { + return readFloat64(buffer); + } else { + throw new IllegalStateException("impossible"); + } + } + protected Expression read(Expression serializer, Expression buffer, TypeRef returnType) { Class type = returnType.getRawType(); Expression read = new Invoke(serializer, readMethodName, returnType, buffer); 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 c472aee6b7..81fecc1c78 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 @@ -207,9 +207,7 @@ private Expression serializeGroup( // `bean` will be replaced by `Reference` to cut-off expr dependency. Expression fieldValue = getFieldValue(bean, d); walkPath.add(d.getDeclaringClass() + d.getName()); - boolean nullable = d.isNullable(); - Expression fieldExpr = - serializeForNullable(fieldValue, buffer, d.getTypeRef(), nullable); + Expression fieldExpr = serializeField(fieldValue, buffer, d); walkPath.removeLast(); groupExpressions.add(fieldExpr); } @@ -555,17 +553,15 @@ protected Expression deserializeGroup( for (Descriptor d : group) { ExpressionVisitor.ExprHolder exprHolder = ExpressionVisitor.ExprHolder.of("bean", bean); walkPath.add(d.getDeclaringClass() + d.getName()); - boolean nullable = d.isNullable(); Expression action = - deserializeForNullable( + deserializeField( buffer, - d.getTypeRef(), + d, // `bean` will be replaced by `Reference` to cut-off expr // dependency. expr -> setFieldValue( - exprHolder.get("bean"), d, tryInlineCast(expr, d.getTypeRef())), - nullable); + exprHolder.get("bean"), d, tryInlineCast(expr, d.getTypeRef()))); walkPath.removeLast(); groupExpressions.add(action); } 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 77a66cdfcd..e6d38b44ba 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 @@ -110,6 +110,7 @@ import org.apache.fory.serializer.CompatibleSerializer; import org.apache.fory.serializer.EnumSerializer; import org.apache.fory.serializer.ExternalizableSerializer; +import org.apache.fory.serializer.FinalFieldReplaceResolveSerializer; import org.apache.fory.serializer.ForyCopyableSerializer; import org.apache.fory.serializer.JavaSerializer; import org.apache.fory.serializer.JdkProxySerializer; @@ -789,8 +790,8 @@ public void clearSerializer(Class cls) { } } - /** Ass serializer for specified class. */ - private void addSerializer(Class type, Serializer serializer) { + /** Add serializer for specified class. */ + public void addSerializer(Class type, Serializer serializer) { Preconditions.checkNotNull(serializer); // 1. Try to get ClassInfo from `registeredId2ClassInfo` and // `classInfoMap` or create a new `ClassInfo`. @@ -801,7 +802,8 @@ private void addSerializer(Class type, Serializer serializer) { if (registered) { classInfo = registeredId2ClassInfo[classId]; } else { - if (serializer instanceof ReplaceResolveSerializer) { + if (serializer instanceof ReplaceResolveSerializer + && !(serializer instanceof FinalFieldReplaceResolveSerializer)) { classId = REPLACE_STUB_ID; } else { classId = NO_CLASS_ID; diff --git a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java index b5451869ff..284b31803d 100644 --- a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java +++ b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java @@ -706,6 +706,7 @@ public Serializer getSerializer(Class cls) { return (Serializer) getClassInfo(cls).serializer; } + @Override public Serializer getRawSerializer(Class cls) { return getClassInfo(cls).serializer; } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java index 930e8b67ed..7ecde09930 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java @@ -1035,6 +1035,12 @@ private FinalTypeField(Fory fory, Descriptor d) { classInfo = null; } else { classInfo = SerializationUtils.getClassInfo(fory, typeRef.getRawType()); + if (!fory.isShareMeta() + && !fory.isCompatible() + && classInfo.getSerializer() instanceof ReplaceResolveSerializer) { + // overwrite replace resolve serializer for final field + classInfo.setSerializer(new FinalFieldReplaceResolveSerializer(fory, classInfo.getCls())); + } } } } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java new file mode 100644 index 0000000000..2a3a99fc7f --- /dev/null +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializer.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.serializer; + +import org.apache.fory.Fory; +import org.apache.fory.config.CompatibleMode; +import org.apache.fory.memory.MemoryBuffer; + +/** + * Serializer for class which: - has jdk `writeReplace`/`readResolve` method defined, - is a final + * class. Main advantage of this serializer is that it does not write class name to the payload. + * NOTE: this serializer is used only with {@link CompatibleMode#SCHEMA_CONSISTENT} mode. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +public class FinalFieldReplaceResolveSerializer extends ReplaceResolveSerializer { + + public FinalFieldReplaceResolveSerializer(Fory fory, Class type) { + // the serializer does not write class info + // and does not set itself for the provided class + // see checks in ReplaceResolveSerializer constructor + super(fory, type, true, false); + } + + @Override + protected void writeObject( + MemoryBuffer buffer, Object value, MethodInfoCache jdkMethodInfoCache) { + jdkMethodInfoCache.objectSerializer.write(buffer, value); + } + + @Override + protected Object readObject(MemoryBuffer buffer) { + MethodInfoCache jdkMethodInfoCache = getMethodInfoCache(type); + Object o = jdkMethodInfoCache.objectSerializer.read(buffer); + ReplaceResolveInfo replaceResolveInfo = jdkMethodInfoCache.info; + if (replaceResolveInfo.readResolveMethod == null) { + return o; + } + return replaceResolveInfo.readResolve(o); + } +} diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java index db2ada5b94..3d5bd29078 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ReplaceResolveSerializer.java @@ -53,15 +53,15 @@ public class ReplaceResolveSerializer extends Serializer { */ public static class ReplaceStub {} - private static final byte ORIGINAL = 0; - private static final byte REPLACED_NEW_TYPE = 1; - private static final byte REPLACED_SAME_TYPE = 2; + protected static final byte ORIGINAL = 0; + protected static final byte REPLACED_NEW_TYPE = 1; + protected static final byte REPLACED_SAME_TYPE = 2; // Extract Method Info to cache for graalvm build time lambda generation and avoid // generate function repeatedly too. - private static class ReplaceResolveInfo { - private final Method writeReplaceMethod; - private final Method readResolveMethod; + protected static class ReplaceResolveInfo { + protected final Method writeReplaceMethod; + protected final Method readResolveMethod; private final Function writeReplaceFunc; private final Function readResolveFunc; @@ -162,10 +162,10 @@ protected ReplaceResolveInfo computeValue(Class type) { } }; - private static class MethodInfoCache { - private final ReplaceResolveInfo info; + protected static class MethodInfoCache { + protected final ReplaceResolveInfo info; - private Serializer objectSerializer; + protected Serializer objectSerializer; public MethodInfoCache(ReplaceResolveInfo info) { this.info = info; @@ -210,26 +210,38 @@ private static Serializer createDataSerializer( return serializer; } - private final RefResolver refResolver; - private final ClassResolver classResolver; - private final MethodInfoCache jdkMethodInfoWriteCache; - private final ClassInfo writeClassInfo; - private final Map, MethodInfoCache> classClassInfoHolderMap = new HashMap<>(); + protected final RefResolver refResolver; + protected final ClassResolver classResolver; + protected final MethodInfoCache jdkMethodInfoWriteCache; + protected final ClassInfo writeClassInfo; + protected final Map, MethodInfoCache> classClassInfoHolderMap = new HashMap<>(); public ReplaceResolveSerializer(Fory fory, Class type) { + this(fory, type, false, true); + } + + public ReplaceResolveSerializer( + Fory fory, Class type, boolean isFinalField, boolean setSerializer) { super(fory, type); refResolver = fory.getRefResolver(); classResolver = fory.getClassResolver(); - // `setSerializer` before `newJDKMethodInfoCache` since it query classinfo from `classResolver`, - // which create serializer in turn. - // ReplaceResolveSerializer is used as data serializer for ImmutableList/Map, - // which serializer is already set. - classResolver.setSerializerIfAbsent(type, this); + if (setSerializer) { + // `setSerializer` before `newJDKMethodInfoCache` since it query classinfo from + // `classResolver`, + // which create serializer in turn. + // ReplaceResolveSerializer is used as data serializer for ImmutableList/Map, + // which serializer is already set. + classResolver.setSerializerIfAbsent(type, this); + } if (type != ReplaceStub.class) { jdkMethodInfoWriteCache = newJDKMethodInfoCache(type, fory); classClassInfoHolderMap.put(type, jdkMethodInfoWriteCache); - // FIXME new classinfo may miss serializer update in async compilation mode. - writeClassInfo = classResolver.newClassInfo(type, this, ClassResolver.NO_CLASS_ID); + if (isFinalField) { + writeClassInfo = null; + } else { + // FIXME new classinfo may miss serializer update in async compilation mode. + writeClassInfo = classResolver.newClassInfo(type, this, ClassResolver.NO_CLASS_ID); + } } else { jdkMethodInfoWriteCache = null; writeClassInfo = null; @@ -280,7 +292,8 @@ public void write(MemoryBuffer buffer, Object value) { } } - private void writeObject(MemoryBuffer buffer, Object value, MethodInfoCache jdkMethodInfoCache) { + protected void writeObject( + MemoryBuffer buffer, Object value, MethodInfoCache jdkMethodInfoCache) { classResolver.writeClassInternal(buffer, writeClassInfo); jdkMethodInfoCache.objectSerializer.write(buffer, value); } @@ -319,7 +332,7 @@ public Object read(MemoryBuffer buffer) { } } - private Object readObject(MemoryBuffer buffer) { + protected Object readObject(MemoryBuffer buffer) { Class cls = classResolver.readClassInternal(buffer); MethodInfoCache jdkMethodInfoCache = getMethodInfoCache(cls); Object o = jdkMethodInfoCache.objectSerializer.read(buffer); @@ -350,7 +363,7 @@ public Object copy(Object originObj) { return newObj; } - private MethodInfoCache getMethodInfoCache(Class cls) { + protected MethodInfoCache getMethodInfoCache(Class cls) { MethodInfoCache jdkMethodInfoCache = classClassInfoHolderMap.get(cls); if (jdkMethodInfoCache == null) { jdkMethodInfoCache = newJDKMethodInfoCache(cls, fory); diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java new file mode 100644 index 0000000000..6b5dd80cef --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/FinalFieldReplaceResolveSerializerTest.java @@ -0,0 +1,514 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fory.serializer; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.ImmutableIntArray; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.apache.fory.Fory; +import org.apache.fory.ForyTestBase; +import org.apache.fory.config.CompatibleMode; +import org.apache.fory.config.Language; +import org.testng.annotations.Ignore; +import org.testng.annotations.Test; + +/** + * Test class for FieldReplaceResolveSerializer. This serializer is used for final fields that have + * writeReplace/readResolve methods. + */ +public class FinalFieldReplaceResolveSerializerTest extends ForyTestBase { + + @Data + public static class CustomReplaceClass1 implements Serializable { + public transient String name; + + public CustomReplaceClass1(String name) { + this.name = name; + } + + private Object writeReplace() { + return new Replaced(name); + } + + private static final class Replaced implements Serializable { + public String name; + + public Replaced(String name) { + this.name = name; + } + + private Object readResolve() { + return new CustomReplaceClass1(name); + } + } + } + + public static class CustomReplaceClass3 implements Serializable { + public Object ref; + + private Object writeReplace() { + return ref; + } + + private Object readResolve() { + return ref; + } + } + + /** Container class with final field that uses writeReplace/readResolve */ + @Data + @AllArgsConstructor + public static class ContainerWithFinalReplaceField implements Serializable { + private final CustomReplaceClass1 finalField; + } + + @Data + @AllArgsConstructor + public static class ContainerWithNonFinalImmutableIntArray implements Serializable { + private ImmutableIntArray nonFinalIntArray; + } + + @Data + @AllArgsConstructor + public static class ContainerWithFinalReplaceField2 implements Serializable { + private final CustomReplaceClass2 finalField; + } + + @Data + @AllArgsConstructor + @EqualsAndHashCode + public static class ContainerWithFinalReplaceField3 implements Serializable { + private final CustomReplaceClass3 finalField; + } + + @Data + @AllArgsConstructor + public static class ComplexContainerWithMultipleFinalFields implements Serializable { + private final CustomReplaceClass1 field1; + private final ImmutableList field2; + private final CustomReplaceClass2 field3; + private final ImmutableMap field4; + } + + @Data + @AllArgsConstructor + public static class ContainerWithFinalImmutableIntArray implements Serializable { + private final ImmutableIntArray intArray; + } + + @Data + @AllArgsConstructor + public static class ContainerWithFinalImmutableMap implements Serializable { + private final ImmutableMap finalMap; + } + + @Data + public static class CustomReplaceClass2 implements Serializable { + public boolean copy; + public transient int age; + + public CustomReplaceClass2(boolean copy, int age) { + this.copy = copy; + this.age = age; + } + + Object writeReplace() { + if (age > 5) { + return new Object[] {copy, age}; + } else { + if (copy) { + return new CustomReplaceClass2(copy, age); + } else { + return this; + } + } + } + + Object readResolve() { + if (copy) { + return new CustomReplaceClass2(copy, age); + } + return this; + } + } + + @Test(dataProvider = "referenceTrackingConfig") + public void testFinalFieldReplace(boolean referenceTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(referenceTracking) + .build(); + CustomReplaceClass1 o1 = new CustomReplaceClass1("abc"); + ContainerWithFinalReplaceField container = new ContainerWithFinalReplaceField(o1); + serDeCheck(fory, container); + ContainerWithFinalReplaceField deserialized = serDe(fory, container); + assertEquals(deserialized.getFinalField().getName(), "abc"); + } + + @Test(dataProvider = "foryCopyConfig") + public void testFinalFieldReplaceCopy(Fory fory) { + CustomReplaceClass1 o1 = new CustomReplaceClass1("abc"); + ContainerWithFinalReplaceField container = new ContainerWithFinalReplaceField(o1); + copyCheck(fory, container); + ContainerWithFinalReplaceField copy = fory.copy(container); + assertEquals(copy.getFinalField().getName(), "abc"); + } + + @Test(dataProvider = "referenceTrackingConfig") + public void testFinalFieldWriteReplaceCircularClass(boolean referenceTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(referenceTracking) + .build(); + for (Object inner : + new Object[] { + new CustomReplaceClass2(false, 2), new CustomReplaceClass2(true, 2), + }) { + ContainerWithFinalReplaceField2 container = + new ContainerWithFinalReplaceField2((CustomReplaceClass2) inner); + serDeCheck(fory, container); + } + } + + @Test(dataProvider = "foryCopyConfig") + public void testFinalFieldCopyReplaceCircularClass(Fory fory) { + for (Object inner : + new Object[] { + new CustomReplaceClass2(false, 2), new CustomReplaceClass2(true, 2), + }) { + ContainerWithFinalReplaceField2 container = + new ContainerWithFinalReplaceField2((CustomReplaceClass2) inner); + copyCheck(fory, container); + } + } + + @Test + public void testFinalFieldWriteReplaceSameClassCircularRef() { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(true) + .build(); + { + CustomReplaceClass3 o1 = new CustomReplaceClass3(); + o1.ref = o1; + ContainerWithFinalReplaceField3 container = new ContainerWithFinalReplaceField3(o1); + ContainerWithFinalReplaceField3 o3 = serDe(fory, container); + assertSame(o3.getFinalField().ref, o3.getFinalField()); + } + { + CustomReplaceClass3 o1 = new CustomReplaceClass3(); + CustomReplaceClass3 o2 = new CustomReplaceClass3(); + o1.ref = o2; + o2.ref = o1; + ContainerWithFinalReplaceField3 container = new ContainerWithFinalReplaceField3(o1); + ContainerWithFinalReplaceField3 newContainer = serDe(fory, container); + CustomReplaceClass3 newObj1 = newContainer.getFinalField(); + assertSame(newObj1.ref, newObj1); + assertSame(((CustomReplaceClass3) newObj1.ref).ref, newObj1); + } + } + + @Test(dataProvider = "foryCopyConfig") + public void testFinalFieldWriteReplaceSameClassCircularRefCopy(Fory fory) { + { + CustomReplaceClass3 o1 = new CustomReplaceClass3(); + o1.ref = o1; + ContainerWithFinalReplaceField3 container = new ContainerWithFinalReplaceField3(o1); + ContainerWithFinalReplaceField3 copy = fory.copy(container); + assertSame(copy.getFinalField(), copy.getFinalField().ref); + } + { + CustomReplaceClass3 o1 = new CustomReplaceClass3(); + CustomReplaceClass3 o2 = new CustomReplaceClass3(); + o1.ref = o2; + o2.ref = o1; + ContainerWithFinalReplaceField3 container = new ContainerWithFinalReplaceField3(o1); + ContainerWithFinalReplaceField3 copy = fory.copy(container); + CustomReplaceClass3 newObj1 = copy.getFinalField(); + assertNotSame(newObj1.ref, o2); + } + } + + @Test(dataProvider = "referenceTrackingConfig") + public void testFinalFieldImmutableList(boolean referenceTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(referenceTracking) + .build(); + ImmutableIntArray list1 = ImmutableIntArray.of(1, 2, 3, 4); + ContainerWithFinalImmutableIntArray container = new ContainerWithFinalImmutableIntArray(list1); + serDeCheck(fory, container); + ContainerWithFinalImmutableIntArray deserialized = serDe(fory, container); + assertEquals(deserialized.getIntArray(), list1); + } + + @Test(dataProvider = "foryCopyConfig") + public void testFinalFieldImmutableListCopy(Fory fory) { + ImmutableIntArray list1 = ImmutableIntArray.of(1, 2, 3, 4); + ContainerWithFinalImmutableIntArray container = new ContainerWithFinalImmutableIntArray(list1); + copyCheck(fory, container); + } + + @Test(dataProvider = "referenceTrackingConfig") + public void testFinalFieldImmutableMap(boolean referenceTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(referenceTracking) + .build(); + ImmutableMap map1 = ImmutableMap.of("k1", 1, "k2", 2); + ContainerWithFinalImmutableMap container = new ContainerWithFinalImmutableMap(map1); + serDeCheck(fory, container); + ContainerWithFinalImmutableMap deserialized = serDe(fory, container); + assertEquals(deserialized.getFinalMap(), map1); + } + + @Test(dataProvider = "foryCopyConfig") + public void testFinalFieldImmutableMapCopy(Fory fory) { + ImmutableMap map1 = ImmutableMap.of("k1", 1, "k2", 2); + ContainerWithFinalImmutableMap container = new ContainerWithFinalImmutableMap(map1); + copyCheck(fory, container); + } + + @Test(dataProvider = "referenceTrackingConfig") + public void testMultipleFinalFieldsWithReplace(boolean referenceTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(referenceTracking) + .build(); + ComplexContainerWithMultipleFinalFields container = + new ComplexContainerWithMultipleFinalFields( + new CustomReplaceClass1("test"), + ImmutableList.of("a", "b", "c"), + new CustomReplaceClass2(true, 3), + ImmutableMap.of("k1", 1, "k2", 2)); + serDeCheck(fory, container); + ComplexContainerWithMultipleFinalFields deserialized = serDe(fory, container); + assertEquals(deserialized.getField1().getName(), "test"); + assertEquals(deserialized.getField2(), ImmutableList.of("a", "b", "c")); + assertEquals(deserialized.getField4(), ImmutableMap.of("k1", 1, "k2", 2)); + } + + @Test(dataProvider = "foryCopyConfig") + public void testMultipleFinalFieldsWithReplaceCopy(Fory fory) { + ComplexContainerWithMultipleFinalFields container = + new ComplexContainerWithMultipleFinalFields( + new CustomReplaceClass1("test"), + ImmutableList.of("a", "b", "c"), + new CustomReplaceClass2(true, 3), + ImmutableMap.of("k1", 1, "k2", 2)); + copyCheck(fory, container); + } + + /** + * Verify that the writeClassInfo field is null for FieldReplaceResolveSerializer. This is what + * prevents class names from being written. + */ + @Test + public void testWriteClassInfoIsNull() throws Exception { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(false) + .build(); + + // Get the serializer for a final field with writeReplace + // ImmutableList uses writeReplace internally + ImmutableIntArray list = ImmutableIntArray.of(1, 2, 3); + Class listClass = list.getClass(); + + // Create FieldReplaceResolveSerializer as it would be used for a final field + FinalFieldReplaceResolveSerializer finalFieldSerializer = + new FinalFieldReplaceResolveSerializer(fory, listClass); + + // Use reflection to check that writeClassInfo is null + java.lang.reflect.Field writeClassInfoField = + ReplaceResolveSerializer.class.getDeclaredField("writeClassInfo"); + writeClassInfoField.setAccessible(true); + Object writeClassInfo = writeClassInfoField.get(finalFieldSerializer); + + // For FieldReplaceResolveSerializer, writeClassInfo should be null + assertNull( + writeClassInfo, + "FieldReplaceResolveSerializer should have writeClassInfo=null to avoid writing class names"); + + // Compare with ReplaceResolveSerializer (non-final) + ReplaceResolveSerializer nonFinalFieldSerializer = + new ReplaceResolveSerializer(fory, listClass, false, true); + Object writeClassInfoNonFinal = writeClassInfoField.get(nonFinalFieldSerializer); + + // For ReplaceResolveSerializer (non-final), writeClassInfo should NOT be null + assertNotNull( + writeClassInfoNonFinal, + "ReplaceResolveSerializer (non-final) should have writeClassInfo set to write class names"); + } + + @Test(dataProvider = "enableCodegen") + public void testNoClassNameWrittenForFinalField(boolean codegen) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withCodegen(codegen) + .withRefTracking(false) + .build(); + + // Create a container with a final ImmutableList field + ContainerWithFinalImmutableIntArray containerFinal = + new ContainerWithFinalImmutableIntArray(ImmutableIntArray.of(1, 2, 3)); + byte[] bytesFinal = fory.serialize(containerFinal); + byte[] bytesFinal2 = fory.serialize(containerFinal); + assertEquals(bytesFinal, bytesFinal2); + assertEquals(bytesFinal.length, 108); + + // Create a container with a non-final ImmutableList field for comparison + ContainerWithNonFinalImmutableIntArray containerNonFinal = + new ContainerWithNonFinalImmutableIntArray(ImmutableIntArray.of(1, 2, 3)); + byte[] bytesNonFinal = fory.serialize(containerNonFinal); + + // The final field version should use fewer bytes because it doesn't write class name + System.out.println(bytesFinal.length + " " + bytesNonFinal.length); + assertTrue( + bytesFinal.length < bytesNonFinal.length, + String.format( + "Final field serialization (%d bytes) should be smaller than non-final (%d bytes) " + + "because class name is not written", + bytesFinal.length, bytesNonFinal.length)); + + // Verify deserialization still works correctly + ContainerWithFinalImmutableIntArray deserialized = + (ContainerWithFinalImmutableIntArray) fory.deserialize(bytesFinal); + assertEquals(deserialized.getIntArray(), ImmutableIntArray.of(1, 2, 3)); + } + + /** + * Test that verifies the overridden writeObject method in FieldReplaceResolveSerializer does NOT + * call classResolver.writeClassInternal(). + */ + @Test + public void testWriteObjectSkipsClassNameWrite() { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withRefTracking(false) + .build(); + + // Serialize using a final field container + ContainerWithFinalImmutableIntArray container = + new ContainerWithFinalImmutableIntArray(ImmutableIntArray.of(1, 2, 3, 4, 5)); + + byte[] bytes = fory.serialize(container); + + // Verify it can be deserialized correctly + ContainerWithFinalImmutableIntArray deserialized = + (ContainerWithFinalImmutableIntArray) fory.deserialize(bytes); + assertEquals(deserialized.getIntArray(), ImmutableIntArray.of(1, 2, 3, 4, 5)); + + // The key point: FieldReplaceResolveSerializer.writeObject() directly calls + // jdkMethodInfoCache.objectSerializer.write(buffer, value) + // without calling classResolver.writeClassInternal(buffer, writeClassInfo) + } + + // TODO fix: bug with CompatibleMode and final field replace/resolve on main branch + @Ignore + @Test(dataProvider = "referenceTrackingConfig") + public void testFinalFieldReplaceWithCompatibleModeFinalClass(boolean refTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(refTracking) + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .build(); + + ImmutableIntArray list1 = ImmutableIntArray.of(10, 20, 30); + ContainerWithFinalImmutableIntArray containerList = + new ContainerWithFinalImmutableIntArray(list1); + serDeCheck(fory, containerList); + ContainerWithFinalImmutableIntArray deserializedList = serDe(fory, containerList); + assertEquals(deserializedList.getIntArray(), list1); + } + + /** + * Test that final fields with writeReplace/readResolve work correctly with + * CompatibleMode.COMPATIBLE which uses MetaSharedSerializer instead of ObjectSerializer. + */ + @Test(dataProvider = "referenceTrackingConfig") + public void testFinalFieldReplaceWithCompatibleMode(boolean refTracking) { + Fory fory = + Fory.builder() + .withLanguage(Language.JAVA) + .requireClassRegistration(false) + .withCodegen(false) + .withRefTracking(refTracking) + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .build(); + + // Test CustomReplaceClass1 with final field + CustomReplaceClass1 o1 = new CustomReplaceClass1("test_compatible"); + ContainerWithFinalReplaceField container = new ContainerWithFinalReplaceField(o1); + serDeCheck(fory, container); + ContainerWithFinalReplaceField deserialized = serDe(fory, container); + assertEquals(deserialized.getFinalField().getName(), "test_compatible"); + + ImmutableMap map1 = ImmutableMap.of("a", 100, "b", 200); + ContainerWithFinalImmutableMap containerMap = new ContainerWithFinalImmutableMap(map1); + serDeCheck(fory, containerMap); + ContainerWithFinalImmutableMap deserializedMap = serDe(fory, containerMap); + assertEquals(deserializedMap.getFinalMap(), map1); + + ComplexContainerWithMultipleFinalFields complexContainer = + new ComplexContainerWithMultipleFinalFields( + new CustomReplaceClass1("complex"), + ImmutableList.of("x", "y", "z"), + new CustomReplaceClass2(true, 5), + ImmutableMap.of("key1", 111, "key2", 222)); + serDeCheck(fory, complexContainer); + ComplexContainerWithMultipleFinalFields deserializedComplex = serDe(fory, complexContainer); + assertEquals(deserializedComplex.getField1().getName(), "complex"); + assertEquals(deserializedComplex.getField2(), ImmutableList.of("x", "y", "z")); + assertEquals(deserializedComplex.getField4(), ImmutableMap.of("key1", 111, "key2", 222)); + } +}