This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new ae1a7e07c feat(java): implement FinalFieldReplaceResolveSerializer for 
final fields with writeReplace/readResolve methods (#2917)
ae1a7e07c is described below

commit ae1a7e07c6d4ffae7e0ea16459d0bc0c2857ca45
Author: Mikhail Chernyakov <[email protected]>
AuthorDate: Tue Dec 2 12:38:08 2025 +0100

    feat(java): implement FinalFieldReplaceResolveSerializer for final fields 
with writeReplace/readResolve methods (#2917)
    
    <!--
    **Thanks for contributing to Apache Fory™.**
    
    **If this is your first time opening a PR on fory, you can refer to
    
[CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).**
    
    Contribution Checklist
    
    - The **Apache Fory™** community has requirements on the naming of pr
    titles. You can also find instructions in
    [CONTRIBUTING.md](https://github.com/apache/fory/blob/main/CONTRIBUTING.md).
    
    - Apache Fory™ has a strong focus on performance. If the PR you submit
    will have an impact on performance, please benchmark it first and
    provide the benchmark result here.
    -->
    
    ## Why?
    
    Based on the discussion https://github.com/apache/fory/discussions/2786.
    We can optimize the payload in cases where we have final fields in some
    Object.
    
    re-created, closed PR https://github.com/apache/fory/pull/2904
    
    ## What does this PR do?
    
    Introduce `FinalFieldReplaceResolver` which does not write the classname
    into the payload.
    
    ## Related issues
    
    <!--
    Is there any related issue? If this PR closes them you say say
    fix/closes:
    
    - #xxxx0
    - #xxxx1
    - Fixes #xxxx2
    -->
    
    ## Does this PR introduce any user-facing change?
    
    <!--
    If any user-facing interface changes, please [open an
    issue](https://github.com/apache/fory/issues/new/choose) describing the
    need to do so and update the document if necessary.
    
    Delete section if not applicable.
    -->
    
    - [x] Does this PR introduce any public API change?
    - [x] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    <!--
    When the PR has an impact on performance (if you don't know whether the
    PR will have an impact on performance, you can submit the PR first, and
    if it will have impact on performance, the code reviewer will explain
    it), be sure to attach a benchmark data here.
    
    Delete section if not applicable.
    -->
    
    ---------
    
    Co-authored-by: Shawn Yang <[email protected]>
---
 .../src/main/java/org/apache/fory/Fory.java        |   4 +
 .../fory/builder/BaseObjectCodecBuilder.java       | 251 ++++++++--
 .../apache/fory/builder/ObjectCodecBuilder.java    |  12 +-
 .../org/apache/fory/resolver/ClassResolver.java    |   8 +-
 .../org/apache/fory/resolver/XtypeResolver.java    |   1 +
 .../fory/serializer/AbstractObjectSerializer.java  |   6 +
 .../FinalFieldReplaceResolveSerializer.java        |  57 +++
 .../fory/serializer/ReplaceResolveSerializer.java  |  61 ++-
 .../FinalFieldReplaceResolveSerializerTest.java    | 514 +++++++++++++++++++++
 9 files changed, 835 insertions(+), 79 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 fc9aee8cb..ab56612e6 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 final class Fory implements BaseFory {
     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 992dfee28..bdc90cf0f 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.isBoxed;
 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.RefResolver;
 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 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     }
   }
 
+  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 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
       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 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
    * 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<? extends Serializer> 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 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
           && !MapLikeSerializer.class.isAssignableFrom(serializerClass)) {
         serializerClass = MapLikeSerializer.class;
       }
-      TypeRef<? extends Serializer> 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 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
             false, ctx.type(Serializer.class), name, cast(newSerializerExpr, 
SERIALIZER_TYPE));
         serializerRef = new Reference(name, SERIALIZER_TYPE, false);
       } else {
+        TypeRef<? extends Serializer> serializerTypeRef = 
TypeRef.of(serializerClass);
         ctx.addField(
             true, ctx.type(serializerClass), name, cast(newSerializerExpr, 
serializerTypeRef));
         serializerRef = fieldRef(name, serializerTypeRef);
@@ -1631,26 +1736,7 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
       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 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     }
   }
 
+  protected Expression deserializeField(
+      Expression buffer, Descriptor descriptor, Function<Expression, 
Expression> 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 c472aee6b..81fecc1c7 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 @@ public class ObjectCodecBuilder extends 
BaseObjectCodecBuilder {
             // `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 @@ public class ObjectCodecBuilder extends 
BaseObjectCodecBuilder {
           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 77a66cdfc..e6d38b44b 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.CodegenSerializer.LazyInitBeanSerializer;
 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 class ClassResolver extends TypeResolver {
     }
   }
 
-  /** 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 @@ public class ClassResolver extends TypeResolver {
     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 b5451869f..284b31803 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 class XtypeResolver extends TypeResolver {
     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 930e8b67e..7ecde0993 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 @@ public abstract class AbstractObjectSerializer<T> 
extends Serializer<T> {
         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 000000000..2a3a99fc7
--- /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 db2ada5b9..3d5bd2907 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 @@ public class ReplaceResolveSerializer extends Serializer {
         }
       };
 
-  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 @@ public class ReplaceResolveSerializer extends Serializer {
     return serializer;
   }
 
-  private final RefResolver refResolver;
-  private final ClassResolver classResolver;
-  private final MethodInfoCache jdkMethodInfoWriteCache;
-  private final ClassInfo writeClassInfo;
-  private final Map<Class<?>, MethodInfoCache> classClassInfoHolderMap = new 
HashMap<>();
+  protected final RefResolver refResolver;
+  protected final ClassResolver classResolver;
+  protected final MethodInfoCache jdkMethodInfoWriteCache;
+  protected final ClassInfo writeClassInfo;
+  protected final Map<Class<?>, 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 class ReplaceResolveSerializer extends Serializer {
     }
   }
 
-  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 class ReplaceResolveSerializer extends Serializer {
     }
   }
 
-  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 class ReplaceResolveSerializer extends Serializer {
     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 000000000..6b5dd80ce
--- /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<String> field2;
+    private final CustomReplaceClass2 field3;
+    private final ImmutableMap<String, Integer> field4;
+  }
+
+  @Data
+  @AllArgsConstructor
+  public static class ContainerWithFinalImmutableIntArray implements 
Serializable {
+    private final ImmutableIntArray intArray;
+  }
+
+  @Data
+  @AllArgsConstructor
+  public static class ContainerWithFinalImmutableMap implements Serializable {
+    private final ImmutableMap<String, Integer> 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<String, Integer> 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<String, Integer> 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<String, Integer> 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));
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to