SpringBoot项目中,经常使用@Async
来开启一个子线程来完成异步操作。使用异步线程的好处是其执行不影响主线程。比如用户注册成功后,发送一封欢迎邮件,在异步线程中发送邮件,即使出了问题,也不会影响到当前用户的注册体验。
有时在子线程中需要主线程的上下文,可通过如下步骤实现:
1)启用异步功能
在启动类或异步配置类上添加@EnableAsync
注解
@EnableAsync
@SpringBootApplication
public class Application {}
2)配置异步
新建一个配置类,实现AsyncConfigurer
接口,并重写getAsyncExecutor
方法
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setThreadNamePrefix("async-pool-");
// 这一步是关键,异步任务装饰器
executor.setTaskDecorator(new MyContextDecorator());
executor.initialize();
return executor;
}
}
3)任务装饰器
新建一个类,实现TaskDecorator
接口,并重写decorate
方法
public class MyContextDecorator implements TaskDecorator {
@Override
public Runnable decorate(@Nonnull Runnable runnable) {
// 获取主线程中的请求信息
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return () -> {
try {
// 将主线程的请求信息,设置到子线程中
RequestContextHolder.setRequestAttributes(attributes);
// 执行子线程
runnable.run();
} finally {
// 线程结束,清空这些信息,否则可能造成内存泄漏
RequestContextHolder.resetRequestAttributes();
}
};
}
到此,通过@Async开启的子线程,就可以正常拿到父线程中的Request信息了。
备注:通常我们可以把需要的信息封装到任务中,在需要的地方再取出来。
原理探究
Spring给我们预留了任务装饰器TaskDecorator,通过它可以对线程做一些功能增强。在ThreadPoolTaskExecutor
的源码中,initializeExecutor 方法中根据是否有装饰器创建的ThreadPoolExecutor
不一样。
ThreadPoolExecutor是Java并发包中的用于创建线程池,ThreadPoolTaskExecutor是Spring框架对ThreadPoolExecutor的封装。
protected ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);
ThreadPoolExecutor executor;
if (this.taskDecorator != null) {
//如果有装饰器,就对任务先装饰,再执行装饰后的任务
executor = new ThreadPoolExecutor(this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler) {
@Override
public void execute(Runnable command) {
Runnable decorated = taskDecorator.decorate(command);
if (decorated != command) {
decoratedTaskMap.put(decorated, command);
}
super.execute(decorated);
}
};
}
else {
executor = new ThreadPoolExecutor(
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler);
}
if (this.allowCoreThreadTimeOut) {
executor.allowCoreThreadTimeOut(true);
}
this.threadPoolExecutor = executor;
return executor;
}
注意事项
在 @Async 异步线程使用过程中,需要注意的是以下的用法会使 @Async 失效:
- 异步方法使用 static 修饰;
- 异步类没有使用 @Component 注解(或其他注解)导致 Spring 无法扫描到异步类;
- 异步方法不能与被调用的异步方法在同一个类中;
- 类中需要使用 @Autowired 或 @Resource 等注解自动注入,不能手动 new 对象;
- 如果使用 Spring Boot 框架必须在启动类(或配置类)上增加 @EnableAsync 注解。