内容简介:尊重外国人写文章的习惯,如果你初次看到此类翻译可能会造成不愉悦,但如果你曾经看到过,那你一定明白我在说什么,有的地方加上我自己的理解和注释注意到倒数第二行和第三行:在这个log级别上输出了[userId:Duke],倒数第三行是在一个web线程里(一个使用@RestController注解的类)发出的,倒数第二行是在一个用了@Async注解的异步线程里发出的。本质上,MDC数据从web线程中复制到了使用@Async注解的异步线程里中了(这就是最酷的部分,:smirk:)
人工手打,翻译自: https://moelholm.com/2017/07/24/spring-4-3-using-a-taskdecorator-to-copy-mdc-data-to-async-threads 本来想自己写一篇关于线程池threadlocal的,偶然看到这篇文章觉得挺好的,便直接翻译了
尊重外国人写文章的习惯,如果你初次看到此类翻译可能会造成不愉悦,但如果你曾经看到过,那你一定明白我在说什么,有的地方加上我自己的理解和注释
在这篇文章里,我们将会演示如何从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等等,只要你喜欢
墙裂感谢Joris Kuipers (@jkuipers)提醒我这个牛逼的Spring Framework 4.3新功能, An awesome tip :hugging:(这一句怎么翻译?)
参考
[set-task-decorator] ThreadPoolTaskExecutor#setTaskDecorator() (Spring’s JavaDoc) https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html#setTaskDecorator-org.springframework.core.task.TaskDecorator-
以下自己的总结:
- 使用ThreadLocal,不会在子线程中(包括new Thread和new线程池)获取到
- 使用InheritableThreadLocal,可以在子线程中(包括new Thread和new线程池)获取到,但是如果用的是线程池,一般不会每次使用的时候重新创建,而他的赋值只能在首次创建的时候可以(Thread类的inheritableThreadLocals变量),后面线程池中的线程重复使用时,一开始赋值的那个变量将会一直存在,你可能会得到错误的结果或者理解为这也是一种内存泄漏
- 在spring中,一般通过xml或者@Configuration来配置线程池,那么在项目启动的时候,线程池就完成创建了,根本没有机会给你设置变量,所以最佳实践就是,在线程池提交任务的时候(execute和submit方法),把当前线程的threadlocal变量保存起来,重写run方法或者call方法,并且在调用实际的run方法前,保存刚才保存起来的变量,一般也是放到threadlocal里面,这样在实际的run方法里,就可以方便的通过threadlocal获取到了。
- 实现原理如上述3所说,这篇翻译的文章中也是该原理,ali提供了一个 transmittable-thread-local ,原理也是上面3所讲的,不过个人觉得它实现有点绕,用起来还算简单,可以用下
关于threadlocal的代码细节,见我的另外一篇文章:再看ThreadLocal
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS主线程耗时检测方案
- 基于 Proxy 的线程通信新方案
- 虚拟机线程问题分析(一):服务调优或问题排查执行方案
- Transmittable-Thread-Local:阿里开源的线程间上下文传递解决方案
- java中线程安全,线程死锁,线程通信快速入门
- ObjC 多线程简析(一)-多线程简述和线程锁的基本应用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。