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 d25481825 fix(java): configure type checker during build (#3550)
d25481825 is described below

commit d2548182555a062bcbc76449903dce70ae4de5c0
Author: Shawn Yang <[email protected]>
AuthorDate: Fri Apr 10 22:17:41 2026 +0800

    fix(java): configure type checker during build (#3550)
    
    ## Why?
    
    ## What does this PR do?
    
    - add `ForyBuilder.withTypeChecker` so type checkers can be installed
    before build
    - wire builder-configured checkers into `Fory` creation and centralize
    `AllowListChecker` listener setup
    - update the Java type-registration docs and tests to use the supported
    allow-list flow
    
    ## Related issues
    
    Closes #3530.
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
---
 docs/guide/java/type-registration.md               | 20 +++++---
 .../org/apache/fory/AbstractThreadSafeFory.java    | 11 +---
 .../src/main/java/org/apache/fory/Fory.java        |  5 ++
 .../java/org/apache/fory/config/ForyBuilder.java   | 35 ++++++++++---
 .../org/apache/fory/resolver/TypeResolver.java     |  5 +-
 .../apache/fory/resolver/AllowListCheckerTest.java | 59 +++++++++++++++++++---
 6 files changed, 103 insertions(+), 32 deletions(-)

diff --git a/docs/guide/java/type-registration.md 
b/docs/guide/java/type-registration.md
index 7e82572da..2e63a35e1 100644
--- a/docs/guide/java/type-registration.md
+++ b/docs/guide/java/type-registration.md
@@ -60,14 +60,15 @@ If there are no duplicate names for types, `namespace` can 
be left as empty to r
 
 ### Type Checker
 
-If you invoke `ForyBuilder#requireClassRegistration(false)` to disable class 
registration check, you can set `org.apache.fory.resolver.TypeChecker` by 
`TypeResolver#setTypeChecker` to control which classes are allowed for 
serialization.
+If you invoke `ForyBuilder#requireClassRegistration(false)` to disable class 
registration check, you can configure `org.apache.fory.resolver.TypeChecker` by 
`ForyBuilder#withTypeChecker` or `TypeResolver#setTypeChecker` to control which 
classes are allowed for serialization.
 
 For example, you can allow classes started with `org.example.*`:
 
 ```java
-Fory fory = xxx;
-fory.getTypeResolver().setTypeChecker(
-  (typeResolver, className) -> className.startsWith("org.example."));
+Fory fory = Fory.builder()
+  .requireClassRegistration(false)
+  .withTypeChecker((typeResolver, className) -> 
className.startsWith("org.example."))
+  .build();
 ```
 
 ### AllowListChecker
@@ -76,12 +77,17 @@ Fory provides a `org.apache.fory.resolver.AllowListChecker` 
which is an allowed/
 
 ```java
 AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.STRICT);
-ThreadSafeFory fory = 
Fory.builder().requireClassRegistration(false).buildThreadSafeFory();
-fory.setTypeChecker(checker);
 checker.allowClass("org.example.*");
+ThreadSafeFory fory = Fory.builder()
+  .requireClassRegistration(false)
+  .withTypeChecker(checker)
+  .buildThreadSafeFory();
 ```
 
-You can use this checker or implement a more sophisticated checker by yourself.
+`withTypeChecker` installs the checker on every created runtime immediately, 
which also avoids the
+generic startup warning emitted when class registration is disabled without 
any checker. You can
+still use `TypeResolver#setTypeChecker` or `ThreadSafeFory#setTypeChecker` 
later if you need to
+replace the checker after build time.
 
 ## Limit Max Deserialization Depth
 
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/AbstractThreadSafeFory.java 
b/java/fory-core/src/main/java/org/apache/fory/AbstractThreadSafeFory.java
index 19d1474bd..33ac6af9e 100644
--- a/java/fory-core/src/main/java/org/apache/fory/AbstractThreadSafeFory.java
+++ b/java/fory-core/src/main/java/org/apache/fory/AbstractThreadSafeFory.java
@@ -20,8 +20,6 @@
 package org.apache.fory;
 
 import java.util.function.Function;
-import org.apache.fory.resolver.AllowListChecker;
-import org.apache.fory.resolver.ClassResolver;
 import org.apache.fory.resolver.TypeChecker;
 import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.serializer.Serializer;
@@ -121,14 +119,7 @@ public abstract class AbstractThreadSafeFory implements 
ThreadSafeFory {
 
   @Override
   public void setTypeChecker(TypeChecker typeChecker) {
-    registerCallback(
-        fory -> {
-          TypeResolver typeResolver = fory.getTypeResolver();
-          typeResolver.setTypeChecker(typeChecker);
-          if (typeChecker instanceof AllowListChecker && typeResolver 
instanceof ClassResolver) {
-            ((AllowListChecker) typeChecker).addListener((ClassResolver) 
typeResolver);
-          }
-        });
+    registerCallback(fory -> 
fory.getTypeResolver().setTypeChecker(typeChecker));
   }
 
   @Override
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 1222f4145..adf1e8c6d 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
@@ -52,6 +52,7 @@ import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
 import org.apache.fory.resolver.ClassResolver;
 import org.apache.fory.resolver.SharedRegistry;
+import org.apache.fory.resolver.TypeChecker;
 import org.apache.fory.resolver.TypeInfo;
 import org.apache.fory.resolver.TypeResolver;
 import org.apache.fory.resolver.XtypeResolver;
@@ -133,6 +134,10 @@ public final class Fory implements BaseFory {
             ? new XtypeResolver(config, classLoader, sharedRegistry, 
jitContext)
             : new ClassResolver(config, classLoader, sharedRegistry, 
jitContext);
     typeResolver.initialize();
+    TypeChecker configuredTypeChecker = builder.getTypeChecker();
+    if (configuredTypeChecker != null) {
+      typeResolver.setTypeChecker(configuredTypeChecker);
+    }
     MetaStringWriter metaStringWriter = new MetaStringWriter();
     MetaStringReader metaStringReader = new MetaStringReader(sharedRegistry);
     writeContext =
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java 
b/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java
index feb7f4565..044a9ac25 100644
--- a/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java
+++ b/java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java
@@ -35,6 +35,7 @@ import org.apache.fory.meta.MetaCompressor;
 import org.apache.fory.pool.ThreadPoolFory;
 import org.apache.fory.reflect.ReflectionUtils;
 import org.apache.fory.resolver.SharedRegistry;
+import org.apache.fory.resolver.TypeChecker;
 import org.apache.fory.serializer.JavaSerializer;
 import org.apache.fory.serializer.ObjectStreamSerializer;
 import org.apache.fory.serializer.Serializer;
@@ -97,6 +98,7 @@ public final class ForyBuilder {
   int maxDepth = 50;
   float mapRefLoadFactor = 0.51f;
   boolean forVirtualThread = false;
+  TypeChecker typeChecker;
   private List<Consumer<ForyBuilder>> actions = new ArrayList<>();
   private boolean replayingActions = false;
 
@@ -349,9 +351,9 @@ public final class ForyBuilder {
    * attack if the classes `constructor`/`equals`/`hashCode` method contain 
malicious code. Do not
    * disable class registration if you can't ensure your environment are 
*indeed secure*. We are not
    * responsible for security risks if you disable this option. If you disable 
this option, you can
-   * configure {@link org.apache.fory.resolver.TypeChecker} by {@link
-   * org.apache.fory.resolver.TypeResolver#setTypeChecker} to control which 
classes are allowed
-   * being serialized.
+   * configure {@link org.apache.fory.resolver.TypeChecker} by {@link 
#withTypeChecker(TypeChecker)}
+   * or {@link org.apache.fory.resolver.TypeResolver#setTypeChecker} to 
control which classes are
+   * allowed being serialized.
    */
   public ForyBuilder requireClassRegistration(boolean 
requireClassRegistration) {
     this.requireClassRegistration = requireClassRegistration;
@@ -359,6 +361,21 @@ public final class ForyBuilder {
     return this;
   }
 
+  /**
+   * Configure a {@link TypeChecker} during build time so it is installed on 
every created runtime.
+   * This checker is only consulted for unknown class names when class 
registration checks are
+   * disabled.
+   */
+  public ForyBuilder withTypeChecker(TypeChecker typeChecker) {
+    this.typeChecker = typeChecker;
+    recordAction(b -> b.withTypeChecker(typeChecker));
+    return this;
+  }
+
+  public TypeChecker getTypeChecker() {
+    return typeChecker;
+  }
+
   /**
    * Whether suppress class registration warnings. The warnings can be used 
for security audit, but
    * may be annoying, this suppression will be enabled by default.
@@ -572,11 +589,13 @@ public final class ForyBuilder {
       }
     }
     if (!requireClassRegistration) {
-      LOG.warn(
-          "Class registration isn't forced, unknown classes can be 
deserialized. "
-              + "If the environment isn't secure, please enable class 
registration by "
-              + "`ForyBuilder#requireClassRegistration(true)` or configure 
TypeChecker by "
-              + "`TypeResolver#setTypeChecker`");
+      if (typeChecker == null) {
+        LOG.warn(
+            "Class registration isn't forced, unknown classes can be 
deserialized. "
+                + "If the environment isn't secure, please enable class 
registration by "
+                + "`ForyBuilder#requireClassRegistration(true)` or configure 
TypeChecker by "
+                + "`ForyBuilder#withTypeChecker` or 
`TypeResolver#setTypeChecker`");
+      }
     }
     if (GraalvmSupport.IN_GRAALVM_NATIVE_IMAGE && asyncCompilationEnabled) {
       LOG.info("Use sync compilation for graalvm native image since it doesn't 
support JIT.");
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 ff7cecb50..f092cf52e 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
@@ -1580,7 +1580,10 @@ public abstract class TypeResolver {
   }
 
   public void setTypeChecker(TypeChecker typeChecker) {
-    extRegistry.typeChecker = typeChecker;
+    extRegistry.typeChecker = typeChecker == null ? DEFAULT_TYPE_CHECKER : 
typeChecker;
+    if (extRegistry.typeChecker instanceof AllowListChecker && this instanceof 
ClassResolver) {
+      ((AllowListChecker) extRegistry.typeChecker).addListener((ClassResolver) 
this);
+    }
   }
 
   public void setSerializerFactory(SerializerFactory serializerFactory) {
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
index 6ab489754..db5b7a280 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
@@ -21,9 +21,14 @@ package org.apache.fory.resolver;
 
 import static org.testng.Assert.*;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
 import org.apache.fory.Fory;
 import org.apache.fory.ThreadSafeFory;
 import org.apache.fory.exception.InsecureException;
+import org.apache.fory.logging.LogLevel;
+import org.apache.fory.logging.LoggerFactory;
 import org.testng.annotations.Test;
 
 public class AllowListCheckerTest {
@@ -37,7 +42,6 @@ public class AllowListCheckerTest {
       assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
       checker.allowClass(AllowListCheckerTest.class.getName());
       byte[] bytes = fory.serialize(new AllowListCheckerTest());
-      checker.addListener((ClassResolver) fory.getTypeResolver());
       checker.disallowClass(AllowListCheckerTest.class.getName());
       assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
       assertThrows(InsecureException.class, () -> fory.deserialize(bytes));
@@ -46,7 +50,6 @@ public class AllowListCheckerTest {
       Fory fory = Fory.builder().requireClassRegistration(false).build();
       AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.WARN);
       fory.getTypeResolver().setTypeChecker(checker);
-      checker.addListener((ClassResolver) fory.getTypeResolver());
       byte[] bytes = fory.serialize(new AllowListCheckerTest());
       checker.disallowClass(AllowListCheckerTest.class.getName());
       assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
@@ -60,7 +63,6 @@ public class AllowListCheckerTest {
       Fory fory = Fory.builder().requireClassRegistration(false).build();
       AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.STRICT);
       fory.getTypeResolver().setTypeChecker(checker);
-      checker.addListener((ClassResolver) fory.getTypeResolver());
       assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
       checker.allowClass("org.apache.fory.*");
       byte[] bytes = fory.serialize(new AllowListCheckerTest());
@@ -72,7 +74,6 @@ public class AllowListCheckerTest {
       Fory fory = Fory.builder().requireClassRegistration(false).build();
       AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.WARN);
       fory.getTypeResolver().setTypeChecker(checker);
-      checker.addListener((ClassResolver) fory.getTypeResolver());
       byte[] bytes = fory.serialize(new AllowListCheckerTest());
       checker.disallowClass("org.apache.fory.*");
       assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
@@ -80,11 +81,57 @@ public class AllowListCheckerTest {
     }
   }
 
+  @Test
+  public void testBuilderConfiguredChecker() {
+    AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.STRICT);
+    checker.allowClass("org.apache.fory.*");
+    Fory fory = 
Fory.builder().requireClassRegistration(false).withTypeChecker(checker).build();
+    byte[] bytes = fory.serialize(new AllowListCheckerTest());
+    checker.disallowClass("org.apache.fory.*");
+    assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
+    assertThrows(InsecureException.class, () -> fory.deserialize(bytes));
+  }
+
+  @Test
+  public void testBuilderConfiguredCheckerSuppressesStartupWarning() {
+    synchronized (AllowListCheckerTest.class) {
+      PrintStream previousOut = System.out;
+      int previousLogLevel = LoggerFactory.getLogLevel();
+      ByteArrayOutputStream output = new ByteArrayOutputStream();
+      try (PrintStream capture = new PrintStream(output, true, 
StandardCharsets.UTF_8.name())) {
+        System.setOut(capture);
+        LoggerFactory.setLogLevel(LogLevel.WARN_LEVEL);
+
+        Fory.builder().requireClassRegistration(false).build();
+        assertTrue(
+            new String(output.toByteArray(), StandardCharsets.UTF_8)
+                .contains("Class registration isn't forced"));
+
+        output.reset();
+        Fory.builder()
+            .requireClassRegistration(false)
+            .withTypeChecker((resolver, className) -> true)
+            .build();
+        assertFalse(
+            new String(output.toByteArray(), StandardCharsets.UTF_8)
+                .contains("Class registration isn't forced"));
+      } catch (Exception e) {
+        throw new AssertionError(e);
+      } finally {
+        LoggerFactory.setLogLevel(previousLogLevel);
+        System.setOut(previousOut);
+      }
+    }
+  }
+
   @Test
   public void testThreadSafeFory() {
     AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.STRICT);
-    ThreadSafeFory fory = 
Fory.builder().requireClassRegistration(false).buildThreadSafeFory();
-    fory.setTypeChecker(checker);
+    ThreadSafeFory fory =
+        Fory.builder()
+            .requireClassRegistration(false)
+            .withTypeChecker(checker)
+            .buildThreadSafeFory();
     checker.allowClass("org.apache.fory.*");
     byte[] bytes = fory.serialize(new AllowListCheckerTest());
     checker.disallowClass("org.apache.fory.*");


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

Reply via email to