SpringBoot2.X異步編程@Async之請求上下文信息的傳遞

NO IMAGE

前言

前兩天研究了一下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”) ,這個註解中一定要指明使用的是自己配置的線程池,不然不生效的!

相關文章

設計模式之外觀設計模式

Flutter升級1.12適配教程

數據庫中間件分片算法之enum

一文入門mybatis