为Java日志File Handler的Formatter获取调用类名
获取包装Logger后实际调用日志的类
好问题!当你对Logger做了包装后,LogRecord.getSourceClassName()确实会返回包装类的名称,而不是实际调用log方法的业务类——这是因为默认的LogRecord在Logger.log被调用时,会把当前执行的类(也就是你的包装类)填充为源类名,所以这个方法自然就失效了。
不过我们可以通过线程栈轨迹来绕过这个限制,直接找到实际发起日志调用的类。下面是具体的实现思路和代码示例:
实现步骤
- 在自定义
Formatter的format方法中,获取当前线程的栈轨迹数组。 - 遍历栈轨迹,过滤掉
java.util.logging包下的类、你的日志包装类,找到第一个符合条件的栈元素。 - 从这个栈元素中提取实际调用类的名称。
代码示例
假设你的日志包装类叫WrappedLogger,自定义Formatter可以这么写:
import java.util.logging.Formatter; import java.util.logging.LogRecord; public class CustomFormatter extends Formatter { // 定义需要排除的类前缀 private static final String[] EXCLUDED_PREFIXES = { "java.util.logging.", "com.yourpackage.WrappedLogger" // 替换成你的包装类全限定名 }; @Override public String format(LogRecord record) { // 获取实际调用类的名称 String actualClassName = getActualCallingClassName(); // 组装日志内容,这里可以根据你的需求自定义格式 return String.format("[%s] [%s] %s%n", actualClassName, record.getLevel().getName(), record.getMessage()); } private String getActualCallingClassName() { // 获取当前线程的栈轨迹 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); // 遍历栈元素,跳过不需要的类 for (StackTraceElement element : stackTrace) { String className = element.getClassName(); boolean isExcluded = false; // 检查是否在排除列表中 for (String prefix : EXCLUDED_PREFIXES) { if (className.startsWith(prefix)) { isExcluded = true; break; } } if (!isExcluded) { return className; } } // 如果找不到,返回默认值 return "UnknownClass"; } }
注意事项
- 性能影响:获取线程栈轨迹是相对耗时的操作,如果你的系统日志量极大,可能会有一定性能开销。如果是普通业务场景,这个影响可以忽略不计。
- 精确性调整:如果你的日志包装类有多层,或者有其他中间类需要排除,可以在
EXCLUDED_PREFIXES中添加对应的前缀或全类名。 - JVM优化:某些JVM优化可能会影响栈轨迹的准确性,比如方法内联。如果遇到这种情况,可以考虑在包装类的
log方法中手动填充LogRecord的源类名。
更高效的可选方案:包装时手动传递调用类
如果你觉得遍历栈轨迹太耗时,也可以在包装类的log方法中,提前获取调用类的信息,然后设置到LogRecord中:
import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; public class WrappedLogger { private final Logger delegate; public WrappedLogger(Logger delegate) { this.delegate = delegate; } public void log(Level level, String msg) { // 获取调用类的名称(跳过当前类和Thread类的栈元素) String callingClassName = Thread.currentThread().getStackTrace()[2].getClassName(); LogRecord record = new LogRecord(level, msg); record.setSourceClassName(callingClassName); delegate.log(record); } }
这种方式只在包装类的log方法中获取一次栈轨迹,比在Formatter中每次都获取更高效,而且Formatter可以直接使用record.getSourceClassName()拿到正确的类名。
内容的提问来源于stack exchange,提问作者Ian Lim




