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 4fb6685d9 fix(java): finalize codegen config hash on first use (#3495)
4fb6685d9 is described below

commit 4fb6685d9f76b377875b4c2f96c17c8935d13972
Author: Shawn Yang <[email protected]>
AuthorDate: Fri Mar 20 18:02:48 2026 +0800

    fix(java): finalize codegen config hash on first use (#3495)
    
    ## Why?
    
    The hash-based isolation must stay, but it cannot remain mutable once
    codegen or GraalVM-native caches start using it. If any first-use path
    consumes the hash before registration is complete, a later registration
    can still split one `Fory` instance across multiple namespaces.
    
    ## What does this PR do?
    
    - move config-hash lifecycle ownership into `TypeResolver.ExtRegistry`
    - remove the separate stable-hash API and keep a single
    `getConfigHash()` entry point
    - update the hash at the resolver registration points so it reflects the
    final registered state instead of facade call paths
    - finalize the hash on first `getConfigHash()`, first
    `ensureSerializersCompiled()`, and the first serialize, deserialize, or
    copy path
    - reject any later class, serializer, union, or serializer-factory
    registration after the hash is finalized
    - defer GraalVM native-image class registration until the finalized hash
    is materialized
    - add focused regression tests for freeze-after-hash,
    freeze-after-serialize, freeze-after-`ensureSerializersCompiled()`, and
    the previously missed registration paths
    
    ## Verification
    
    - `cd java && ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T4 -pl fory-core
    
-Dtest=org.apache.fory.xlang.RegisterTest,org.apache.fory.serializer.TimeSerializersTest,org.apache.fory.resolver.ClassResolverTest
    test`
    - `cd java && mvn -T4 -pl fory-core -DskipTests spotless:check
    checkstyle:check`
    
    ## Related issues
    
    Closes #3116
    Closes #3122
    
    ## Does this PR introduce any user-facing change?
    
    Yes. Registration is now intentionally closed after the first hash
    access, serializer compilation, or serialization path starts.
    
    - [x] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    Not applicable. This is a correctness fix; no benchmark logic changed.
    
    ---------
    
    Co-authored-by: Neale Upstone <[email protected]>
---
 .../src/main/java/org/apache/fory/Fory.java        |  25 ++++-
 .../fory/builder/BaseObjectCodecBuilder.java       |   2 +-
 .../java/org/apache/fory/builder/CodecUtils.java   |   3 +-
 .../main/java/org/apache/fory/config/Config.java   |  16 ---
 .../org/apache/fory/resolver/ClassResolver.java    |  35 ++++---
 .../org/apache/fory/resolver/TypeResolver.java     |  82 ++++++++++++---
 .../org/apache/fory/resolver/XtypeResolver.java    |  17 ++-
 .../org/apache/fory/serializer/RegisterTest.java   | 115 +++++++++++++++++++++
 .../fory/serializer/TimeSerializersTest.java       |  68 +++++-------
 9 files changed, 266 insertions(+), 97 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 d98862c91..354900f09 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
@@ -169,13 +169,13 @@ public final class Fory implements BaseFory {
   @Deprecated
   @Override
   public void register(Class<?> cls, boolean createSerializer) {
-    getTypeResolver().register(cls);
+    register(cls);
   }
 
   @Deprecated
   @Override
   public void register(Class<?> cls, int id, boolean createSerializer) {
-    getTypeResolver().register(cls, Integer.toUnsignedLong(id));
+    register(cls, id);
   }
 
   /**
@@ -235,7 +235,16 @@ public final class Fory implements BaseFory {
 
   @Override
   public void registerSerializer(Class<?> type, Function<Fory, Serializer<?>> 
serializerCreator) {
-    getTypeResolver().registerSerializer(type, serializerCreator.apply(this));
+    registerSerializer(type, serializerCreator.apply(this));
+  }
+
+  /**
+   * Returns the current config hash used to isolate generated serializer 
class names.
+   *
+   * <p>The first access finalizes the hash and closes further registration.
+   */
+  public int getConfigHash() {
+    return typeResolver.getConfigHash();
   }
 
   @Override
@@ -252,7 +261,7 @@ public final class Fory implements BaseFory {
   @Override
   public void registerSerializerAndType(
       Class<?> type, Function<Fory, Serializer<?>> serializerCreator) {
-    getTypeResolver().registerSerializerAndType(type, 
serializerCreator.apply(this));
+    registerSerializerAndType(type, serializerCreator.apply(this));
   }
 
   @Override
@@ -269,6 +278,10 @@ public final class Fory implements BaseFory {
     return typeResolver.getSerializer(cls);
   }
 
+  private void finalizeConfigHash() {
+    typeResolver.getConfigHash();
+  }
+
   @Override
   public MemoryBuffer serialize(Object obj, long address, int size) {
     MemoryBuffer buffer = MemoryUtils.buffer(address, size);
@@ -303,6 +316,7 @@ public final class Fory implements BaseFory {
 
   @Override
   public MemoryBuffer serialize(MemoryBuffer buffer, Object obj, 
BufferCallback callback) {
+    finalizeConfigHash();
     byte bitmap = 0;
     if (crossLanguage) {
       bitmap |= isCrossLanguageFlag;
@@ -705,6 +719,7 @@ public final class Fory implements BaseFory {
 
   @Override
   public <T> T deserialize(MemoryBuffer buffer, Class<T> type) {
+    finalizeConfigHash();
     try {
       jitContext.lock();
       if (depth > 0) {
@@ -771,6 +786,7 @@ public final class Fory implements BaseFory {
    */
   @Override
   public Object deserialize(MemoryBuffer buffer, Iterable<MemoryBuffer> 
outOfBandBuffers) {
+    finalizeConfigHash();
     try {
       jitContext.lock();
       if (depth > 0) {
@@ -1017,6 +1033,7 @@ public final class Fory implements BaseFory {
 
   @Override
   public <T> T copy(T obj) {
+    finalizeConfigHash();
     try {
       return copyObject(obj);
     } catch (Throwable e) {
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 879062968..3a04434ef 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
@@ -237,7 +237,7 @@ public abstract class BaseObjectCodecBuilder extends 
CodecBuilder {
     nameBuilder.append("Codec").append(codecSuffix());
     Map<String, Integer> subGenerator =
         idGenerator.computeIfAbsent(nameBuilder.toString(), k -> new 
ConcurrentHashMap<>());
-    String key = fory.getConfig().getConfigHash() + "_" + 
CodeGenerator.getClassUniqueId(beanClass);
+    String key = fory.getConfigHash() + "_" + 
CodeGenerator.getClassUniqueId(beanClass);
     Integer id = subGenerator.get(key);
     if (id == null) {
       synchronized (subGenerator) {
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java 
b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
index eeb582fb2..df6587687 100644
--- a/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
+++ b/java/fory-core/src/main/java/org/apache/fory/builder/CodecUtils.java
@@ -153,8 +153,8 @@ public class CodecUtils {
 
   private static <T> Class<? extends Serializer<T>> loadSerializer(
       String name, Class<?> cls, Fory fory, Callable<Class<? extends 
Serializer<T>>> func) {
-    int configHash = fory.getConfig().getConfigHash();
     if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      int configHash = fory.getConfigHash();
       Tuple3<String, Class<?>, Integer> key = Tuple3.of(name, cls, configHash);
       Class serializerClass = graalvmSerializers.get(key);
       if (serializerClass != null) {
@@ -164,6 +164,7 @@ public class CodecUtils {
     try {
       Class serializerClass = func.call();
       if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+        int configHash = fory.getConfigHash();
         graalvmSerializers.putIfAbsent(Tuple3.of(name, cls, configHash), 
serializerClass);
       }
       return serializerClass;
diff --git a/java/fory-core/src/main/java/org/apache/fory/config/Config.java 
b/java/fory-core/src/main/java/org/apache/fory/config/Config.java
index e07ada4ec..63ab82d36 100644
--- a/java/fory-core/src/main/java/org/apache/fory/config/Config.java
+++ b/java/fory-core/src/main/java/org/apache/fory/config/Config.java
@@ -21,9 +21,6 @@ package org.apache.fory.config;
 
 import java.io.Serializable;
 import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.fory.Fory;
 import org.apache.fory.meta.MetaCompressor;
 import org.apache.fory.serializer.Serializer;
@@ -60,7 +57,6 @@ public class Config implements Serializable {
   private final boolean asyncCompilationEnabled;
   private final boolean deserializeUnknownClass;
   private final boolean scalaOptimizationEnabled;
-  private transient int configHash;
   private final UnknownEnumValueStrategy unknownEnumValueStrategy;
   private final boolean serializeEnumByName;
   private final int bufferSizeLimitBytes;
@@ -364,18 +360,6 @@ public class Config implements Serializable {
         scalaOptimizationEnabled);
   }
 
-  private static final AtomicInteger counter = new AtomicInteger(0);
-  // Different config instance with equality will be hold only one instance, 
no memory
-  // leak will happen.
-  private static final ConcurrentMap<Config, Integer> configIdMap = new 
ConcurrentHashMap<>();
-
-  public int getConfigHash() {
-    if (configHash == 0) {
-      configHash = configIdMap.computeIfAbsent(this, k -> 
counter.incrementAndGet());
-    }
-    return configHash;
-  }
-
   /** Returns max depth for deserialization, when depth exceeds, an exception 
will be thrown. */
   public int maxDepth() {
     return maxDepth;
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 82fb027df..5bf5eceb7 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
@@ -119,7 +119,6 @@ import org.apache.fory.serializer.PrimitiveSerializers;
 import org.apache.fory.serializer.ReplaceResolveSerializer;
 import org.apache.fory.serializer.SerializationUtils;
 import org.apache.fory.serializer.Serializer;
-import org.apache.fory.serializer.SerializerFactory;
 import org.apache.fory.serializer.Serializers;
 import org.apache.fory.serializer.StringSerializer;
 import org.apache.fory.serializer.TimeSerializers;
@@ -237,7 +236,6 @@ public class ClassResolver extends TypeResolver {
     typeInfoCache = NIL_TYPE_INFO;
     extRegistry.classIdGenerator = NONEXISTENT_META_SHARED_ID + 1;
     shimDispatcher = new ShimDispatcher(fory);
-    _addGraalvmClassRegistry(fory.getConfig().getConfigHash(), this);
   }
 
   @Override
@@ -495,7 +493,8 @@ public class ClassResolver extends TypeResolver {
     compositeNameBytes2TypeInfo.put(
         new TypeNameBytes(nsBytes.hashCode, nameBytes.hashCode), typeInfo);
     extRegistry.registeredClasses.put(fullname, cls);
-    GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+    registerGraalvmClass(cls);
+    updateConfigHash(typeInfo);
   }
 
   @Override
@@ -515,7 +514,8 @@ public class ClassResolver extends TypeResolver {
     }
     updateTypeInfo(cls, typeInfo);
     extRegistry.registeredClasses.put(cls.getName(), cls);
-    GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+    registerGraalvmClass(cls);
+    updateConfigHash(typeInfo, serializer.getClass());
   }
 
   @Override
@@ -547,7 +547,8 @@ public class ClassResolver extends TypeResolver {
     compositeNameBytes2TypeInfo.put(
         new TypeNameBytes(nsBytes.hashCode, nameBytes.hashCode), typeInfo);
     extRegistry.registeredClasses.put(fullname, cls);
-    GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+    registerGraalvmClass(cls);
+    updateConfigHash(typeInfo, serializer.getClass());
   }
 
   /**
@@ -620,7 +621,7 @@ public class ClassResolver extends TypeResolver {
     }
     updateTypeInfo(cls, typeInfo);
     extRegistry.registeredClasses.put(cls.getName(), cls);
-    GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+    registerGraalvmClass(cls);
   }
 
   private void registerUserImpl(Class<?> cls, int userId) {
@@ -637,7 +638,8 @@ public class ClassResolver extends TypeResolver {
     }
     updateTypeInfo(cls, typeInfo);
     extRegistry.registeredClasses.put(cls.getName(), cls);
-    GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+    registerGraalvmClass(cls);
+    updateConfigHash(typeInfo);
   }
 
   private int buildUserTypeId(Class<?> cls, Serializer<?> serializer) {
@@ -1020,6 +1022,7 @@ public class ClassResolver extends TypeResolver {
     TypeInfo typeInfo = classInfoMap.get(type);
     classInfoMap.put(type, typeInfo);
     extRegistry.registeredTypeInfos.add(typeInfo);
+    updateConfigHash(typeInfo, serializer.getClass());
     // in order to support customized serializer for abstract or interface.
     if (!type.isPrimitive() && (ReflectionUtils.isAbstract(type) || 
type.isInterface())) {
       extRegistry.absTypeInfo.put(type, typeInfo);
@@ -1027,14 +1030,6 @@ public class ClassResolver extends TypeResolver {
     }
   }
 
-  public void setSerializerFactory(SerializerFactory serializerFactory) {
-    this.extRegistry.serializerFactory = serializerFactory;
-  }
-
-  public SerializerFactory getSerializerFactory() {
-    return extRegistry.serializerFactory;
-  }
-
   /**
    * Set the serializer for <code>cls</code>, overwrite serializer if exists. 
Note if class info is
    * already related with a class, this method should try to reuse that class 
info, otherwise jit
@@ -1949,9 +1944,17 @@ public class ClassResolver extends TypeResolver {
     if (extRegistry.ensureSerializersCompiled) {
       return;
     }
+    // Freeze the config hash before forcing any serializer resolution so 
later registrations
+    // cannot invalidate generated class names or GraalVM registrations.
+    getConfigHash();
     extRegistry.ensureSerializersCompiled = true;
     try {
       fory.getJITContext().lock();
+      if (GraalvmSupport.isGraalBuildtime()) {
+        // Materialize the final GraalVM registry bucket up front so this 
resolver is attached to
+        // the finalized hash even when the serializers below were already 
initialized earlier.
+        getGraalvmClassRegistry();
+      }
       // Lambda and JdkProxy serializers use java.lang.Class which is not 
supported in xlang mode
       if (!fory.isCrossLanguage()) {
         Serializers.newSerializer(fory, LambdaSerializer.STUB_LAMBDA_CLASS, 
LambdaSerializer.class);
@@ -1960,7 +1963,7 @@ public class ClassResolver extends TypeResolver {
       }
       classInfoMap.forEach(
           (cls, classInfo) -> {
-            GraalvmSupport.registerClass(cls, 
fory.getConfig().getConfigHash());
+            registerGraalvmClass(cls);
             if (classInfo.serializer == null) {
               if (isSerializable(classInfo.cls)) {
                 createSerializer0(cls);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 14c213799..074818d6f 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -127,7 +127,7 @@ public abstract class TypeResolver {
   protected TypeResolver(Fory fory) {
     this.fory = fory;
     metaContextShareEnabled = fory.getConfig().isMetaShareEnabled();
-    extRegistry = new ExtRegistry();
+    extRegistry = new ExtRegistry(fory.getConfig().hashCode());
     metaStringResolver = fory.getMetaStringResolver();
   }
 
@@ -137,6 +137,53 @@ public abstract class TypeResolver {
           "Cannot register class/serializer after 
serialization/deserialization has started. "
               + "Please register all classes before invoking 
`serialize/deserialize` methods of Fory.");
     }
+    if (extRegistry.configHashFrozen) {
+      throw new ForyException(
+          "Cannot register class/serializer after the config hash has been 
used. "
+              + "Please finish all class and serializer registrations before 
serializer compilation, serialization, or config hash lookup starts.");
+    }
+  }
+
+  protected final void updateConfigHash(Object... values) {
+    Object[] args = new Object[values.length + 1];
+    args[0] = extRegistry.configHash;
+    System.arraycopy(values, 0, args, 1, values.length);
+    extRegistry.configHash = Objects.hashCode(args);
+  }
+
+  protected final void updateConfigHash(TypeInfo typeInfo, Object... values) {
+    Object[] args = new Object[values.length + 5];
+    args[0] = typeInfo.getCls();
+    args[1] = typeInfo.typeId;
+    args[2] = typeInfo.userTypeId;
+    args[3] = typeInfo.typeNameBytes == null ? null : 
typeInfo.decodeNamespace();
+    args[4] = typeInfo.typeNameBytes == null ? null : 
typeInfo.decodeTypeName();
+    System.arraycopy(values, 0, args, 5, values.length);
+    updateConfigHash(args);
+  }
+
+  @Internal
+  public final int getConfigHash() {
+    if (!extRegistry.configHashFrozen) {
+      extRegistry.configHashFrozen = true;
+      if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && 
!extRegistry.pendingGraalvmClasses.isEmpty()) {
+        extRegistry.pendingGraalvmClasses.forEach(
+            cls -> GraalvmSupport.registerClass(cls, extRegistry.configHash));
+        extRegistry.pendingGraalvmClasses.clear();
+      }
+    }
+    return extRegistry.configHash;
+  }
+
+  protected final void registerGraalvmClass(Class<?> cls) {
+    if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return;
+    }
+    if (extRegistry.configHashFrozen) {
+      GraalvmSupport.registerClass(cls, extRegistry.configHash);
+    } else {
+      extRegistry.pendingGraalvmClasses.add(cls);
+    }
   }
 
   /**
@@ -1444,7 +1491,9 @@ public abstract class TypeResolver {
   }
 
   public void setSerializerFactory(SerializerFactory serializerFactory) {
+    checkRegisterAllowed();
     extRegistry.serializerFactory = serializerFactory;
+    updateConfigHash(serializerFactory == null ? null : 
serializerFactory.getClass());
   }
 
   public CodeGenerator getCodeGenerator(ClassLoader... loaders) {
@@ -1467,18 +1516,14 @@ public abstract class TypeResolver {
 
   public void resetWrite() {}
 
-  // CHECKSTYLE.OFF:MethodName
-  public static void _addGraalvmClassRegistry(int foryConfigHash, 
ClassResolver classResolver) {
-    // CHECKSTYLE.ON:MethodName
-    if (GraalvmSupport.isGraalBuildtime()) {
-      GraalvmSupport.GraalvmClassRegistry registry =
-          GraalvmSupport.getClassRegistry(foryConfigHash);
-      registry.resolvers.add(classResolver);
-    }
-  }
-
   final GraalvmSupport.GraalvmClassRegistry getGraalvmClassRegistry() {
-    return GraalvmSupport.getClassRegistry(fory.getConfig().getConfigHash());
+    GraalvmSupport.GraalvmClassRegistry registry = 
GraalvmSupport.getClassRegistry(getConfigHash());
+    if (GraalvmSupport.isGraalBuildtime()
+        && this instanceof ClassResolver
+        && !registry.resolvers.contains(this)) {
+      registry.resolvers.add(this);
+    }
+    return registry;
   }
 
   final Class<? extends Serializer> getGraalvmSerializerClass(Serializer 
serializer) {
@@ -1489,6 +1534,9 @@ public abstract class TypeResolver {
   }
 
   final Class<? extends Serializer> 
getSerializerClassFromGraalvmRegistry(Class<?> cls) {
+    if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return null;
+    }
     GraalvmSupport.GraalvmClassRegistry registry = getGraalvmClassRegistry();
     List<TypeResolver> resolvers = registry.resolvers;
     if (resolvers.isEmpty()) {
@@ -1518,6 +1566,9 @@ public abstract class TypeResolver {
 
   private Class<? extends Serializer> 
getMetaSharedDeserializerClassFromGraalvmRegistry(
       Class<?> cls, TypeDef typeDef) {
+    if (!GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE) {
+      return null;
+    }
     GraalvmSupport.GraalvmClassRegistry registry = getGraalvmClassRegistry();
     List<TypeResolver> resolvers = registry.resolvers;
     if (resolvers.isEmpty()) {
@@ -1550,6 +1601,9 @@ public abstract class TypeResolver {
   }
 
   static class ExtRegistry {
+    int configHash;
+    boolean configHashFrozen;
+    final Set<Class<?>> pendingGraalvmClasses = new HashSet<>();
     // Here we set it to 1 to avoid calculating it again in `register(Class<?> 
cls)`.
     int classIdGenerator = 1;
     int userIdGenerator = 0;
@@ -1580,6 +1634,10 @@ public abstract class TypeResolver {
     final Set<TypeInfo> registeredTypeInfos = new HashSet<>();
     boolean ensureSerializersCompiled;
 
+    ExtRegistry(int configHash) {
+      this.configHash = configHash;
+    }
+
     public boolean isTypeCheckerSet() {
       return typeChecker != DEFAULT_TYPE_CHECKER;
     }
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 0ca109846..b82533b8d 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
@@ -184,7 +184,8 @@ public class XtypeResolver extends TypeResolver {
     TypeInfo typeInfo = classInfoMap.get(type);
     if (type.isArray()) {
       buildTypeInfo(type);
-      GraalvmSupport.registerClass(type, fory.getConfig().getConfigHash());
+      registerGraalvmClass(type);
+      updateConfigHash(classInfoMap.get(type));
       return;
     }
     Serializer<?> serializer = null;
@@ -288,7 +289,7 @@ public class XtypeResolver extends TypeResolver {
     String qualifiedName = qualifiedName(namespace, typeName);
     qualifiedType2TypeInfo.put(qualifiedName, typeInfo);
     extRegistry.registeredClasses.put(qualifiedName, type);
-    GraalvmSupport.registerClass(type, fory.getConfig().getConfigHash());
+    registerGraalvmClass(type);
     if (serializer == null) {
       if (type.isEnum()) {
         typeInfo.serializer = new EnumSerializer(fory, (Class<Enum>) type);
@@ -317,6 +318,11 @@ public class XtypeResolver extends TypeResolver {
       }
     }
     updateTypeInfo(type, typeInfo);
+    if (serializer == null) {
+      updateConfigHash(typeInfo);
+    } else {
+      updateConfigHash(typeInfo, serializer.getClass());
+    }
   }
 
   @Override
@@ -455,6 +461,7 @@ public class XtypeResolver extends TypeResolver {
     typeInfo = typeInfo.copy(foryId);
     typeInfo.serializer = serializer;
     updateTypeInfo(type, typeInfo);
+    updateConfigHash(typeInfo, serializer.getClass());
     if (typeInfo.typeNameBytes != null) {
       String qualifiedName = qualifiedName(typeInfo.decodeNamespace(), 
typeInfo.decodeTypeName());
       qualifiedType2TypeInfo.put(qualifiedName, typeInfo);
@@ -1258,9 +1265,13 @@ public class XtypeResolver extends TypeResolver {
    */
   @Override
   public void ensureSerializersCompiled() {
+    getConfigHash();
+    if (GraalvmSupport.isGraalBuildtime()) {
+      getGraalvmClassRegistry();
+    }
     classInfoMap.forEach(
         (cls, classInfo) -> {
-          GraalvmSupport.registerClass(cls, fory.getConfig().getConfigHash());
+          registerGraalvmClass(cls);
           if (classInfo.serializer != null) {
             // Trigger serializer initialization and resolution for deferred 
serializers
             if (classInfo.serializer
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java 
b/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java
index 97cb088d1..4ad78e09d 100644
--- a/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/serializer/RegisterTest.java
@@ -24,12 +24,30 @@ import org.apache.fory.ForyTestBase;
 import org.apache.fory.config.CompatibleMode;
 import org.apache.fory.config.ForyBuilder;
 import org.apache.fory.config.Language;
+import org.apache.fory.exception.ForyException;
 import org.apache.fory.memory.MemoryBuffer;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 public class RegisterTest extends ForyTestBase {
 
+  private static Fory newCodegenJavaFory() {
+    return Fory.builder()
+        .withLanguage(Language.JAVA)
+        .requireClassRegistration(false)
+        .withCodegen(true)
+        .build();
+  }
+
+  private static CodegenWrapper newCodegenWrapper(String id, int number) {
+    CodegenWrapper wrapper = new CodegenWrapper();
+    wrapper.name = "wrapper-" + number;
+    wrapper.number = number;
+    wrapper.myExt = new MyExt();
+    wrapper.myExt.id = id;
+    return wrapper;
+  }
+
   @Test(dataProvider = "enableCodegen")
   public void testRegisterForCompatible(boolean enableCodegen) {
     A a = new A();
@@ -71,6 +89,12 @@ public class RegisterTest extends ForyTestBase {
 
   public static class B {}
 
+  public static class CodegenWrapper {
+    public String name;
+    public int number;
+    public MyExt myExt;
+  }
+
   @Test
   public void testRegisterThenRegisterSerializer() {
     Fory fory =
@@ -139,6 +163,97 @@ public class RegisterTest extends ForyTestBase {
     Assert.assertEquals(deserialized.id, "idempotent-test");
   }
 
+  @Test
+  public void testCodegenCacheIsolation() {
+    Fory foryA = newCodegenJavaFory();
+    foryA.registerSerializer(MyExt.class, MyExtSerializer.class);
+    foryA.serialize(newCodegenWrapper("20", 20));
+
+    Fory foryB = newCodegenJavaFory();
+    CodegenWrapper deserialized =
+        foryB.deserialize(foryB.serialize(newCodegenWrapper("40", 40)), 
CodegenWrapper.class);
+
+    Assert.assertNotNull(deserialized);
+    Assert.assertNotNull(deserialized.myExt);
+    Assert.assertEquals(deserialized.myExt.id, "40");
+  }
+
+  @Test
+  public void testCodegenCacheIsolationWithRegisterSerializerAndType() {
+    Fory foryA = newCodegenJavaFory();
+    foryA.registerSerializerAndType(MyExt.class, MyExtSerializer.class);
+    foryA.serialize(newCodegenWrapper("20", 20));
+
+    Fory foryB = newCodegenJavaFory();
+    CodegenWrapper deserialized =
+        foryB.deserialize(foryB.serialize(newCodegenWrapper("40", 40)), 
CodegenWrapper.class);
+
+    Assert.assertNotNull(deserialized);
+    Assert.assertNotNull(deserialized.myExt);
+    Assert.assertEquals(deserialized.myExt.id, "40");
+  }
+
+  @Test
+  public void testConfigHashTracksAdditionalRegistrationPaths() {
+    int baseHash = newCodegenJavaFory().getConfigHash();
+
+    Fory registerByName = newCodegenJavaFory();
+    registerByName.register(MyExt.class, "test.pkg", "MyExt");
+    int registerByNameHash = registerByName.getConfigHash();
+    Assert.assertNotEquals(registerByNameHash, baseHash);
+
+    Fory registerByName2 = newCodegenJavaFory();
+    registerByName2.register(MyExt.class, "test.pkg", "MyExt");
+    Assert.assertNotEquals(registerByName2.getConfigHash(), baseHash);
+
+    Fory serializerBase = newCodegenJavaFory();
+    serializerBase.register(MyExt.class, 103);
+    int serializerBaseHash = serializerBase.getConfigHash();
+
+    Fory serializerByFunction = newCodegenJavaFory();
+    serializerByFunction.register(MyExt.class, 103);
+    serializerByFunction.registerSerializer(MyExt.class, MyExtSerializer::new);
+    int serializerByFunctionHash = serializerByFunction.getConfigHash();
+    Assert.assertNotEquals(serializerByFunctionHash, serializerBaseHash);
+
+    Fory serializerAndType = newCodegenJavaFory();
+    serializerAndType.registerSerializerAndType(MyExt.class, 
MyExtSerializer.class);
+    int serializerAndTypeHash = serializerAndType.getConfigHash();
+    Assert.assertNotEquals(serializerAndTypeHash, baseHash);
+
+    Fory union = newCodegenJavaFory();
+    union.registerUnion(MyExt.class, 103, new MyExtSerializer(union));
+    int unionHash = union.getConfigHash();
+    Assert.assertNotEquals(unionHash, baseHash);
+
+    Fory union2 = newCodegenJavaFory();
+    union2.registerUnion(MyExt.class, 103, new MyExtSerializer(union2));
+    Assert.assertNotEquals(union2.getConfigHash(), baseHash);
+  }
+
+  @Test
+  public void testConfigHashFinalizesAfterHashAccess() {
+    Fory fory = newCodegenJavaFory();
+    fory.getConfigHash();
+    Assert.expectThrows(ForyException.class, () -> fory.register(MyExt.class, 
103));
+  }
+
+  @Test
+  public void testConfigHashFinalizesAfterSerialize() {
+    Fory fory = newCodegenJavaFory();
+    fory.serialize(newCodegenWrapper("20", 20));
+    Assert.expectThrows(
+        ForyException.class, () -> fory.registerSerializer(MyExt.class, 
MyExtSerializer.class));
+  }
+
+  @Test
+  public void testConfigHashFinalizesAfterEnsureSerializersCompiled() {
+    Fory fory = newCodegenJavaFory();
+    fory.register(CodegenWrapper.class);
+    fory.ensureSerializersCompiled();
+    Assert.expectThrows(ForyException.class, () -> fory.register(MyExt.class, 
103));
+  }
+
   public static class MyExt {
     public String id;
   }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/serializer/TimeSerializersTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/serializer/TimeSerializersTest.java
index 549c3134e..e946059a1 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/serializer/TimeSerializersTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/serializer/TimeSerializersTest.java
@@ -219,9 +219,11 @@ public class TimeSerializersTest extends ForyTestBase {
     struct.duration = Duration.between(Instant.now(), 
Instant.ofEpochSecond(-1));
     {
       Fory fory =
-          
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
-      fory.registerSerializer(
-          TimeStruct.class, CodegenSerializer.loadCodegenSerializer(fory, 
TimeStruct.class));
+          Fory.builder()
+              .withLanguage(Language.JAVA)
+              .requireClassRegistration(false)
+              .withCodegen(true)
+              .build();
       serDe(fory, struct);
     }
     {
@@ -290,8 +292,6 @@ public class TimeSerializersTest extends ForyTestBase {
               .withRefTracking(true)
               .ignoreTimeRef(true)
               .build();
-      fory.registerSerializer(
-          TimeStructRef.class, CodegenSerializer.loadCodegenSerializer(fory, 
TimeStructRef.class));
       TimeStructRef struct = createTimeStructRef(new TimeStructRef());
       TimeStructRef struct1 = (TimeStructRef) serDeCheck(fory, struct);
       Assert.assertNotSame(struct1.date1, struct1.date2);
@@ -305,14 +305,10 @@ public class TimeSerializersTest extends ForyTestBase {
           Fory.builder()
               .withLanguage(Language.JAVA)
               .requireClassRegistration(false)
+              .withCodegen(true)
               .withRefTracking(true)
               .ignoreTimeRef(false)
               .build();
-      fory.registerSerializer(
-          TimeStruct.class, CodegenSerializer.loadCodegenSerializer(fory, 
TimeStruct.class));
-      fory.registerSerializer(
-          TimeStructRef1.class,
-          CodegenSerializer.loadCodegenSerializer(fory, TimeStructRef1.class));
       TimeStructRef1 struct = (TimeStructRef1) createTimeStructRef(new 
TimeStructRef1());
       TimeStructRef1 struct1 = (TimeStructRef1) serDeCheck(fory, struct);
       Assert.assertSame(struct1.date1, struct1.date2);
@@ -337,8 +333,10 @@ public class TimeSerializersTest extends ForyTestBase {
       {
         TimeStructRef struct = createTimeStructRef(new TimeStructRef());
         TimeStructRef struct2 = (TimeStructRef) serDeCheck(fory, struct);
-        // TimeStructRef serializer already generated, enable ref tracking 
doesn't take effect.
-        Assert.assertNotSame(struct2.date1, struct2.date2);
+
+        // Despite a TimeStructRef codegen serializer having already been 
generated, the change to
+        // enable ref tracking should now take effect due to improvements to 
the codegen caching
+        Assert.assertSame(struct2.date1, struct2.date2);
       }
       {
         TimeStructRef struct = createTimeStructRef(new TimeStructRef2());
@@ -352,34 +350,17 @@ public class TimeSerializersTest extends ForyTestBase {
     }
   }
 
-  @Test(dataProvider = "foryCopyConfig")
-  public void testTimeStructRef(Fory fory) {
-    {
-      fory.registerSerializer(
-          TimeStructRef.class, CodegenSerializer.loadCodegenSerializer(fory, 
TimeStructRef.class));
-      TimeStructRef struct = createTimeStructRef(new TimeStructRef());
-      TimeStructRef struct1 = fory.copy(struct);
-      Assert.assertSame(struct1.date1, struct1.date2);
-      Assert.assertSame(struct1.sqlDate1, struct1.sqlDate2);
-      Assert.assertSame(struct1.time1, struct1.time2);
-      Assert.assertSame(struct1.instant1, struct1.instant2);
-      Assert.assertSame(struct1.duration1, struct1.duration2);
-    }
-    {
-      fory.registerSerializer(
-          TimeStruct.class, CodegenSerializer.loadCodegenSerializer(fory, 
TimeStruct.class));
-      fory.registerSerializer(
-          TimeStructRef1.class,
-          CodegenSerializer.loadCodegenSerializer(fory, TimeStructRef1.class));
-      TimeStructRef1 struct = (TimeStructRef1) createTimeStructRef(new 
TimeStructRef1());
-      TimeStructRef1 struct1 = fory.copy(struct);
-      Assert.assertSame(struct1.date1, struct1.date2);
-      Assert.assertSame(struct1.sqlDate1, struct1.sqlDate2);
-      Assert.assertSame(struct1.time1, struct1.time2);
-      Assert.assertSame(struct1.instant1, struct1.instant2);
-      Assert.assertSame(struct1.duration1, struct1.duration2);
-    }
+  @Test
+  public void testTimeStructRefCopy() {
     {
+      Fory fory =
+          Fory.builder()
+              .withLanguage(Language.JAVA)
+              .requireClassRegistration(false)
+              .withCodegen(true)
+              .withRefTracking(true)
+              .ignoreTimeRef(true)
+              .build();
       fory.registerSerializer(Date.class, new 
TimeSerializers.DateSerializer(fory, true));
       fory.registerSerializer(
           java.sql.Date.class, new TimeSerializers.SqlDateSerializer(fory, 
true));
@@ -387,16 +368,15 @@ public class TimeSerializersTest extends ForyTestBase {
       {
         TimeStructRef struct = createTimeStructRef(new TimeStructRef());
         TimeStructRef struct2 = fory.copy(struct);
-        // TimeStructRef serializer already generated, enable ref tracking 
doesn't take effect.
-        Assert.assertSame(struct2.date1, struct2.date2);
+        Assert.assertEquals(struct2.date1, struct2.date2);
       }
       {
         TimeStructRef struct = createTimeStructRef(new TimeStructRef2());
         TimeStructRef struct2 = fory.copy(struct);
-        Assert.assertSame(struct2.date1, struct2.date2);
-        Assert.assertSame(struct2.sqlDate1, struct2.sqlDate2);
+        Assert.assertEquals(struct2.date1, struct2.date2);
+        Assert.assertEquals(struct2.sqlDate1, struct2.sqlDate2);
         Assert.assertSame(struct2.instant1, struct2.instant2);
-        Assert.assertSame(struct2.time1, struct2.time2);
+        Assert.assertEquals(struct2.time1, struct2.time2);
         Assert.assertSame(struct2.duration1, struct2.duration2);
       }
     }


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


Reply via email to