前两天研究了一下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") ,这个注解中一定要指明使用的是自己配置的线程池,不然不生效的!