實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務

零壹技術棧發表於2018-06-18

# 前言

上文介紹了基於 @Async 註解的 非同步呼叫程式設計,本文將繼續引入 Spring BootWebAsyncTask 進行更靈活非同步任務處理,包括 非同步回撥超時處理異常處理

實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務

本系列文章

  1. 實戰Spring Boot 2.0系列(一) - 使用Gradle構建Docker映象
  2. 實戰Spring Boot 2.0系列(二) - 全域性異常處理和測試
  3. 實戰Spring Boot 2.0系列(三) - 使用@Async進行非同步呼叫詳解
  4. 實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務
  5. 實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor
  6. 實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現

正文

1. 處理執行緒和非同步執行緒

在開始下面的講解之前,在這裡先區別下兩個概念:

  1. 處理執行緒:處理執行緒 屬於 web 伺服器執行緒,負責 處理使用者請求,採用 執行緒池 管理。

  2. 非同步執行緒:非同步執行緒 屬於 使用者自定義的執行緒,可採用 執行緒池管理

Spring 提供了對 非同步任務 API,採用 WebAsyncTask 類即可實現 非同步任務。對非同步任務設定相應的 回撥處理,如當 任務超時異常丟擲 等。非同步任務通常非常實用,比如:當一筆訂單支付完成之後,開啟非同步任務查詢訂單的支付結果。

2. 環境準備

配置gradle依賴

利用 Spring Initializer 建立一個 gradle 專案 spring-boot-web-async-task,建立時新增相關依賴。得到的初始 build.gradle 如下:

buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
複製程式碼

配置服務類

配置一個用於非同步任務排程的 Mock 服務。

@Service
public class WebAsyncService {
    public String generateUUID() {
        return UUID.randomUUID().toString();
    }
}
複製程式碼

配置非同步處理控制器並注入以上服務 Bean

@RestController
public class WebAsyncController {
    private final WebAsyncService asyncService;
    private final static String ERROR_MESSAGE = "Task error";
    private final static String TIME_MESSAGE = "Task timeout";

    @Autowired
    public WebAsyncController(WebAsyncService asyncService) {
        this.asyncService = asyncService;
    }
}
複製程式碼

3. 正常非同步任務

配置一個正常的 WebAsyncTask 任務物件,設定任務 超時時間10s。非同步任務執行採用 Thread.sleep(long) 模擬,這裡設定 非同步執行緒 睡眠時間為 5s

@GetMapping("/completion")
public WebAsyncTask<String> asyncTaskCompletion() {
    // 列印處理執行緒名
    out.println(format("請求處理執行緒:%s", currentThread().getName()));

    // 模擬開啟一個非同步任務,超時時間為10s
    WebAsyncTask<String> asyncTask = new WebAsyncTask<>(10 * 1000L, () -> {
        out.println(format("非同步工作執行緒:%s", currentThread().getName()));
        // 任務處理時間5s,不超時
        sleep(5 * 1000L);
        return asyncService.generateUUID();
    });

    // 任務執行完成時呼叫該方法
    asyncTask.onCompletion(() -> out.println("任務執行完成"));
    out.println("繼續處理其他事情");
    return asyncTask;
}
複製程式碼

啟動 Spring Boot 專案,訪問 http://localhost:8080/completion ,發起 正常 的非同步任務請求。

觀察控制檯輸出,可以驗證 WebAsyncTask 的非同步處理流程正常。

請求處理執行緒:http-nio-8080-exec-2
繼續處理其他事情
非同步工作執行緒:MvcAsync1
任務執行完成
複製程式碼

Web 頁面正常響應,頁面響應訊息如下:

實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務

注意:WebAsyncTask.onCompletion(Runnable) :在當前任務執行結束以後,無論是執行成功還是異常中止,onCompletion的回撥最終都會被呼叫。

4. 丟擲異常非同步任務

配置一個 錯誤WebAsyncTask 任務物件,設定任務 超時時間10s。在非同步任務執行方法中 丟擲異常

@GetMapping("/exception")
public WebAsyncTask<String> asyncTaskException() {
    // 列印處理執行緒名
    out.println(format("請求處理執行緒:%s", currentThread().getName()));

    // 模擬開啟一個非同步任務,超時時間為10s
    WebAsyncTask<String> asyncTask = new WebAsyncTask<>(10 * 1000L, () -> {
        out.println(format("非同步工作執行緒:%s", currentThread().getName()));
        // 任務處理時間5s,不超時
        sleep(5 * 1000L);
        throw new Exception(ERROR_MESSAGE);
    });

    // 任務執行完成時呼叫該方法
    asyncTask.onCompletion(() -> out.println("任務執行完成"));
    asyncTask.onError(() -> {
        out.println("任務執行異常");
        return ERROR_MESSAGE;
    });

    out.println("繼續處理其他事情");
    return asyncTask;
}
複製程式碼

啟動 Spring Boot 專案,訪問 http://localhost:8080/exception ,發起 異常 的非同步任務請求。

Web 頁面響應異常資訊如下:

實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務

觀察控制檯輸出,可以驗證 WebAsyncTask 對於 異常請求 的非同步處理過程。

請求處理執行緒:http-nio-8080-exec-1
繼續處理其他事情
非同步工作執行緒:MvcAsync2
2018-06-18 21:12:10.110 ERROR 89875 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] threw exception

java.lang.Exception: Task error
	at io.ostenant.springboot.sample.controller.WebAsyncController.lambda$asyncTaskException$2(WebAsyncController.java:55) ~[classes/:na]
	at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:317) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_172]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_172]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_172]

2018-06-18 21:12:10.111 ERROR 89875 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.Exception: Task error] with root cause

java.lang.Exception: Task error
	at io.ostenant.springboot.sample.controller.WebAsyncController.lambda$asyncTaskException$2(WebAsyncController.java:55) ~[classes/:na]
	at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:317) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_172]
	at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_172]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_172]

任務執行異常
2018-06-18 21:12:10.144  WARN 89875 --- [nio-8080-exec-2] o.apache.catalina.core.AsyncContextImpl  : onError() failed for listener of type [org.apache.catalina.core.AsyncListenerWrapper]

java.lang.IllegalArgumentException: Cannot dispatch without an AsyncContext
	at org.springframework.util.Assert.notNull(Assert.java:193) ~[spring-core-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.dispatch(StandardServletAsyncWebRequest.java:131) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.web.context.request.async.WebAsyncManager.setConcurrentResultAndDispatch(WebAsyncManager.java:353) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$2(WebAsyncManager.java:304) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.lambda$onError$0(StandardServletAsyncWebRequest.java:146) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at java.util.ArrayList.forEach(ArrayList.java:1257) ~[na:1.8.0_172]
	at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onError(StandardServletAsyncWebRequest.java:146) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
	at org.apache.catalina.core.AsyncListenerWrapper.fireOnError(AsyncListenerWrapper.java:49) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.catalina.core.AsyncContextImpl.setErrorState(AsyncContextImpl.java:397) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:239) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:232) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1468) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_172]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_172]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.31.jar:8.5.31]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_172]

任務執行完成
複製程式碼

注意:WebAsyncTask.onError(Callable<?>) :當非同步任務丟擲異常的時候,onError()方法即會被呼叫。

5. 超時非同步任務

配置一個正常的 WebAsyncTask 任務物件,設定任務 超時時間10s。非同步任務執行採用 Thread.sleep(long) 模擬,這裡設定 非同步執行緒 睡眠時間為 15s,引發非同步任務超時。

@GetMapping("/timeout")
public WebAsyncTask<String> asyncTaskTimeout() {
    // 列印處理執行緒名
    out.println(format("請求處理執行緒:%s", currentThread().getName()));

    // 模擬開啟一個非同步任務,超時時間為10s
    WebAsyncTask<String> asyncTask = new WebAsyncTask<>(10 * 1000L, () -> {
        out.println(format("非同步工作執行緒:%s", currentThread().getName()));
        // 任務處理時間5s,不超時
        sleep(15 * 1000L);
        return TIME_MESSAGE;
    });

    // 任務執行完成時呼叫該方法
    asyncTask.onCompletion(() -> out.println("任務執行完成"));
    asyncTask.onTimeout(() -> {
        out.println("任務執行超時");
        return TIME_MESSAGE;
    });

    out.println("繼續處理其他事情");
    return asyncTask;
}
複製程式碼

啟動 Spring Boot 專案,訪問 http://localhost:8080/timeout ,發起 超時 的非同步任務請求。

觀察控制檯輸出,可以驗證 WebAsyncTask 的非同步超時處理的過程。

請求處理執行緒:http-nio-8080-exec-1
繼續處理其他事情
非同步工作執行緒:MvcAsync3
任務執行超時
任務執行完成
複製程式碼

Web 頁面常響應超時提示資訊,頁面響應訊息如下:

實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務

注意:WebAsyncTask.onTimeout(Callable<?>) :當非同步任務發生超時的時候,onTimeout()方法即會被呼叫。

6. 執行緒池非同步任務

上面的三種情況中的 非同步任務 預設不是採用 執行緒池機制 進行管理的。

也就是說,一個請求進來,雖然釋放了處理執行緒,但是系統依舊會為每個請求建立一個 非同步任務執行緒,也就是上面看到的 MvcAsync 開頭的 非同步任務執行緒

後果就是開銷嚴重,所以通常採用 執行緒池 進行統一的管理,直接在 WebAsyncTask 類構造器傳入一個 ThreadPoolTaskExecutor 物件例項即可。

構造一個執行緒池 Bean 物件:

@Configuration
public class TaskConfiguration {
    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(5);
        taskExecutor.setMaxPoolSize(10);
        taskExecutor.setQueueCapacity(10);
        taskExecutor.setThreadNamePrefix("asyncTask");
        return taskExecutor;
    }
}
複製程式碼

在控制器中注入 ThreadPoolTaskExecutor 物件,重新配置基於 執行緒池非同步任務處理

@Autowired
@Qualifier("taskExecutor")
private ThreadPoolTaskExecutor executor;

@GetMapping("/threadPool")
public WebAsyncTask<String> asyncTaskThreadPool() {
    return new WebAsyncTask<>(10 * 1000L, executor,
            () -> {
                out.println(format("非同步工作執行緒:%s", currentThread().getName()));
                return asyncService.generateUUID();
            });
}
複製程式碼

併發地請求 http://localhost:8080/threadPool ,觀察控制檯輸出的 非同步執行緒 資訊,可以發現 非同步任務 直接從 執行緒池 中獲取 非同步執行緒

非同步工作執行緒:asyncTask1
非同步工作執行緒:asyncTask2
非同步工作執行緒:asyncTask3
非同步工作執行緒:asyncTask4
非同步工作執行緒:asyncTask5
非同步工作執行緒:asyncTask1
非同步工作執行緒:asyncTask2
非同步工作執行緒:asyncTask3
非同步工作執行緒:asyncTask4
非同步工作執行緒:asyncTask5
複製程式碼

小結

本文介紹了 Spring Boot 提供的 WebAsyncTask 的非同步程式設計 API。相比上問介紹的 @Async 註解,WebAsyncTask 提供更加健全的 超時處理異常處理 支援。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章