滿屏的try-catch,不瘮得慌?

持續原創輸出,點擊上方藍字關注我

目錄

  • 前言
  • 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類型的異常,因此在全局異常處理的時候需要統一處理。幾個常見的異常如下:

  1. NoHandlerFoundException:客戶端的請求沒有找到對應的controller,將會拋出404異常。
  2. HttpRequestMethodNotSupportedException:若匹配到了(匹配結果是一個列表,不同的是http方法不同,如:Get、Post等),則嘗試將請求的http方法與列表的控制器做匹配,若沒有對應http方法的控制器,則拋該異常
  3. HttpMediaTypeNotSupportedException:然後再對請求頭與控制器支持的做比較,比如content-type請求頭,若控制器的參數簽名包含註解@RequestBody,但是請求的content-type請求頭的值沒有包含application/json,那麼會拋該異常(當然,不止這種情況會拋這個異常)
  4. MissingPathVariableException:未檢測到路徑參數。比如url爲:/user/{userId},參數簽名包含@PathVariable("userId"),當請求的url爲/user,在沒有明確定義url爲/user的情況下,會被判定爲:缺少路徑參數

如何統一異常處理?

在統一異常處理之前其實還有許多東西需要優化的,比如統一結果返回的形式。當然這裏不再細說了,不屬於本文範疇。

統一異常處理很簡單,這裏以前後端分離的項目爲例,步驟如下

  1. 新建一個統一異常處理的一個類
  2. 類上標註@RestControllerAdvice這一個註解,或者同時標註@ControllerAdvice@ResponseBody這兩個註解。
  3. 在方法上標註@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失效過期等等異常,如果整合了其他的框架,還要注意這些框架拋出的異常,比如ShiroSpring Security等等框架。

異常匹配的順序是什麼?

有些朋友可能疑惑了,如果我同時捕獲了父類和子類,那麼到底能夠被那個異常處理器捕獲呢?比如ExceptionServiceException

此時可能就疑惑了,這裏先揭曉一下答案,當然是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。值越小,匹配度越高越精準。

總結

全局異常的文章萬萬千,能夠講清楚的能有幾篇呢?只出最精的文章,做最野的程序員,如果覺得不錯的,關注分享走一波,謝謝支持!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章