(給ImportNew加星標,提高Java技能)
編譯:唐尤華
連結:dzone.com/articles/effective-advice-on-spring-async-exceptionhandler-1
本文將討論在 Spring Boot 使用 `@Async` 註解時如何捕捉異常。正文開始前,建議閱讀系列的[第一篇][1]。
從主執行緒 fork 新執行緒時,有兩種情況:
1. “Fire-and-forget“:fork 執行緒,然後為這個執行緒分配任務,接下來什麼也不用管。不需要關心任務執行結果,其他業務邏輯的執行也不依賴該結果。通常任務的傳回型別是 `void`。讓我們透過例子幫助理解:假設你在為員工發薪水,需要給每個員工傳送一份工資單郵件,你可以非同步執行該任務。發郵件顯然不是核心業務邏輯,而是一個橫切關註點。然而,發郵件很好,而且在某些情況下是必須的。這時候需要制定失敗重試或者定時機制。
2. “Fire-with-callback“:在主執行緒中 fork 一個執行緒,為該執行緒分配任務並關聯 `Callback`。接下來,主執行緒會繼續執行其他任務並持續檢查 `Callback` 結果。主執行緒需要子執行緒 `Callback` 執行結果進行下一步工作。
假設你正在做一份員工報告,員工資訊根據各自資料型別儲存在不同的後端。General Service 儲存員工通用資料,比如姓名、生日、性別、地址等;Financial Service 儲存薪資、稅金以及其他 PF 相關資料。因此,你會建立兩個並行執行緒,分別呼叫 General Service 與 Financial Service。這兩組資料最終都要在報告中體現,因此需要進行資料組合,在主執行緒中表現為子執行緒 Callback 結果。一般會用 `CompletebleFuture` 實現。
在上面描述的場景中,如果一切順利是最理想的結果。但如果執行中發生異常,該如何進行異常處理?
第二種情況下,由於回呼執行後能夠傳回成功或失敗,因此處理異常非常容易。失敗的時候,異常會被封裝在 `CompltebleFuture` 裡,在主執行緒中可以檢查異常並處理。處理異常的 Java 程式碼很簡單,這裡直接略過。
然而,第一種情況的異常處理非常棘手:建立的執行緒會執行業務邏輯,但如何確保業務執行成功?或者說,執行失敗該如何進行除錯,如何追蹤是什麼地方出現了問題?
解決方案很簡單:註入自己的 exception handler。這樣,當 `Async` 方法執行過程中發生異常,會把程式控制轉交給 handler。你的 handler 知道接下來該如何處理。很簡單,不是嗎?
要做到這一點,需要執行以下步驟:
1. `AsyncConfigurer`:`AsyncConfigurere` 是一個 Spring 提供的介面,包含兩個方法。一個可以多載 `TaskExecutor`(執行緒池),另一個是 exception handler。Exception handler 支援註入用來捕捉 unCaught 異常,也可以自己定義 class 直接實現。這裡我不會直接實現,而是用 Spring 提供的 `AsyncConfigurerSupport` 類,透過 `@Configuration` 和 `@EnableAsync` 註解提供預設實現。
```java
package com.example.ask2shamik.springAsync;
import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class CustomConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new SimpleAsyncTaskExecutor();
}
@Override
@Nullable
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, obj) -> {
System.out.println("Exception Caught in Thread - " + Thread.currentThread().getName());
System.out.println("Exception message - " + throwable.getMessage());
System.out.println("Method name - " + method.getName());
for (Object param : obj) {
System.out.println("Parameter value - " + param);
}
};
}
}
```
請註意,因為不想使用自定義 task executor,在 `getAsyncExecutor` 方法中,沒有建立任何新的 executor。因此,我將使用 Spring 預設的 `SimpleAsyncExecutor`。
但是,我需要自己定義 uncaught exception handler 處理 uncaught 異常。因此,我寫了一條繼承 `AsyncUncaughtExceptionHandler` 類的 lambda 運算式並改寫 `handleuncaughtexception` 方法。
這樣,會讓 Spring 載入與應用匹配的 `AsyncConfugurer(CustomConfiguration)` 並用 lambda 運算式進行異常處理。
新建一個 `@Async` 方法丟擲異常:
```java
package com.example.ask2shamik.springAsync;
import java.util.Map;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncMailTrigger {
@Async
public void senMailwithException() throws Exception {
throw new Exception("SMTP Server not found :: orginated from Thread :: " + Thread.currentThread().getName());
}
}
```
現在,建立呼叫方法。
```java
package com.example.ask2shamik.springAsync;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class AsyncCaller {
@Autowired
AsyncMailTrigger asyncMailTriggerObject;
public void rightWayToCall() throws Exception {
System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName());
asyncMailTriggerObject.senMailwithException();
}
}
```
接下來讓我們啟動 Spring Boot 應用,看它如何捕捉 `sendMailwithException` 方法引發的異常。
```java
package com.example.ask2shamik.springAsync;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import com.example.ask2shamik.springAsync.demo.AsyncCaller;
@SpringBootApplication
public class DemoApplication {
@Autowired
AsyncCaller caller;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
caller.rightWayToCall();
};
}
}
```
結果如下:
```shell
Calling From rightWayToCall Thread main
Exception Caught in Thread - SimpleAsyncTaskExecutor-1
Exception message - SMTP Server not found:: originated from Thread:: SimpleAsyncTaskExecutor-1
Method name - senMailwithException
```
總結
希望你喜歡這個教程!如果你有任何問題,歡迎在下方的評論區留言。敬請期待第3篇!
朋友會在“發現-看一看”看到你“在看”的內容