持續原創輸出,點擊上方藍字關注我
目錄
-
前言 -
Spring Boot 版本 -
全局統一異常處理的前世今生 -
Spring Boot的異常如何分類? -
如何統一異常處理? -
異常匹配的順序是什么? -
總結
前言
軟件開發過程中難免遇到各種的BUG,各種的異常,一直就是在解決異常的路上永不停歇,如果你的代碼中再出現try(){...}catch(){...}finally{...}代碼塊,你還有心情看下去嗎?自己不覺得惡心嗎?
冗余的代碼往往回喪失寫代碼的動力,每天搬磚似的寫代碼,真的很難受。今天這篇文章教你如何去掉滿屏的try(){...}catch(){...}finally{...},解放你的雙手。
Spring Boot 版本
本文基於的Spring Boot的版本是2.3.4.RELEASE。
全局統一異常處理的前世今生
早在Spring 3.x就已經提出了@ControllerAdvice,可以與@ExceptionHandler、@InitBinder、@ModelAttribute 等注解注解配套使用,這幾個此處就不再詳細解釋了。
這幾個注解小眼一瞟只有@ExceptionHandler與異常有關啊,翻譯過來就是異常處理器。其實異常的處理可以分為兩類,分別是局部異常處理和全局異常處理。
局部異常處理:@ExceptionHandler和@Controller注解搭配使用,只有指定的controller層出現了異常才會被@ExceptionHandler捕獲到,實際生產中怕是有成百上千個controller了吧,顯然這種方式不合適。
全局異常處理:既然局部異常處理不合適了,自然有人站出來解決問題了,於是就有了@ControllerAdvice這個注解的橫空出世了,@ControllerAdvice搭配@ExceptionHandler徹底解決了全局統一異常處理。當然后面還出現了@RestControllerAdvice這個注解,其實就是@ControllerAdvice和@ResponseBody結晶。
Spring Boot的異常如何分類?
Java中的異常就很多,更別說Spring Boot中的異常了,這里不再根據傳統意義上Java的異常進行分類了,而是按照controller進行分類,分為進入controller前的異常和業務層的異常,如下圖:
進入controller之前異常一般是javax.servlet.ServletException類型的異常,因此在全局異常處理的時候需要統一處理。幾個常見的異常如下:
-
NoHandlerFoundException:客戶端的請求沒有找到對應的controller,將會拋出404異常。 -
HttpRequestMethodNotSupportedException:若匹配到了(匹配結果是一個列表,不同的是http方法不同,如:Get、Post等),則嘗試將請求的http方法與列表的控制器做匹配,若沒有對應http方法的控制器,則拋該異常 -
HttpMediaTypeNotSupportedException:然后再對請求頭與控制器支持的做比較,比如content-type請求頭,若控制器的參數簽名包含注解@RequestBody,但是請求的content-type請求頭的值沒有包含application/json,那么會拋該異常(當然,不止這種情況會拋這個異常) -
MissingPathVariableException:未檢測到路徑參數。比如url為:/user/{userId},參數簽名包含@PathVariable("userId"),當請求的url為/user,在沒有明確定義url為/user的情況下,會被判定為:缺少路徑參數
如何統一異常處理?
在統一異常處理之前其實還有許多東西需要優化的,比如統一結果返回的形式。當然這里不再細說了,不屬於本文范疇。
統一異常處理很簡單,這里以前后端分離的項目為例,步驟如下:
-
新建一個統一異常處理的一個類 -
類上標注 @RestControllerAdvice這一個注解,或者同時標注@ControllerAdvice和@ResponseBody這兩個注解。 -
在方法上標注 @ExceptionHandler注解,並且指定需要捕獲的異常,可以同時捕獲多個。
下面是作者隨便配置一個demo,如下:
/** * 全局統一的異常處理,簡單的配置下,根據自己的業務要求詳細配置 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 重復請求的異常 * @param ex * @return */ @ExceptionHandler(RepeatSubmitException.class) public ResultResponse onException(RepeatSubmitException ex){ //打印日志 log.error(ex.getMessage()); //todo 日志入庫等等操作 //統一結果返回 return new ResultResponse(ResultCodeEnum.CODE_NOT_REPEAT_SUBMIT); } /** * 自定義的業務上的異常 */ @ExceptionHandler(ServiceException.class) public ResultResponse onException(ServiceException ex){ //打印日志 log.error(ex.getMessage()); //todo 日志入庫等等操作 //統一結果返回 return new ResultResponse(ResultCodeEnum.CODE_SERVICE_FAIL); } /** * 捕獲一些進入controller之前的異常,有些4xx的狀態碼統一設置為200 * @param ex * @return */ @ExceptionHandler({HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, MissingPathVariableException.class, MissingServletRequestParameterException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, TypeMismatchException.class, HttpMessageNotReadableException.class, HttpMessageNotWritableException.class, MissingServletRequestPartException.class, BindException.class, NoHandlerFoundException.class, AsyncRequestTimeoutException.class}) public ResultResponse onException(Exception ex){ //打印日志 log.error(ex.getMessage()); //todo 日志入庫等等操作 //統一結果返回 return new ResultResponse(ResultCodeEnum.CODE_FAIL); } }
注意:上面的只是一個例子,實際開發中還有許多的異常需要捕獲,比如TOKEN失效、過期等等異常,如果整合了其他的框架,還要注意這些框架拋出的異常,比如Shiro,Spring Security等等框架。
異常匹配的順序是什么?
有些朋友可能疑惑了,如果我同時捕獲了父類和子類,那么到底能夠被那個異常處理器捕獲呢?比如Exception和ServiceException。 
此時可能就疑惑了,這里先揭曉一下答案,當然是ServiceException的異常處理器捕獲了,精確匹配,如果沒有ServiceException的異常處理器才會輪到它的父親,父親沒有才會到祖父。總之一句話,精准匹配,找那個關系最近的。
為什么呢?這可不是憑空瞎說的,源碼為證,出處org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#getMappedMethod,如下:
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) { List<Class<? extends Throwable>> matches = new ArrayList<>(); //遍歷異常處理器中定義的異常類型 for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { //是否是拋出異常的父類,如果是添加到集合中 if (mappedException.isAssignableFrom(exceptionType)) { //添加到集合中 matches.add(mappedException); } } //如果集合不為空,則按照規則進行排序 if (!matches.isEmpty()) { matches.sort(new ExceptionDepthComparator(exceptionType)); //取第一個 return this.mappedMethods.get(matches.get(0)); } else { return null; } }
在初次異常處理的時候會執行上述的代碼找到最匹配的那個異常處理器方法,后續都是直接從緩存中(一個Map結構,key是異常類型,value是異常處理器方法)。
別着急,上面代碼最精華的地方就是對matches進行排序的代碼了,我們來看看ExceptionDepthComparator這個比較器的關鍵代碼,如下:
//遞歸調用,獲取深度,depth值越小越精准匹配
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) { //如果匹配了,返回 if (exceptionToMatch.equals(declaredException)) { // Found it! return depth; } // 遞歸結束的條件,最大限度了 if (exceptionToMatch == Throwable.class) { return Integer.MAX_VALUE; } //繼續匹配父類 return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1); }
精髓全在這里了,一個遞歸搞定,計算深度,depth初始值為0。值越小,匹配度越高越精准。
總結
全局異常的文章萬萬千,能夠講清楚的能有幾篇呢?只出最精的文章,做最野的程序員,如果覺得不錯的,關注分享走一波,謝謝支持!!!
