qinfengge

qinfengge

醉后不知天在水,满船清梦压星河
github
email
telegram

Java非同步及返回

最近在工作中,學到了幾個知識點,還是很有用的,因此準備寫幾篇 blog 記錄一下

上週接到一個新的需求,要獲取程序的報錯,並發送通知給負責人,實現其實不難,但難的是如何優雅的實現,而剛好我就看過 2 個知識點,結合一下就能完美實現需求

全局異常處理#

全局異常處理很簡單,只需要 2 個註解。@RestControllerAdvice@ExceptionHandler

首先 @RestControllerAdvice 的解釋是 @RestControllerAdvice 是一個組合註解,由 @ControllerAdvice、@ResponseBody 組成,而 @ControllerAdvice 繼承了 @Component,因此 @RestControllerAdvice 本質上是一個 Component

@ExceptionHandler 則是一個異常攔截器,可以使用 @ExceptionHandler({Exception.class}) *//申明捕獲那個異常類*

這兩個註解加起來就是全局的異常攔截器

@RestControllerAdvice
public class GlobalController {

    @ExceptionHandler
    public void notifyWeChat(Exception e) throws 
        log.error(e)
    }

机器通知#

ok,現在我們有了全局的異常攔截器,只要程序報錯,我們就能攔截到錯誤信息。但我們還需要發送通知給負責人啊。有沒有什麼優雅的通知方式呢?

當然有了,叮鸽 就是一個優雅的消息通知中間件,它支持使用 Spring Boot 集成釘釘 / 企業微信 / 飛書群機器人實現消息通知。

此處 是官方的開發文檔。

首先引入依賴

<dependency>
    <groupId>com.github.answerail</groupId>
    <artifactId>dinger-spring-boot-starter</artifactId>
    <version>${dinger.version}</version>
</dependency>

以企業微信機器人為例,配置文件

#Dinger
spring.dinger.project-id=sa-token
#微信機器人token
spring.dinger.dingers.wetalk.token-id=xxx
#配置@成員的手機號
wetalk.notify.phones = 17633*****,17633*****

然後需要定義一個接口

public interface DingerConfig {
    @DingerText(value = "訂單號${orderNum}下單成功啦, 下單金額${amt}")
    DingerResponse orderSuccess(
            @DingerPhone List<String> phones,
            @Parameter("orderNum") String orderNo,
            @Parameter("amt") BigDecimal amt
    );

    @DingerMarkdown(
            value = "#### 方法錯誤\n - 請求時間: ${requestTime}\n - 請求路徑: ${requestPath}\n - 請求參數: ${queryString}\n - 錯誤信息: ${exceptionMessage}",
            title = "錯誤詳情"
    )
    DingerResponse exceptionNotify(@DingerPhone List<String> phones, String requestTime, String requestPath, String queryString, String exceptionMessage);

@DingerText 是發送文本類型的消息,而 @DingerMarkdown 是發送 markdown 格式的消息,注意只有文本消息能正確的 @用戶。

然後在啟動類,添加掃描包路徑

@DingerScan(basePackages = "xyz.qinfengge.satokendemo.dinger")

配置 @指定手機號用戶,只需要添加一個組件

從配置文件中讀取信息,然後變成需要的格式即可

@Component
public class NotifyPhones {

    @Value("${wetalk.notify.phones}")
    private String phones;

    public List<String> handlePhones() {
        return Arrays.asList(phones.split(","));
    }

}

最後使用即可

List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        dingerConfig.exceptionNotify(phones, requestDate, request.getServletPath(), parameters, e.getMessage());

異步#

現在我們能捕獲異常並發送消息了,但是訪問接口出錯的話,能感受到速度明顯變慢了,那麼怎麼優化呢?

一般對於這種實時性要求比較低的操作,我們可以使用異步

使用異步也很簡單,只需要在方法上加上 @Async 註解,表明這是一個異步方法,然後在啟動類上加上 @EnableAsync 開啟異步即可。

加上異步是這樣的

@RestControllerAdvice
public class GlobalController {

    @Resource
    private NotifyPhones notifyPhones;

    @Resource
    private DingerConfig dingerConfig;

    @ExceptionHandler
    @Async
    public void handleException(Exception e) {
        List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        dingerConfig.exceptionNotify(phones, e.getMessage());
    }

}

但是,另一個問題出現了,你會發現加了異步後,方法的返回值只能是 void 或者 **CompletableFuture ** 了,如果接口出錯了那就沒有返回了,所以還需要再改造一下,讓它返回正常的接口結構

那麼,最終的結構就是這樣的

加入了 HttpServletRequest 來獲取請求路徑和請求參數,還可以加入其他的東西,比如請求的 IP 地址等。

@Slf4j
@RestControllerAdvice
public class GlobalController {

    @Resource
    private NotifyPhones notifyPhones;

    @Resource
    private DingerConfig dingerConfig;

    @ExceptionHandler
    public Result<Object> notifyWeChat(HttpServletRequest request, Exception e) throws ExecutionException, InterruptedException {
        return Result.fail(this.handleException(request, e).get());
    }

    /**
     * 全局異常攔截
     *
     * @param e 異常
     * @return 異常信息
     */
    @Async
    public CompletableFuture<Result<Object>> handleException(HttpServletRequest request, Exception e) {
        // 在異步方法中獲取異步上下文
        AsyncContext asyncContext = request.startAsync();
        List<String> phones = notifyPhones.handlePhones();
        String requestDate = DateUtil.format(new Date(), "yyyy-MM-dd--HH:mm:ss");
        Map<String, String[]> parameterMap = request.getParameterMap();
        String parameters = JSON.toJSONString(parameterMap);
        dingerConfig.exceptionNotify(phones, requestDate, request.getServletPath(), parameters, e.getMessage());
        log.error(MessageFormat.format("請求 {0} 出錯,請求參數{1},錯誤信息:{2}", request.getServletPath(), parameters, e.getMessage()));
        // 異步方法執行完畢後,調用complete方法通知容器結束異步調用
        // 回收request
        asyncContext.complete();
        // 返回結果,異步方法只能返回CompletableFuture或void,因此需要先返回一個CompletableFuture再調用其方法獲取裡面的值
        return CompletableFuture.supplyAsync(() -> Result.fail(MessageFormat.format("請求 {0} 出錯,請求參數{1},錯誤信息:{2}", request.getServletPath(), parameters, e.getMessage())));
//        return CompletableFuture.completedFuture(Result.fail(MessageFormat.format("請求 {0} 出錯,請求參數{1},錯誤信息:{2}", request.getServletPath(), parameters, e.getMessage())));
    }

}

最後的結果就是這樣的

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。