實戰SpringBoot(7)透過Aop機制進行全域性異常捕獲

上一篇透過Spring Aop機制實現了對api響應資料的統一封裝(傳送門:

實戰SpringBoot(6)透過Aop機制進行api響應資料統一封裝

),本篇將繼續完善整個專案,透過Spring Aop機制進行全域性的異常捕獲。

目標

透過Spring Aop機制實現全域性異常捕獲,為客戶端返回友好的響應資料。

步驟

1、用try catch進行異常捕獲

之前的示例中曾經建立過一個api查詢使用者資訊

@GetMapping(“/info”) public Object info(Long id) { return userService。getUserById(id); }

在實際中,不可能這麼簡單的處理服務查詢的資料,這裡UserService的實現中可能會丟擲異常,info方法本身在處理時寫不好也會出異常。如果請求處理過程中出現了異常,客戶端就得不到任何響應,使用者操作之後沒有任何效果就會比較懵逼,處理不好還會將錯誤資訊直接暴露給使用者,所以程式碼層面需要進行異常捕獲。大概的流程是這樣的。

@GetMapping(“/info”) public Object info(Long id) { try { return userService。getUserById(id); } catch (Exception ex) { return ResResult。error(“系統異常”); } }

看起來萬事大吉了,異常被捕獲,客戶端得到了友好的響應,但是在程式碼層面不那麼優雅,實際業務中會建立很多Controller類,Controller類裡會寫很多方法,每個方法都要寫try catch就比較尷尬了,重複程式碼太多,而且被try catch包起來的程式碼在編譯器做程式碼最佳化時會受到影響,影響最佳化效果。

這時就是該Aop出場的時候了,透過Aop機制,將異常捕獲程式碼抽象出來,從全域性上進行異常捕獲。

2、全域性異常捕獲

要利用Aop進行全域性異常捕獲需要藉助@RestControllerAdvice和@ExceptionHandler註解。

@RestControllerAdvicepublic class GlobalExceptionHandlerAdvice { @ExceptionHandler(value = Exception。class) public ResResult handleException(Exception ex) { return ResResult。error(ex。getMessage()); }}

下面在IndexController中建立一個測試api

@GetMapping(value = “/exception”) public void exception() throws Exception { throw new Exception(“Exception #1”); }

啟動專案後訪問http:/localhost:8080/exception,得到一個響應

實戰SpringBoot(7)透過Aop機制進行全域性異常捕獲

如此就完成了異常的全域性捕獲,但是又面臨一個新問題:

後端沒有得到異常提醒

3、後端異常提醒

介面異常了,後端系統卻風平浪靜,系統並沒有給出異常的提醒,我們可能還以為線上一切正常呢,直到使用者找過來說系統出錯了,這肯定是不行的,如果有異常要及時報警,常規做法就是打日誌。

@RestControllerAdvicepublic class GlobalExceptionHandlerAdvice { private final static Logger logger = LoggerFactory。getLogger(GlobalExceptionHandlerAdvice。class); @ExceptionHandler(value = Exception。class) public ResResult handleException(Exception ex) { logger。error(ex。getMessage(), ex); //這裡打出異常日誌,客戶端能得到友好的響應資料,後端開發也能得到錯誤提醒 return ResResult。error(ex。getMessage()); }}

再次執行專案,訪問http://localhost:8080/exception,在得到響應的同時,後端有了錯誤日誌。

java。lang。Exception: Exception #1 at com。sidianban。pangu。controller。IndexController。exception(IndexController。java:21) ~[main/:na] at java。base/jdk。internal。reflect。NativeMethodAccessorImpl。invoke0(Native Method) ~[na:na] at java。base/jdk。internal。reflect。NativeMethodAccessorImpl。invoke(NativeMethodAccessorImpl。java:62) ~[na:na] at java。base/jdk。internal。reflect。DelegatingMethodAccessorImpl。invoke(DelegatingMethodAccessorImpl。java:43) ~[na:na] at java。base/java。lang。reflect。Method。invoke(Method。java:566) ~[na:na]

4、自定義異常

後端錯誤提醒的問題解決了,我又想到了一個新的問題:

不同場景下的異常如何做區分

比如:當用戶以未登入狀態訪問需要登入才能開啟的內容時,後端返回一個未登入的異常,客戶端收到這種未登入異常就提醒使用者去登入,並直接跳轉到登入頁。

關鍵點是客戶端要能識別出這種異常是未登入異常,一般是透過給不同的異常賦不同的錯誤碼來區分,比如常規異常錯誤碼是500,未登入異常錯誤碼可以設定成600,客戶端接收到600錯誤碼就可以判定是未登入異常,去走相應的處理邏輯就可以了。

要實現給不同的異常賦不同的錯誤碼,java。lang。Exception已經不能滿足要求了,我們要擴充套件一些自定義的異常型別。

首先,建立一個自定義異常的抽象父類。

/** * 自定義異常抽象父類 */abstract public class BusinessException extends Exception{ private static final long serialVersionUID = 8684333269210029273L; /** * 返回給客戶端的提示資訊 */ private final String tips; public BusinessException(String tips) { super(tips); this。tips = tips; } public BusinessException(String tips, String message) { super(message); this。tips = tips; } public String getTips() { return tips; }}

關於這個抽象的自定義異常父類,做一下說明:

建立目的

有了這個抽象的父類,可以在後面進行自定義異常處理時進行一些抽象,同時也能方便後續對自定義異常的擴充套件。

tips欄位的意義

Exception異常裡已經有一個message欄位用來儲存異常資訊了,這裡又加一個tips的目的是想把給使用者看的異常資訊和給後端開發自己看的異常資訊分開,把後端開發自己看的異常資訊放到message中,可以在message中存放一些業務資料,打進日誌裡幫助開發人員排查;如果不分開的話,放了業務資訊就會暴露給使用者,不放又對故障排查不利,所以還是分開放的好。

接下來,定義未登入異常

為了準備自定義異常的定義,對ResStatus類做了修改,加了兩個列舉值。

public enum ResStatus { …… /** * 未登入 */ NEVER_LOGIN(600, “未登入”), /** * 狀態未知 */ UNKOWN(900, “未知錯誤”); ……}

自定義異常的錯誤碼配置採用註解的形式,新加了一個自定義註解ExceptionStatus。

/** * 異常狀態資訊 * 用於自定義異常的定義 * */@Target(ElementType。TYPE)@Retention(RetentionPolicy。RUNTIME)@Documentedpublic @interface ExceptionStatus { /** * @return 狀態 */ ResStatus status() default ResStatus。UNKOWN; /** * @return 狀態描述 */ String description() default “”;}

好了,萬事俱備,下面定義未登入異常類。

/** * 未登入異常 * */@ExceptionStatus(status = ResStatus。NEVER_LOGIN, description = “未登入”)public class NeverLoginException extends BusinessException { private static final long serialVersionUID = 3892919274244039652L; public NeverLoginException(String tips) { super(tips); } public NeverLoginException(String tips, String message) { super(tips, message); }}

最後,全域性捕獲自定義異常

在異常捕獲增強類中新增對BusinessException型別異常的捕獲邏輯。

@RestControllerAdvicepublic class GlobalExceptionHandlerAdvice { private final static Logger logger = LoggerFactory。getLogger(GlobalExceptionHandlerAdvice。class); @ExceptionHandler(value = Exception。class) public ResResult handleException(Exception ex) { logger。error(ex。getMessage(), ex); return ResResult。error(ex。getMessage()); } @ExceptionHandler(value = BusinessException。class) public ResResult handleBusinessException(BusinessException ex) { ExceptionStatus annotation = ex。getClass()。getAnnotation(ExceptionStatus。class); if (null == annotation) { logger。error(“Invalid annotation config, {}”, ex。getClass(), ex); return ResResult。error(“系統異常”); } logger。error(“系統異常”, ex); return ResResult。error(annotation。status(), ex。getTips()); }}

這裡就能體現出BusinessException這個類定義的意義了,如果沒有這個類,那麼增加一種自定義異常就要在這裡有一個對應的捕獲邏輯,而有了這個類,只需要對它進行捕獲,需要增加新的自定義異常型別時,只需要繼承它進行擴充套件就可以了。

同時,需要在ResResult類中另一個靜態方法

public static ResResult error(ResStatus status, String msg) { ResResult result = new ResResult(); result。code = status。getCode(); result。msg = msg; result。timestamp = String。valueOf(Instant。now()。toEpochMilli()); return result;}

5、測試自定義異常捕獲

在IndexController中加一個api測試未登入異常的捕獲

@GetMapping(value = “/exception/neverlogin”) public void exceptionNeverLogin() throws NeverLoginException { throw new NeverLoginException(“未登入,請登入後再試”, “Never Login userId: 10010”); }

執行專案,訪問http://lcoalhost:8080/exception/neverlogin,得到結果

實戰SpringBoot(7)透過Aop機制進行全域性異常捕獲

再看一下後端日誌

com。sidianban。pangu。core。exception。NeverLoginException: Never Login userId: 10010 at com。sidianban。pangu。controller。IndexController。exceptionNeverLogin(IndexController。java:27) ~[main/:na] at java。base/jdk。internal。reflect。NativeMethodAccessorImpl。invoke0(Native Method) ~[na:na] at java。base/jdk。internal。reflect。NativeMethodAccessorImpl。invoke(NativeMethodAccessorImpl。java:62) ~[na:na] at java。base/jdk。internal。reflect。DelegatingMethodAccessorImpl。invoke(DelegatingMethodAccessorImpl。java:43) ~[na:na] at java。base/java。lang。reflect。Method。invoke(Method。java:566) ~[na:na]

完美!~

總結

本篇介紹了透過Aop機制進行全域性異常捕獲,並不斷最佳化完成了一個相對完整的示例。

示例程式碼:

sidianban: 實戰SpringBoot示例程式碼 - Gitee。com

下一篇計劃介紹一下攔截器Interceptor,敬請期待~

番外篇

寫這篇小文的時候,有幾個問題一直縈繞在我腦中,平時也經常能看到同事們在這幾個問題上犯錯誤,寫出來供大家一起思考討論:

為什麼要進行異常捕獲?

應該在什麼地方進行異常捕獲?

異常捕獲之後應該怎麼處理?

最近連續碼了幾篇文章,深深地體會到碼字真累!希望大家多多支援點選關注,賜予我力量繼續碼下去~~~