在这篇文章里,我们将会演示如何从web线程里复制MDC数据到@Async注解的线程里,我们将会使用一个全新的 Spring Framework 4.3的特性: ThreadPoolTaskExecutor#setTaskDecorator() [set-task-decorator]. 下面是最终结果:
注意到倒数第二行和第三行:在这个log级别上输出了[userId:Duke],倒数第三行是在一个web线程里(一个使用@RestController注解的类)发出的,倒数第二行是在一个用了@Async注解的异步线程里发出的。本质上,MDC数据从web线程中复制到了使用@Async注解的异步线程里中了(这就是最酷的部分,:smirk:)
这篇文章的所有代码都可以在GitGub上的示例中找到。如果有需要的话,可以去看看细节。
关于示例项目
这个示例项目基于Spring Boot 2。日志API这里用的是SLF4J和Logback(用了Logger, LoggerFactory和MDC) 如果你去看了那个示例项目,你将会发现这个@RestController注解的Controler
@RestController
public class MessageRestController {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final MessageRepository messageRepository;
MessageRestController(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
@GetMapping
List<String> list() throws Exception {
logger.info("RestController in action");
return messageRepository.findAll().get();
}
}
注意到它输出了日志:RestController in action,同时注意到它有一个古怪的调用:messageRepository.findAll().get(),这是因为它执行了一个异步的方法,接收了一个Future对象,并且调用了get()方法来等待结果返回,所以这是一个在web线程里调用使用@Async注解的异步方法。这是一个很显然的人为的为了演示而写的示例(我猜你在工作中的一些场景中会明智的调用此类异步方法)
下面是那个repository类:
@Repository
class MessageRepository {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Async
Future<List<String>> findAll() {
logger.info("Repository in action");
return new AsyncResult<>(Arrays.asList("Hello World", "Spring Boot is awesome"));
}
}
注意到findAll方法里打印了日志:Repository in action。
为了完整起见,让我向你展示如何在web线程里设置MDC数据的:
@Component
public class MdcFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
MDC.put("mdcData", "[userId:Duke]");
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
如果我们什么也不做,我们可以在web线程里很轻松的拿到正确配置的MDC数据,但是当一个web请求进入了@Async注解的异步方法调用里,我们却不能跟踪它:MDC数据里的ThreadLocal数据不会简单的自动复制过来,好消息是这个超级简单解决
解决方案第一步: 配置@Async线程池
首先,定制化你的异步功能,我是这样做的:
@EnableAsync(proxyTargetClass = true)
@SpringBootApplication
public class Application extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
有意思的地方是我们扩展了AsyncConfigurerSupport,好让我们可以自定义线程池
更精确的说:秘密在于executor.setTaskDecorator(new MdcTaskDecorator())。就是这行代码使我们可以自定义TaskDecorator
解决方案第二步: 实现TaskDecorator
现在到了说明自定义的TaskDecorator:
class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// Right now: Web thread context !
// (Grab the current thread MDC data)
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// Right now: @Async thread context !
// (Restore the Web thread context's MDC data)
MDC.setContextMap(contextMap);
runnable.run();
} finally {
MDC.clear();
}
};
}
}
decorate()方法的参数是一个Runnable对象,返回结果也是另一个Runnable对象
这里,我只是把原始的Runnable对象包装了一下,首先取得MDC数据,然后把它放到了委托的run方法里(Here, I basically wrap the original Runnable and maintain the MDC data around a delegation to its run() method.英文原文是这样,太难翻译了,囧)
总结
从web线程里复制MDC数据到异步线程是如此的容易,这里展示的技巧不局限于复制MDC数据,你也可以使用它来复制其他ThreadLocal数据(MDC内部就是使用ThreadLocal),或者你可以使用TaskDecorator做一些其他完全不同的事情:记录日志,度量方法执行的时间,吞掉异常,退出JVM等等,只要你喜欢。
感谢您的观看,喜欢的小伙伴可以点个赞!!!专注Java、大数据知识干货及相关领域动态分享,请多多关注哦!
留言与评论(共有 0 条评论) |