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 48abb3ce0 feat(java): add once logging APIs (#3821)
48abb3ce0 is described below

commit 48abb3ce00a27a129598a884fb50c67a6f86dd75
Author: Shawn Yang <[email protected]>
AuthorDate: Mon Jul 6 11:01:30 2026 +0530

    feat(java): add once logging APIs (#3821)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    Closes #3785
    ## 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 or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, 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?
    
    ## Benchmark
---
 .../java/org/apache/fory/logging/ForyLogger.java   |  83 +++++++++++
 .../java/org/apache/fory/logging/LogOnceState.java | 161 +++++++++++++++++++++
 .../main/java/org/apache/fory/logging/Logger.java  |  20 +++
 .../java/org/apache/fory/logging/Slf4jLogger.java  |  71 +++++++++
 .../org/apache/fory/resolver/AllowListChecker.java |   2 +-
 .../org/apache/fory/resolver/ClassResolver.java    |  14 +-
 .../org/apache/fory/resolver/TypeResolver.java     |   6 +-
 .../org/apache/fory/resolver/XtypeResolver.java    |   4 +-
 .../org/apache/fory/serializer/JavaSerializer.java |   6 +-
 .../fory/serializer/ObjectStreamSerializer.java    |   4 +-
 .../fory/serializer/ReplaceResolveSerializer.java  |   4 +-
 .../serializer/collection/SubListSerializers.java  |  12 +-
 .../collection/SynchronizedSerializers.java        |   4 +-
 .../collection/UnmodifiableSerializers.java        |   4 +-
 .../fory/serializer/shim/ProtobufDispatcher.java   |   6 +-
 .../fory/serializer/shim/ShimDispatcher.java       |   2 +-
 .../fory-core/native-image.properties              |   4 +
 .../org/apache/fory/logging/Slf4jLoggerTest.java   |  56 +++++++
 .../apache/fory/resolver/ClassResolverTest.java    |  86 +++++++++++
 19 files changed, 515 insertions(+), 34 deletions(-)

diff --git 
a/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java 
b/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java
index d1d3ed029..921410075 100644
--- a/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java
+++ b/java/fory-core/src/main/java/org/apache/fory/logging/ForyLogger.java
@@ -31,6 +31,7 @@ public class ForyLogger implements Logger {
   private static final DateTimeFormatter dateTimeFormatter =
       DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
   private final String name;
+  private final LogOnceState logOnceState = new LogOnceState();
 
   public ForyLogger(Class<?> targetClass) {
     this.name = targetClass.getSimpleName();
@@ -93,6 +94,42 @@ public class ForyLogger implements Logger {
     }
   }
 
+  @Override
+  public void infoOnce(String msg) {
+    if (LoggerFactory.getLogLevel() >= INFO_LEVEL
+        && logOnceState.shouldLog(INFO_LEVEL, msg, LogOnceState.NO_ARGS)) {
+      log(INFO_LEVEL, msg, LogOnceState.NO_ARGS, false);
+    }
+  }
+
+  @Override
+  public void infoOnce(String msg, Object arg) {
+    if (LoggerFactory.getLogLevel() >= INFO_LEVEL) {
+      Object[] args = new Object[] {arg};
+      if (logOnceState.shouldLog(INFO_LEVEL, msg, args)) {
+        log(INFO_LEVEL, msg, args, false);
+      }
+    }
+  }
+
+  @Override
+  public void infoOnce(String msg, Object arg1, Object arg2) {
+    if (LoggerFactory.getLogLevel() >= INFO_LEVEL) {
+      Object[] args = new Object[] {arg1, arg2};
+      if (logOnceState.shouldLog(INFO_LEVEL, msg, args)) {
+        log(INFO_LEVEL, msg, args, false);
+      }
+    }
+  }
+
+  @Override
+  public void infoOnce(String msg, Object... args) {
+    if (LoggerFactory.getLogLevel() >= INFO_LEVEL
+        && logOnceState.shouldLog(INFO_LEVEL, msg, args)) {
+      log(INFO_LEVEL, msg, args, false);
+    }
+  }
+
   @Override
   public void warn(String msg) {
     if (LoggerFactory.getLogLevel() >= WARN_LEVEL) {
@@ -128,6 +165,52 @@ public class ForyLogger implements Logger {
     }
   }
 
+  @Override
+  public void warnOnce(String msg) {
+    if (LoggerFactory.getLogLevel() >= WARN_LEVEL
+        && logOnceState.shouldLog(WARN_LEVEL, msg, LogOnceState.NO_ARGS)) {
+      log(WARN_LEVEL, msg, LogOnceState.NO_ARGS, false);
+    }
+  }
+
+  @Override
+  public void warnOnce(String msg, Object arg) {
+    if (LoggerFactory.getLogLevel() >= WARN_LEVEL) {
+      Object[] args = new Object[] {arg};
+      if (logOnceState.shouldLog(WARN_LEVEL, msg, args)) {
+        log(WARN_LEVEL, msg, args, true);
+      }
+    }
+  }
+
+  @Override
+  public void warnOnce(String msg, Object arg1, Object arg2) {
+    if (LoggerFactory.getLogLevel() >= WARN_LEVEL) {
+      Object[] args = new Object[] {arg1, arg2};
+      if (logOnceState.shouldLog(WARN_LEVEL, msg, args)) {
+        log(WARN_LEVEL, msg, args, true);
+      }
+    }
+  }
+
+  @Override
+  public void warnOnce(String msg, Object... args) {
+    if (LoggerFactory.getLogLevel() >= WARN_LEVEL
+        && logOnceState.shouldLog(WARN_LEVEL, msg, args)) {
+      log(WARN_LEVEL, msg, args, true);
+    }
+  }
+
+  @Override
+  public void warnOnce(String msg, Throwable t) {
+    if (LoggerFactory.getLogLevel() >= WARN_LEVEL) {
+      Object[] args = new Object[] {t};
+      if (logOnceState.shouldLog(WARN_LEVEL, msg, args)) {
+        log(WARN_LEVEL, msg, args, true);
+      }
+    }
+  }
+
   @Override
   public void error(String msg) {
     if (LoggerFactory.getLogLevel() >= ERROR_LEVEL) {
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/logging/LogOnceState.java 
b/java/fory-core/src/main/java/org/apache/fory/logging/LogOnceState.java
new file mode 100644
index 000000000..4878dceb7
--- /dev/null
+++ b/java/fory-core/src/main/java/org/apache/fory/logging/LogOnceState.java
@@ -0,0 +1,161 @@
+/*
+ * 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.logging;
+
+import java.lang.reflect.Member;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+final class LogOnceState {
+  static final Object[] NO_ARGS = new Object[0];
+
+  private final Set<LogKey> logged =
+      Collections.newSetFromMap(new ConcurrentHashMap<LogKey, Boolean>());
+
+  boolean shouldLog(int level, String msg, Object[] args) {
+    return logged.add(new LogKey(level, msg, args));
+  }
+
+  private static final class LogKey {
+    private final int level;
+    private final String msg;
+    private final Object[] args;
+    private final int hashCode;
+
+    private LogKey(int level, String msg, Object[] args) {
+      this.level = level;
+      this.msg = msg;
+      this.args = normalizeArgs(args);
+      int result = level;
+      result = 31 * result + (msg == null ? 0 : msg.hashCode());
+      result = 31 * result + Arrays.deepHashCode(this.args);
+      hashCode = result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof LogKey)) {
+        return false;
+      }
+      LogKey other = (LogKey) obj;
+      return level == other.level && msgEquals(other.msg) && 
Arrays.deepEquals(args, other.args);
+    }
+
+    @Override
+    public int hashCode() {
+      return hashCode;
+    }
+
+    private boolean msgEquals(String otherMsg) {
+      return msg == null ? otherMsg == null : msg.equals(otherMsg);
+    }
+  }
+
+  private static Object[] normalizeArgs(Object[] args) {
+    if (args == null || args.length == 0) {
+      return NO_ARGS;
+    }
+    Object[] normalized = new Object[args.length];
+    for (int i = 0; i < args.length; i++) {
+      normalized[i] = normalizeArg(args[i]);
+    }
+    return normalized;
+  }
+
+  private static Object normalizeArg(Object arg) {
+    if (arg instanceof Class) {
+      Class<?> cls = (Class<?>) arg;
+      return classArg(cls);
+    }
+    if (arg instanceof Member) {
+      Member member = (Member) arg;
+      return new MemberArg(member.toString(), 
classArg(member.getDeclaringClass()));
+    }
+    if (arg instanceof Object[]) {
+      return normalizeArgs((Object[]) arg);
+    }
+    return arg;
+  }
+
+  private static ClassArg classArg(Class<?> cls) {
+    // Once keys live as long as the logger. Store class identity data, not 
the Class object, so
+    // one-time logs do not pin user classes or their classloaders.
+    return new ClassArg(cls.getName(), System.identityHashCode(cls));
+  }
+
+  private static final class ClassArg {
+    private final String name;
+    private final int identityHash;
+
+    private ClassArg(String name, int identityHash) {
+      this.name = name;
+      this.identityHash = identityHash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof ClassArg)) {
+        return false;
+      }
+      ClassArg other = (ClassArg) obj;
+      return identityHash == other.identityHash && name.equals(other.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * identityHash + name.hashCode();
+    }
+  }
+
+  private static final class MemberArg {
+    private final String member;
+    private final ClassArg declaringClass;
+
+    private MemberArg(String member, ClassArg declaringClass) {
+      this.member = member;
+      this.declaringClass = declaringClass;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof MemberArg)) {
+        return false;
+      }
+      MemberArg other = (MemberArg) obj;
+      return member.equals(other.member) && 
declaringClass.equals(other.declaringClass);
+    }
+
+    @Override
+    public int hashCode() {
+      return 31 * declaringClass.hashCode() + member.hashCode();
+    }
+  }
+}
diff --git a/java/fory-core/src/main/java/org/apache/fory/logging/Logger.java 
b/java/fory-core/src/main/java/org/apache/fory/logging/Logger.java
index 74f9aba7d..1e598a59c 100644
--- a/java/fory-core/src/main/java/org/apache/fory/logging/Logger.java
+++ b/java/fory-core/src/main/java/org/apache/fory/logging/Logger.java
@@ -44,6 +44,15 @@ public interface Logger {
 
   void info(String msg, Object... args);
 
+  /** Logs an info message once per logger for the same template and argument 
values. */
+  void infoOnce(String msg);
+
+  void infoOnce(String msg, Object arg);
+
+  void infoOnce(String msg, Object arg1, Object arg2);
+
+  void infoOnce(String msg, Object... args);
+
   void warn(String msg);
 
   void warn(String msg, Object arg);
@@ -54,6 +63,17 @@ public interface Logger {
 
   void warn(String msg, Throwable t);
 
+  /** Logs a warning once per logger for the same template and argument 
values. */
+  void warnOnce(String msg);
+
+  void warnOnce(String msg, Object arg);
+
+  void warnOnce(String msg, Object... args);
+
+  void warnOnce(String msg, Object arg1, Object arg2);
+
+  void warnOnce(String msg, Throwable t);
+
   void error(String msg);
 
   void error(String msg, Object arg);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/logging/Slf4jLogger.java 
b/java/fory-core/src/main/java/org/apache/fory/logging/Slf4jLogger.java
index c20e3583f..3e5911cd5 100644
--- a/java/fory-core/src/main/java/org/apache/fory/logging/Slf4jLogger.java
+++ b/java/fory-core/src/main/java/org/apache/fory/logging/Slf4jLogger.java
@@ -33,6 +33,7 @@ public class Slf4jLogger implements Logger {
   private final boolean isLocationAwareLogger;
 
   private final org.slf4j.Logger logger;
+  private final LogOnceState logOnceState = new LogOnceState();
 
   public Slf4jLogger(Class<?> cls) {
     this.logger = org.slf4j.LoggerFactory.getLogger(cls);
@@ -94,6 +95,34 @@ public class Slf4jLogger implements Logger {
     }
   }
 
+  @Override
+  public void infoOnce(String msg) {
+    infoOnce(msg, (Object[]) null);
+  }
+
+  @Override
+  public void infoOnce(String msg, Object arg) {
+    infoOnce(msg, new Object[] {arg});
+  }
+
+  @Override
+  public void infoOnce(String msg, Object arg1, Object arg2) {
+    infoOnce(msg, new Object[] {arg1, arg2});
+  }
+
+  @Override
+  public void infoOnce(String msg, Object... args) {
+    if (LoggerFactory.getLogLevel() < INFO_LEVEL
+        || !logOnceState.shouldLog(INFO_LEVEL, msg, args)) {
+      return;
+    }
+    if (isLocationAwareLogger) {
+      ((LocationAwareLogger) logger).log(null, FQCN, 
LocationAwareLogger.INFO_INT, msg, args, null);
+    } else {
+      logger.info(msg, args);
+    }
+  }
+
   @Override
   public void warn(String msg) {
     warn(msg, (Object[]) null);
@@ -133,6 +162,48 @@ public class Slf4jLogger implements Logger {
     }
   }
 
+  @Override
+  public void warnOnce(String msg) {
+    warnOnce(msg, (Object[]) null);
+  }
+
+  @Override
+  public void warnOnce(String msg, Object arg) {
+    warnOnce(msg, new Object[] {arg});
+  }
+
+  @Override
+  public void warnOnce(String msg, Object arg1, Object arg2) {
+    warnOnce(msg, new Object[] {arg1, arg2});
+  }
+
+  @Override
+  public void warnOnce(String msg, Object... args) {
+    if (LoggerFactory.getLogLevel() < WARN_LEVEL
+        || !logOnceState.shouldLog(WARN_LEVEL, msg, args)) {
+      return;
+    }
+    if (isLocationAwareLogger) {
+      ((LocationAwareLogger) logger).log(null, FQCN, 
LocationAwareLogger.WARN_INT, msg, args, null);
+    } else {
+      logger.warn(msg, args);
+    }
+  }
+
+  @Override
+  public void warnOnce(String msg, Throwable t) {
+    Object[] args = new Object[] {t};
+    if (LoggerFactory.getLogLevel() < WARN_LEVEL
+        || !logOnceState.shouldLog(WARN_LEVEL, msg, args)) {
+      return;
+    }
+    if (isLocationAwareLogger) {
+      ((LocationAwareLogger) logger).log(null, FQCN, 
LocationAwareLogger.WARN_INT, msg, null, t);
+    } else {
+      logger.warn(msg, t);
+    }
+  }
+
   @Override
   public void error(String msg) {
     error(msg, (Object[]) null);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java
index 704e6bd7f..b414c12b7 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java
@@ -106,7 +106,7 @@ public class AllowListChecker implements TypeChecker {
               String.format("Class %s is forbidden for serialization.", 
className));
         }
         if (!containsPrefix(allowList, allowListPrefix, className)) {
-          LOG.warn(
+          LOG.warnOnce(
               "Class {} not in allow list, please check whether objects of 
this class "
                   + "are allowed for serialization.",
               className);
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 b0c8caf7c..7b12fae6b 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
@@ -1303,12 +1303,12 @@ public class ClassResolver extends TypeResolver {
   public void setSerializer(String className, Class<? extends Serializer> 
serializer) {
     for (Map.Entry<Class<?>, TypeInfo> entry : classInfoMap.iterable()) {
       if (extRegistry.registeredClasses.get(className) != null) {
-        LOG.warn("Skip clear serializer for registered class {}", className);
+        LOG.warnOnce("Skip clear serializer for registered class {}", 
className);
         return;
       }
       Class<?> cls = entry.getKey();
       if (cls.getName().equals(className)) {
-        LOG.info("Clear serializer for class {}.", className);
+        LOG.infoOnce("Clear serializer for class {}.", className);
         entry.getValue().setSerializer(this, Serializers.newSerializer(this, 
cls, serializer));
         clearTypeInfoCache();
         return;
@@ -1325,7 +1325,7 @@ public class ClassResolver extends TypeResolver {
         continue;
       }
       if (className.startsWith(classNamePrefix)) {
-        LOG.info("Clear serializer for class {}.", className);
+        LOG.infoOnce("Clear serializer for class {}.", className);
         entry.getValue().setSerializer(this, Serializers.newSerializer(this, 
cls, serializer));
         clearTypeInfoCache();
       }
@@ -1554,7 +1554,9 @@ public class ClassResolver extends TypeResolver {
         }
       }
       if (isCrossLanguage()) {
-        LOG.warn("Class {} isn't supported for cross-language serialization.", 
cls);
+        if (!config.suppressClassRegistrationWarnings()) {
+          LOG.warnOnce("Class {} isn't supported for cross-language 
serialization.", cls);
+        }
       }
       if (useReplaceResolveSerializer(cls)) {
         return ReplaceResolveSerializer.class;
@@ -1632,7 +1634,7 @@ public class ClassResolver extends TypeResolver {
       }
     } else {
       if (codegen) {
-        LOG.info("Object of type {} can't be serialized by jit", cls);
+        LOG.infoOnce("Object of type {} can't be serialized by jit", cls);
       }
       return staticSerializerClass == null ? ObjectSerializer.class : 
staticSerializerClass;
     }
@@ -1759,7 +1761,7 @@ public class ClassResolver extends TypeResolver {
           && !hasGraalvmSerializerClass(cls)
           && !shimDispatcher.contains(cls)
           && !extRegistry.isTypeCheckerSet()) {
-        LOG.warn(generateSecurityMsg(cls));
+        LOG.warnOnce(generateSecurityMsg(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 8837fb8e5..fde985f4e 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
@@ -1261,7 +1261,7 @@ public abstract class TypeResolver {
         sc = CompatibleSerializer.class;
       } else if (sc == null && GraalvmSupport.isGraalRuntime()) {
         sc = CompatibleSerializer.class;
-        LOG.warn(
+        LOG.warnOnce(
             "Can't generate class at runtime in graalvm for class def {}, use 
{} instead",
             typeDef,
             sc);
@@ -1431,7 +1431,9 @@ public abstract class TypeResolver {
                 "Class %s not found from classloaders [%s, %s]",
                 className, extRegistry.classLoader, 
Thread.currentThread().getContextClassLoader());
         if (deserializeUnknownClass) {
-          LOG.warn(msg);
+          if (!config.suppressClassRegistrationWarnings()) {
+            LOG.warnOnce(msg);
+          }
           return UnknownClass.getUnknowClass(className, isEnum, arrayDims, 
metaContextShareEnabled);
         }
         throw new IllegalStateException(msg, ex);
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 ef8ffba6d..48e40ef72 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
@@ -1345,7 +1345,9 @@ public class XtypeResolver extends TypeResolver {
     String msg = String.format("Class %s not registered", qualifiedName);
     Class<?> type = null;
     if (config.deserializeUnknownClass()) {
-      LOG.warn(msg);
+      if (!config.suppressClassRegistrationWarnings()) {
+        LOG.warnOnce(msg);
+      }
       switch (typeId) {
         case Types.NAMED_ENUM:
         case Types.NAMED_STRUCT:
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java 
b/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java
index d0c2007f8..b9f13d2d5 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/JavaSerializer.java
@@ -67,7 +67,7 @@ public class JavaSerializer extends Serializer<Object> {
     // TODO(chgaokunyang) enable this check when ObjectSerializer is 
implemented.
     // 
Preconditions.checkArgument(ClassResolver.requireJavaSerialization(cls));
     if (cls != SerializedLambda.class) {
-      LOG.warn(
+      LOG.warnOnce(
           "{} use java built-in serialization, which is inefficient. "
               + "Please replace it with a {} or implements {}",
           cls,
@@ -209,7 +209,7 @@ public class JavaSerializer extends Serializer<Object> {
           && readResolve.getReturnType() == Object.class) {
         return readResolve;
       } else {
-        LOG.warn(
+        LOG.warnOnce(
             "`readResolve` method doesn't match signature: 
`ANY-ACCESS-MODIFIER Object readResolve()`");
       }
     }
@@ -230,7 +230,7 @@ public class JavaSerializer extends Serializer<Object> {
           && writeReplace.getReturnType() == Object.class) {
         return writeReplace;
       } else {
-        LOG.warn(
+        LOG.warnOnce(
             "`writeReplace` method doesn't match signature: 
`ANY-ACCESS-MODIFIER Object writeReplace()");
       }
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
index 8f310efc7..a29d6d639 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/ObjectStreamSerializer.java
@@ -172,7 +172,7 @@ public class ObjectStreamSerializer extends 
AbstractObjectSerializer {
         // In GraalVM native image, ObjectStreamClass.lookup may fail for 
certain classes due to
         // missing SerializationConstructorAccessor. Returning null keeps 
stream reconstruction on
         // the serializer-owned object instantiator path.
-        LOG.warn(
+        LOG.warnOnce(
             "ObjectStreamClass.lookup failed for {} in GraalVM native image: 
{}",
             type.getName(),
             e.getMessage());
@@ -191,7 +191,7 @@ public class ObjectStreamSerializer extends 
AbstractObjectSerializer {
           String.format("Class %s should implement %s.", type, 
Serializable.class));
     }
     if (!Throwable.class.isAssignableFrom(type)) {
-      LOG.warn(
+      LOG.warnOnce(
           "{} customized jdk serialization, which is inefficient. "
               + "Please replace it with a {} or implements {}",
           type,
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 55d8e302c..c07ae3132 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
@@ -86,14 +86,14 @@ public class ReplaceResolveSerializer extends Serializer {
         writeReplaceMethod = JavaSerializer.getWriteReplaceMethod(cls);
         readResolveMethod = JavaSerializer.getReadResolveMethod(cls);
         if (writeReplaceMethod != null) {
-          LOG.warn(
+          LOG.warnOnce(
               "{} doesn't implement {}, but defined writeReplace method {}",
               cls,
               Serializable.class,
               writeReplaceMethod);
         }
         if (readResolveMethod != null) {
-          LOG.warn(
+          LOG.warnOnce(
               "{} doesn't implement {}, but defined readResolve method {}",
               cls,
               Serializable.class,
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java
index 0d390ac17..e81fa246a 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SubListSerializers.java
@@ -109,7 +109,6 @@ public class SubListSerializers {
   public static final class SubListSerializer extends 
CollectionSerializer<List> {
     private final ViewFields viewFields;
     private final int subListOwnerBytes;
-    private boolean serializedViewBefore;
 
     public SubListSerializer(TypeResolver typeResolver, Class<List> type) {
       this(typeResolver, type, false);
@@ -193,13 +192,10 @@ public class SubListSerializers {
     }
 
     private void checkViewSerialization(Object value) {
-      if (!serializedViewBefore) {
-        serializedViewBefore = true;
-        LOG.warn(
-            "List view of type {} is being serialized/deserialized. Fory 
writes the source list, "
-                + "offset, and size so JVM and Android readers can parse the 
same payload.",
-            value.getClass());
-      }
+      LOG.warnOnce(
+          "List view of type {} is being serialized/deserialized. Fory writes 
the source list, "
+              + "offset, and size so JVM and Android readers can parse the 
same payload.",
+          value.getClass());
     }
   }
 
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java
index 764004368..a6619d56e 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/SynchronizedSerializers.java
@@ -64,7 +64,7 @@ public class SynchronizedSerializers {
         SOURCE_COLLECTION_ACCESSOR =
             
FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("c"));
       } catch (Exception e) {
-        LOG.info("Could not access source collection field in {}", clsName);
+        LOG.infoOnce("Could not access source collection field in {}", 
clsName);
         throw new RuntimeException(e);
       }
       clsName = "java.util.Collections$SynchronizedMap";
@@ -72,7 +72,7 @@ public class SynchronizedSerializers {
         SOURCE_MAP_ACCESSOR =
             
FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("m"));
       } catch (Exception e) {
-        LOG.info("Could not access source map field in {}", clsName);
+        LOG.infoOnce("Could not access source map field in {}", clsName);
         throw new RuntimeException(e);
       }
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java
index 8b799da5b..fa77d540b 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/UnmodifiableSerializers.java
@@ -64,7 +64,7 @@ public class UnmodifiableSerializers {
         SOURCE_COLLECTION_ACCESSOR =
             
FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("c"));
       } catch (Exception e) {
-        LOG.info("Could not access source collection field in {}", clsName);
+        LOG.infoOnce("Could not access source collection field in {}", 
clsName);
         throw new RuntimeException(e);
       }
       clsName = "java.util.Collections$UnmodifiableMap";
@@ -73,7 +73,7 @@ public class UnmodifiableSerializers {
         SOURCE_MAP_ACCESSOR =
             
FieldAccessor.createAccessor(Class.forName(clsName).getDeclaredField("m"));
       } catch (Exception e) {
-        LOG.info("Could not access source map field in {}", clsName);
+        LOG.infoOnce("Could not access source map field in {}", clsName);
         throw new RuntimeException(e);
       }
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ProtobufDispatcher.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ProtobufDispatcher.java
index 3c099115a..b2c5fc281 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ProtobufDispatcher.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ProtobufDispatcher.java
@@ -19,7 +19,6 @@
 
 package org.apache.fory.serializer.shim;
 
-import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.fory.Fory;
 import org.apache.fory.logging.Logger;
 import org.apache.fory.logging.LoggerFactory;
@@ -35,7 +34,6 @@ public class ProtobufDispatcher {
   private static Class<? extends Serializer> pbByteStringSerializerClass;
   private static Class<?> pbMessageClass;
   private static Class<? extends Serializer> pbMessageSerializerClass;
-  private static final AtomicBoolean warningLogged = new AtomicBoolean(false);
 
   static {
     try {
@@ -63,8 +61,8 @@ public class ProtobufDispatcher {
       return null;
     }
     if (pbMessageSerializerClass == null) {
-      if (type.getName().startsWith("com.google.protobuf") && 
!warningLogged.getAndSet(true)) {
-        LOG.warn("ProtobufSerializer not loaded, please add fory-extensions 
dependency.");
+      if (type.getName().startsWith("com.google.protobuf")) {
+        LOG.warnOnce("ProtobufSerializer not loaded, please add 
fory-extensions dependency.");
       }
       return null;
     }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ShimDispatcher.java
 
b/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ShimDispatcher.java
index 4325a8452..84087c670 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ShimDispatcher.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/shim/ShimDispatcher.java
@@ -77,7 +77,7 @@ public class ShimDispatcher {
     try {
       return Serializers.newSerializer(typeResolver, clazz, serializerClass);
     } catch (Throwable e) {
-      LOG.warn(
+      LOG.warnOnce(
           "Construct shim serializer failed for class [{}] with serializer 
class [{}]",
           className,
           serializerClass);
diff --git 
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
 
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
index d607b3406..6cef0befe 100644
--- 
a/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
+++ 
b/java/fory-core/src/main/resources/META-INF/native-image/org.apache.fory/fory-core/native-image.properties
@@ -214,6 +214,10 @@ 
Args=--initialize-at-build-time=org.apache.fory.annotation.ForyField$Dynamic,\
     org.apache.fory.logging.ForyLogger,\
     org.apache.fory.logging.LoggerFactory,\
     org.apache.fory.logging.LogLevel,\
+    org.apache.fory.logging.LogOnceState,\
+    org.apache.fory.logging.LogOnceState$ClassArg,\
+    org.apache.fory.logging.LogOnceState$LogKey,\
+    org.apache.fory.logging.LogOnceState$MemberArg,\
     org.apache.fory.memory.BoundsChecking,\
     org.apache.fory.memory.MemoryBuffer,\
     org.apache.fory.memory.MemoryAllocator,\
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/logging/Slf4jLoggerTest.java 
b/java/fory-core/src/test/java/org/apache/fory/logging/Slf4jLoggerTest.java
index 154655230..615cd5233 100644
--- a/java/fory-core/src/test/java/org/apache/fory/logging/Slf4jLoggerTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/logging/Slf4jLoggerTest.java
@@ -21,6 +21,9 @@ package org.apache.fory.logging;
 
 import static org.testng.Assert.assertEquals;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
 import org.testng.annotations.Test;
 
 public class Slf4jLoggerTest {
@@ -31,10 +34,14 @@ public class Slf4jLoggerTest {
     ForyLogger foryLogger = new ForyLogger((Slf4jLoggerTest.class));
     logger.info("testInfo");
     logger.info("testInfo {}", "placeHolder");
+    logger.infoOnce("testInfo {}", "placeHolder");
     logger.warn("testInfo {}", "placeHolder");
+    logger.warnOnce("testInfo {}", "placeHolder");
     foryLogger.info("testInfo");
     foryLogger.info("testInfo {}", "placeHolder");
+    foryLogger.infoOnce("testInfo {}", "placeHolder");
     foryLogger.warn("testInfo {}", "placeHolder");
+    foryLogger.warnOnce("testInfo {}", "placeHolder");
     int previousLogLevel = LoggerFactory.getLogLevel();
     try {
       LoggerFactory.disableLogging();
@@ -48,6 +55,42 @@ public class Slf4jLoggerTest {
     }
   }
 
+  @Test
+  public void testLogOnce() throws Exception {
+    ForyLogger logger = new ForyLogger(Slf4jLoggerTest.class);
+    int previousLogLevel = LoggerFactory.getLogLevel();
+    PrintStream previousOut = System.out;
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    try {
+      LoggerFactory.setLogLevel(LogLevel.INFO_LEVEL);
+      System.setOut(new PrintStream(out, true, StandardCharsets.UTF_8.name()));
+      logger.infoOnce("arg-once {}", "alpha");
+      logger.infoOnce("arg-once {}", "alpha");
+      logger.infoOnce("arg-once {}", "beta");
+      logger.infoOnce("level-once {}", "alpha");
+      logger.warnOnce("level-once {}", "alpha");
+      logger.warnOnce("level-once {}", "alpha");
+      logger.warnOnce("class-once {}", Slf4jLoggerTest.class);
+      logger.warnOnce("class-once {}", Slf4jLoggerTest.class);
+      logger.warnOnce("method-once {}", String.class.getMethod("trim"));
+      logger.warnOnce("method-once {}", String.class.getMethod("trim"));
+      LoggerFactory.disableLogging();
+      logger.warnOnce("disabled-once {}", "alpha");
+      LoggerFactory.setLogLevel(LogLevel.WARN_LEVEL);
+      logger.warnOnce("disabled-once {}", "alpha");
+    } finally {
+      System.setOut(previousOut);
+      LoggerFactory.setLogLevel(previousLogLevel);
+    }
+    String logs = out.toString(StandardCharsets.UTF_8.name());
+    assertEquals(count(logs, " - arg-once alpha"), 1);
+    assertEquals(count(logs, " - arg-once beta"), 1);
+    assertEquals(count(logs, " - level-once alpha"), 2);
+    assertEquals(count(logs, " - class-once class 
org.apache.fory.logging.Slf4jLoggerTest"), 1);
+    assertEquals(count(logs, " - method-once "), 1);
+    assertEquals(count(logs, " - disabled-once alpha"), 1);
+  }
+
   @Test
   public void testDefaultLogLevel() {
     assertEquals(LoggerFactory.getLogLevel(), LogLevel.DEFAULT_LEVEL);
@@ -70,4 +113,17 @@ public class Slf4jLoggerTest {
     assertEquals(LogLevel.getDefaultLogLevel("unknown", false), 
LogLevel.WARN_LEVEL);
     assertEquals(LogLevel.getDefaultLogLevel("unknown", true), 
LogLevel.INFO_LEVEL);
   }
+
+  private static int count(String text, String pattern) {
+    int count = 0;
+    int from = 0;
+    while (true) {
+      int index = text.indexOf(pattern, from);
+      if (index < 0) {
+        return count;
+      }
+      count++;
+      from = index + pattern.length();
+    }
+  }
 }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java 
b/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
index 0b94da34c..5f5a17fc5 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/resolver/ClassResolverTest.java
@@ -28,6 +28,9 @@ import static org.testng.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.primitives.Primitives;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -56,10 +59,13 @@ import org.apache.fory.context.ReadContext;
 import org.apache.fory.context.WriteContext;
 import org.apache.fory.exception.ForyException;
 import org.apache.fory.exception.InsecureException;
+import org.apache.fory.logging.LogLevel;
 import org.apache.fory.logging.Logger;
 import org.apache.fory.logging.LoggerFactory;
 import org.apache.fory.memory.MemoryBuffer;
 import org.apache.fory.memory.MemoryUtils;
+import org.apache.fory.meta.EncodedMetaString;
+import org.apache.fory.meta.Encoders;
 import org.apache.fory.meta.TypeDef;
 import org.apache.fory.reflect.TypeRef;
 import org.apache.fory.resolver.longlongpkg.C1;
@@ -287,6 +293,33 @@ public class ClassResolverTest extends ForyTestBase {
         MapSerializers.DefaultJavaMapSerializer.class);
   }
 
+  @Test
+  public void testSuppressXtypeWarnings() throws Exception {
+    String suppressed =
+        captureOutput(
+            () ->
+                resolveMissingXtype(
+                    Fory.builder()
+                        .withXlang(true)
+                        .withMetaShare(true)
+                        .withDeserializeUnknownClass(true)
+                        .suppressClassRegistrationWarnings(true)
+                        .build()));
+    assertEquals(count(suppressed, "Class missing.pkg.MissingType not 
registered"), 0);
+
+    String unsuppressed =
+        captureOutput(
+            () ->
+                resolveMissingXtype(
+                    Fory.builder()
+                        .withXlang(true)
+                        .withMetaShare(true)
+                        .withDeserializeUnknownClass(true)
+                        .suppressClassRegistrationWarnings(false)
+                        .build()));
+    assertEquals(count(unsuppressed, "Class missing.pkg.MissingType not 
registered"), 1);
+  }
+
   @Test
   public void testSharedRegistrySharesTypeDefCachesAcrossForyInstances() {
     ForyBuilder builder =
@@ -1370,4 +1403,57 @@ public class ClassResolverTest extends ForyTestBase {
     Assert.assertEquals(result[0].getValue(), 1);
     Assert.assertEquals(result[1].getValue(), 2);
   }
+
+  private static String captureOutput(Runnable action) throws Exception {
+    int previousLogLevel = LoggerFactory.getLogLevel();
+    PrintStream previousOut = System.out;
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    try {
+      LoggerFactory.setLogLevel(LogLevel.WARN_LEVEL);
+      System.setOut(new PrintStream(out, true, StandardCharsets.UTF_8.name()));
+      action.run();
+    } finally {
+      System.setOut(previousOut);
+      LoggerFactory.setLogLevel(previousLogLevel);
+    }
+    return out.toString(StandardCharsets.UTF_8.name());
+  }
+
+  private static void resolveMissingXtype(Fory fory) {
+    try {
+      Method method =
+          XtypeResolver.class.getDeclaredMethod(
+              "loadBytesToTypeInfoWithTypeId",
+              int.class,
+              EncodedMetaString.class,
+              EncodedMetaString.class);
+      method.setAccessible(true);
+      method.invoke(
+          fory.getTypeResolver(),
+          Types.NAMED_STRUCT,
+          Encoders.PACKAGE_ENCODER.encodeBinary("missing.pkg"),
+          Encoders.TYPE_NAME_ENCODER.encodeBinary("MissingType"));
+    } catch (InvocationTargetException e) {
+      Throwable cause = e.getCause();
+      if (!(cause instanceof IllegalStateException)
+          || !cause.getMessage().contains("missing.pkg.MissingType")) {
+        throw new AssertionError(e);
+      }
+    } catch (ReflectiveOperationException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  private static int count(String text, String pattern) {
+    int count = 0;
+    int from = 0;
+    while (true) {
+      int index = text.indexOf(pattern, from);
+      if (index < 0) {
+        return count;
+      }
+      count++;
+      from = index + pattern.length();
+    }
+  }
 }


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


Reply via email to