歡迎光臨
每天分享高質量文章

Spring Async 最佳實踐(2):ExceptionHandler

(給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篇!

    已同步到看一看
    贊(0)

    分享創造快樂