最近在工作中,學到了幾個知識點,還是很有用的,因此準備寫幾篇 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())));
}
}
最後的結果就是這樣的