Java版Play框架中SLF4J MDC/X-Request-Id跨线程传播问题及解决方案
Play框架SLF4J MDC跨线程传播X-Request-Id问题的解决方案
我最近在Play框架中实现日志追踪时,遇到了SLF4J MDC的X-Request-Id无法在多线程间传递的问题:在Filter中设置MDC值后,控制器异步任务里获取到的MDC值始终为null。尝试了异步环境下MDC传递的常规方案但未生效,最终通过以下两步解决了问题,分享给大家:
一、用Action拦截器替代Filter管理MDC生命周期
Play的Filter执行线程和后续控制器/异步任务的线程往往不属于同一个上下文,直接在Filter中设置MDC无法自动传播到后续线程。改用Play原生的Action.Simple实现请求拦截,能更好地管理MDC在请求整个生命周期的创建与清理:
public class DefaultAction extends Action.Simple { @Override public CompletionStage<Result> call(final Request request) { try { // 从请求头提取X-Request-Id,无则生成UUID String requestId = request.header("X-Request-Id").orElse(UUID.randomUUID().toString()); MDC.put("X-Request-Id", requestId); // 继续执行后续请求逻辑 return delegate.call(request); } finally { // 请求结束后必须清理MDC,防止线程池上下文污染 MDC.remove("X-Request-Id"); } } }
生效方式:
- 全局生效:在
application.conf中配置全局Action Creator:play.http.actionCreator = "com.yourpackage.DefaultAction" - 局部生效:在需要的控制器上添加注解:
@With(DefaultAction.class) public class FetchDirectoryApi extends NewAuthenticatedInterceptorStack { // ... 控制器逻辑 }
二、改造自定义Execution Context以传播MDC上下文
对于自定义的线程池(比如MediumExecutionContext),需要重写execute方法,在提交任务时捕获当前MDC上下文,并在任务执行时恢复,执行完成后还原原线程的MDC:
public class MediumExecutionContext extends CustomExecutionContext { public MediumExecutionContext(final ActorSystem actorSystem) { // 对应application.conf中定义的自定义线程池 super(actorSystem, "medium-dispatcher"); } @Override public void execute(final Runnable command) { // 捕获当前线程的MDC上下文快照 final Map<String, String> mdcContext = MDC.getCopyOfContextMap(); // 包装原任务,处理MDC上下文的切换 final Runnable wrappedTask = () -> { // 备份执行线程原有的MDC上下文 final Map<String, String> originalMdc = MDC.getCopyOfContextMap(); try { // 设置当前请求的MDC上下文 if (mdcContext != null) { MDC.setContextMap(mdcContext); } else { MDC.clear(); } // 执行原业务任务 command.run(); } finally { // 恢复执行线程原有的MDC上下文,避免污染 if (originalMdc != null) { MDC.setContextMap(originalMdc); } else { MDC.clear(); } } }; // 将包装后的任务提交到自定义线程池 super.execute(wrappedTask); } }
关键注意点:
- 不要直接在当前线程执行
wrappedTask.run(),必须调用super.execute(wrappedTask)将任务提交到目标线程池,否则无法实现跨线程传播。 - 必须在
finally块中恢复原线程的MDC上下文,防止线程池中的线程被复用后携带旧的MDC数据。
验证效果
确保你的logback配置已经包含X-Request-Id的打印:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %mdc{X-Request-Id:--} - %msg%n </pattern> </encoder> </appender>
此时控制器中的异步任务日志就能正常打印出X-Request-Id了。
内容的提问来源于stack exchange,提问作者Shorifuzzaman
相关产品推荐
相关产品推荐

