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]