前两天研究了一下Spring中@Async这个注解,简单的说就是异步调用的一个注解。项目中也基本没用过,主要是没有响应的业务场景。比如:发短信、发邮件、发送队列消息等场景我觉得都可以使用异步编程。使用这个注解也比较简单,其中还是有一个坑的,就是请求的上下文信息的传递。
正文
异步调用对应的是 同步调用 , 同步调用 指程序按照 定义顺序 依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行; 异步调用 指程序在顺序执行时, 不等待 异步调用的语句 返回结果 就执行后面的程序。
准备完整代码: Controller层:
@ApiOperation(value = "t-1.5-异步执行测试")
@GetMapping("/task")
public String taskExecute() {
try {
Future<String> r1 = testTableService.doTaskOne();
Future<String> r2 = testTableService.doTaskTwo();
Future<String> r3 = testTableService.doTaskThree();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("当前线程为 {},请求方法为 {},请求路径为:{}", Thread.currentThread().getName(), request.getMethod(), request.getRequestURL().toString());
while (true) {
if (r1.isDone() && r2.isDone() && r3.isDone()) {
log.info("execute all tasks");
break;
}
Thread.sleep(200);
}
log.info("/n" + r1.get() + "/n" + r2.get() + "/n" + r3.get());
} catch (Exception e) {
log.error("error executing task for {}", e.getMessage());
}
return "ok";
}
复制代码 Service层:
@Async("asyncExecutor") //一定要指明使用的哪个线程池
@Override
public Future<String> doTaskOne() throws InterruptedException {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(1000);
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("当前线程为 {},请求方法为 {},请求路径为:{}", Thread.currentThread().getName(), request.getMethod(), request.getRequestURL().toString());
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务一完成,耗时" + (end - start) + "毫秒");
}
@Async("asyncExecutor")
@Override
public Future<String> doTaskTwo() throws InterruptedException {
log.info("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("当前线程为 {},请求方法为 {},请求路径为:{}", Thread.currentThread().getName(), request.getMethod(), request.getRequestURL().toString());
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务二完成,耗时" + (end - start) + "毫秒");
}
@Async("asyncExecutor")
@Override
public Future<String> doTaskThree() throws InterruptedException {
log.info("开始做任务三");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
log.info("当前线程为 {},请求方法为 {},请求路径为:{}", Thread.currentThread().getName(), request.getMethod(), request.getRequestURL().toString());
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务三完成,耗时" + (end - start) + "毫秒");
}
复制代码 配置类: executor.setTaskDecorator(new ContextCopyingDecorator()); 这行代码是重点,设置请求上下文的传递!
/**
* @Description:
* @author: ListenerSun(男, 未婚) 微信:810548252
* @Date: Created in 2019-12-16 19:06
*/
@Slf4j
@Configuration
public class ConfigBean {
public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";
@Bean(name = ASYNC_EXECUTOR_NAME)
public Executor asyncExecutor() {
log.info("==========>开始注入线程池 Bean");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// for passing in request scope context
// 线程上下文拷贝实现类
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
}
复制代码 线程上下文信息拷贝类:
/**
* @Description: 异步调用复制 请求 上下文
* @author: ListenerSun(男, 未婚) 微信:810548252
* @Date: Created in 2019-12-31 18:53
*/
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(context);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
}
复制代码 入口类: 添加@EnableAsync注解,代表允许开启异步编程!
@EnableAsync
@Slf4j
@SpringBootApplication
@EnableFeignClients("com.sqt.edu")
@EnableEurekaClient
@ComponentScan(basePackages = "com.sqt.edu")
public class Account_APP {
public static void main(String[] args) {
SpringApplication.run(Account_APP.class);
log.info("==========>Account service start successful !");
}
}
复制代码 首先如果我们将 executor.setTaskDecorator(new ContextCopyingDecorator()); 这行代码注释掉
/**
* @Description:
* @author: ListenerSun(男, 未婚) 微信:810548252
* @Date: Created in 2019-12-16 19:06
*/
@Slf4j
@Configuration
public class ConfigBean {
public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";
@Bean(name = ASYNC_EXECUTOR_NAME)
public Executor asyncExecutor() {
log.info("==========>开始注入线程池 Bean");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// for passing in request scope context
// 线程上下文拷贝实现类
//executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
}
复制代码 打印输出的结果:
可以看到日志报错了,也就是在Service层中的Task任务中的有一行代码报了空指针异常! 就是以下代码:
HttpServletRequest request = requestAttributes.getRequest(); 复制代码
可以看到获取到的 requestAttributes 为 null !
在 TaskService 中,每个异步线程的方法获取 RequestContextHolder 中的请求信息时,报了空指针异常。这说明了请求的上下文信息未传递到异步方法的线程中。我们可以看一下 RequestContextHolder 的实现,里面有两个 ThreadLocal 保存当前线程下的 request。关于ThreadLocal这个类,个人的理解就是根据字面意思 "本地线程",意思就是保存在ThreadLocal中的是你自己的,就跟JVM内存模型中的本地内存一样,从主内存中拷贝到线程本地内存一个道理!所以如何将上下文信息传递到异步线程呢?Spring 中的 ThreadPoolTaskExecutor 有一个配置属性 TaskDecorator , TaskDecorator 是一个回调接口,采用装饰器模式。装饰模式是动态的给一个对象添加一些额外的功能,就增加功能来说,装饰模式比生成子类更为灵活。因此 TaskDecorator 主要用于任务的调用时设置一些执行上下文,或者为任务执行提供一些监视/统计。
此时我们把之前的 executor.setTaskDecorator(new ContextCopyingDecorator()); 这行代码注释去掉
@Slf4j
@Configuration
public class ConfigBean {
public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";
@Bean(name = ASYNC_EXECUTOR_NAME)
public Executor asyncExecutor() {
log.info("==========>开始注入线程池 Bean");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// for passing in request scope context
// 线程上下文拷贝实现类
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
}
复制代码
此时控制台打印结果:
到此请求的上下文信息就已经被传递到了每一个任务当中!其中还有一点要注意的是 @Async("asyncExecutor") ,这个注解中一定要指明使用的是自己配置的线程池,不然不生效的!