如何使用java.util.logging在运行时为日志添加小程序名称前缀?
问题
我有一个运行在Tomcat上的Java Web应用,它可解释并运行其他小型Java应用(Beanshells)。主应用与小程序均使用默认的java.util.logging,主应用通过logging.properties文件配置将日志写入独立于Tomcat主日志的文件中。
我希望在小程序解释器运行时,将当前小程序名称添加到每条日志语句开头。使用默认的java.util.logging能否实现这一需求?
我曾尝试在logging.properties的格式字符串中使用名为app.name的系统属性,在运行时设置和清除该属性,但未成功——日志内容前未出现预期前缀。相关配置与代码如下:
主应用logging.properties配置
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler org.apache.juli.FileHandler.level=INFO org.apache.juli.FileHandler.directory=${catalina.base}/logs org.apache.juli.FileHandler.prefix=apprunner. org.apache.juli.FileHandler.encoding=UTF-8 org.apache.juli.FileHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.ConsoleHandler.level=INFO java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format=${app.name} %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n
com.apprunner.App类的evalJava方法代码
logger.info("### BEGIN JAVA EXECUTION ###"); System.setProperty("app.name", ui_object_name); logger.info(System.getProperty("app.name"));
实际日志输出(apprunner.[date].log)
Jul 19, 2023 4:09:29 PM com.apprunner.App evalJava INFO: ### BEGIN JAVA EXECUTION ### Jul 19, 2023 4:09:29 PM com.apprunner.App evalJava INFO: testapp/app/testapp.java
解决方案
可以实现,但直接用系统属性的方式行不通——java.util.logging.SimpleFormatter的格式字符串在Handler初始化阶段就会解析系统属性,后续运行时修改System.setProperty不会触发格式字符串的重新解析,所以你的尝试无法生效。以下是两种可行方案:
方案一:自定义Formatter结合ThreadLocal
这种方式适合多并发场景,通过线程隔离的方式存储当前小程序名称,避免干扰其他请求。
- 编写自定义Formatter类
import java.io.PrintWriter; import java.io.StringWriter; import java.util.logging.Formatter; import java.util.logging.LogRecord; public class AppNameFormatter extends Formatter { // ThreadLocal存储当前线程的小程序名称,线程隔离避免并发冲突 private static final ThreadLocal<String> CURRENT_APP_NAME = new ThreadLocal<>(); // 设置当前小程序名称的静态方法 public static void setCurrentAppName(String appName) { CURRENT_APP_NAME.set(appName); } // 清除当前线程的小程序名称,防止线程池复用导致的残留 public static void clearCurrentAppName() { CURRENT_APP_NAME.remove(); } @Override public String format(LogRecord record) { String appName = CURRENT_APP_NAME.get(); // 无小程序名称时用空前缀,也可设置默认值如"[UNKNOWN]" String prefix = appName != null ? appName + " " : ""; // 按需求拼接日志格式 return String.format( "%s%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n", prefix, record.getMillis(), record.getLoggerName(), record.getLevel().getLocalizedName(), formatMessage(record), record.getThrown() != null ? "\n" + getStackTrace(record.getThrown()) : "" ); } // 辅助方法:转换异常堆栈为字符串 private String getStackTrace(Throwable thrown) { StringWriter sw = new StringWriter(); thrown.printStackTrace(new PrintWriter(sw)); return sw.toString(); } }
- 修改logging.properties配置
将原有的SimpleFormatter替换为自定义的AppNameFormatter:
handlers = org.apache.juli.FileHandler, java.util.logging.ConsoleHandler org.apache.juli.FileHandler.level=INFO org.apache.juli.FileHandler.directory=${catalina.base}/logs org.apache.juli.FileHandler.prefix=apprunner. org.apache.juli.FileHandler.encoding=UTF-8 org.apache.juli.FileHandler.formatter=com.apprunner.AppNameFormatter java.util.logging.ConsoleHandler.level=INFO java.util.logging.ConsoleHandler.formatter=com.apprunner.AppNameFormatter
- 调整evalJava方法逻辑
在小程序执行前后设置并清除ThreadLocal值,务必在finally块中清除,避免线程池复用导致的名称残留:
logger.info("### BEGIN JAVA EXECUTION ###"); try { AppNameFormatter.setCurrentAppName(ui_object_name); logger.info("小程序开始运行"); // 执行小程序核心逻辑 } finally { AppNameFormatter.clearCurrentAppName(); }
方案二:为每个小程序创建独立Logger
如果小程序数量不多且有唯一标识,可以为每个小程序创建专属Logger,利用Logger名称作为日志前缀。
- 修改evalJava方法
logger.info("### BEGIN JAVA EXECUTION ###"); Logger appLogger = Logger.getLogger(ui_object_name); appLogger.info("小程序开始运行"); // 执行小程序逻辑,使用appLogger输出日志
- 调整logging.properties格式
在格式字符串中加入Logger名称(即小程序名称),%2$s对应Logger名称占位符:
java.util.logging.SimpleFormatter.format=%2$s %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %5$s%6$s%n
注意事项
- 方案一中的ThreadLocal必须在finally块清除,Tomcat使用线程池复用线程,残留的名称会导致后续请求日志异常。
- 自定义Formatter需放在主应用类路径下,确保Tomcat能正常加载。
内容的提问来源于stack exchange,提问作者Carter Smith
相关产品推荐
相关产品推荐

