WildFly 26中为异常日志添加ThreadLocal上下文信息的优雅实现方案咨询
首先得说你的思路方向是对的,但redeploy时的类加载器问题确实是Java EE应用常见的“坑”——不同部署的类是不同类加载器加载的,所以instanceof判断会失效,反射解包确实太丑了。咱们一步步来解决:
先解决眼前的重复包装问题:用@PreDestroy清理是完全靠谱的
WildFly在应用卸载(包括redeploy流程中的旧应用卸载)时,对于标注了@Singleton @Startup的Bean,@PreDestroy方法是会被可靠触发的——只要不是极端情况(比如直接kill进程、服务器断电),这个回调是容器保证的。
你只需要给LoggingInitializer加上清理方法,把包装的Handler还原回原始状态就行:
@PreDestroy public void cleanup() { unwrapHandlers(LogContext.getSystemLogContext().getLogger("")); logger.info("Logging handlers unwrapped during application undeployment"); } private void unwrapHandlers(Logger logger) { Handler[] handlers = logger.getHandlers(); for (Handler handler : handlers) { // 用类名判断,绕过类加载器的问题 if (handler.getClass().getName().equals(WrappingHandler.class.getName())) { logger.removeHandler(handler); try { Field wrappedField = handler.getClass().getDeclaredField("wrapped"); wrappedField.setAccessible(true); Handler originalHandler = (Handler) wrappedField.get(handler); logger.addHandler(originalHandler); } catch (IllegalAccessException | NoSuchFieldException e) { logger.warning("Failed to unwrap logging handler: " + e.getMessage()); // 兜底:如果解包失败,把当前Handler加回去,避免日志断流 logger.addHandler(handler); } } } }
这样每次redeploy时,旧应用的@PreDestroy会先把Handler还原,新应用启动时再重新包装,就不会出现多层嵌套的问题了,比你现在的反射重新包装要整洁得多。
更优雅的核心需求实现:别自己包装Handler,用标准方案
你的核心需求是给异常日志附加ThreadLocal里的上下文信息,其实完全不需要自己去包装Handler,有几个更简单的标准方案:
方案1:直接修改原LogRecord的消息,不用额外日志行
你现在是额外输出一条日志,其实可以把ThreadLocal的上下文直接追加到原异常日志的消息里,一条日志包含所有信息,更整洁:
@Override public void publish(LogRecord record) { // 只针对异常日志处理 if (record.getThrown() != null) { // 替换成你的ThreadLocal持有类,获取上下文信息 String contextInfo = YourContextHolder.getInfo(); record.setMessage(String.format("[%s] %s", contextInfo, record.getMessage())); } wrapped.publish(record); }
这样既满足了需求,又少输出了一条冗余日志,可读性更好。
方案2:用Filter代替Handler包装,更轻量
WildFly的日志Handler支持添加Filter组件,Filter可以修改LogRecord或者决定是否输出,比替换整个Handler要轻量得多:
public class ContextInjectingFilter implements Filter { @Override public boolean isLoggable(LogRecord record) { if (record.getThrown() != null) { String contextInfo = YourContextHolder.getInfo(); record.setMessage("[" + contextInfo + "] " + record.getMessage()); } return true; // 允许日志输出 } }
然后初始化时给每个Handler添加这个Filter,而不是替换Handler:
private void addContextFilter(Logger logger) { Handler[] handlers = logger.getHandlers(); for (Handler handler : handlers) { // 检查是否已经加过这个Filter(用类名判断) boolean hasFilter = false; for (Filter filter : handler.getFilters()) { if (filter.getClass().getName().equals(ContextInjectingFilter.class.getName())) { hasFilter = true; break; } } if (!hasFilter) { handler.addFilter(new ContextInjectingFilter()); } } }
redeploy时的清理也更简单:在@PreDestroy中遍历Handler,移除对应的Filter即可。
方案3:用MDC(映射诊断上下文),零代码侵入日志组件
这是最优雅的方案——MDC就是专门用来解决“给日志附加请求上下文”的问题的,WildFly基于JBoss Logging,完全支持MDC:
- 在请求入口注入上下文:写一个REST拦截器,把ThreadLocal里的信息放到MDC中
@Provider public class MdcContextInterceptor implements ContainerRequestFilter, ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { // 从请求/ThreadLocal中获取你需要的信息 String user = requestContext.getSecurityContext().getUserPrincipal().getName(); String endpoint = requestContext.getUriInfo().getPath(); // 放入MDC org.jboss.logging.MDC.put("user", user); org.jboss.logging.MDC.put("endpoint", endpoint); } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { // 请求结束后清理MDC,避免内存泄漏 org.jboss.logging.MDC.remove("user"); org.jboss.logging.MDC.remove("endpoint"); } }
- 修改WildFly的日志格式:在
standalone.xml或domain.xml中配置日志格式,直接输出MDC字段
<subsystem xmlns="urn:jboss:domain:logging:8.0"> <formatter name="COLOR-PATTERN"> <pattern-formatter pattern="%d{HH:mm:ss,SSS} %-5p [%c] (%t) [user=%X{user}, endpoint=%X{endpoint}] %s%e%n"/> </formatter> <!-- 其他日志配置(如console-handler、file-handler)引用这个formatter即可 --> </subsystem>
这样所有日志(包括异常日志)会自动带上user和endpoint字段,完全不需要修改任何Handler或Filter,代码侵入性为零,而且完全没有redeploy的问题!
总结一下
- 如果你想继续用原有的Handler包装方案,
@PreDestroy清理是完全可靠的,能解决重复包装的问题; - 强烈推荐用MDC+日志格式配置的方案,这是日志上下文附加的标准做法,最优雅也最省心;
- 退而求其次可以用Filter代替Handler包装,比替换Handler更轻量。
备注:内容来源于stack exchange,提问作者EasterBunnyBugSmasher

