1. 程式人生 > >滿屏的try-catch,不瘮得慌?

滿屏的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。值越小,匹配度越高越精準。

總結

全域性異常的文章萬萬千,能夠講清楚的能有幾篇呢?只出最精的文章,做最野的程式設計師,如果覺得不錯的,關注分享走一波,謝謝支援!!!