转载

SpringBoot2.X异步编程@Async之请求上下文信息的传递

前两天研究了一下Spring中@Async这个注解,简单的说就是异步调用的一个注解。项目中也基本没用过,主要是没有响应的业务场景。比如:发短信、发邮件、发送队列消息等场景我觉得都可以使用异步编程。使用这个注解也比较简单,其中还是有一个坑的,就是请求的上下文信息的传递。

正文

  1. 异步调用对应的是 同步调用同步调用 指程序按照 定义顺序 依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行; 异步调用 指程序在顺序执行时, 不等待 异步调用的语句 返回结果 就执行后面的程序。

  2. 准备完整代码: 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;
        }
    }
    复制代码

    打印输出的结果:

SpringBoot2.X异步编程@Async之请求上下文信息的传递

可以看到日志报错了,也就是在Service层中的Task任务中的有一行代码报了空指针异常! 就是以下代码:

HttpServletRequest request = requestAttributes.getRequest();
复制代码

可以看到获取到的 requestAttributes 为 null !

SpringBoot2.X异步编程@Async之请求上下文信息的传递

在 TaskService 中,每个异步线程的方法获取 RequestContextHolder 中的请求信息时,报了空指针异常。这说明了请求的上下文信息未传递到异步方法的线程中。我们可以看一下 RequestContextHolder 的实现,里面有两个 ThreadLocal 保存当前线程下的 request。关于ThreadLocal这个类,个人的理解就是根据字面意思 "本地线程",意思就是保存在ThreadLocal中的是你自己的,就跟JVM内存模型中的本地内存一样,从主内存中拷贝到线程本地内存一个道理!所以如何将上下文信息传递到异步线程呢?Spring 中的 ThreadPoolTaskExecutor 有一个配置属性 TaskDecoratorTaskDecorator 是一个回调接口,采用装饰器模式。装饰模式是动态的给一个对象添加一些额外的功能,就增加功能来说,装饰模式比生成子类更为灵活。因此 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;
    }
}

复制代码

此时控制台打印结果:

SpringBoot2.X异步编程@Async之请求上下文信息的传递

到此请求的上下文信息就已经被传递到了每一个任务当中!其中还有一点要注意的是 @Async("asyncExecutor") ,这个注解中一定要指明使用的是自己配置的线程池,不然不生效的!

原文  https://juejin.im/post/5e120b9ae51d45412862af4e
正文到此结束
Loading...