在这篇文章里,我们将会演示如何从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 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> 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 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、大资料知识干货及相关领域动态分享,请多多关注哦!