Spring Boot 統一異常這樣處理和剖析,安否?

  • 2019 年 10 月 3 日
  • 筆記

話說異常

「欲渡黃河冰塞川,將登太行雪滿天」,無論生活還是計算機世界難免發生異常,上一篇文章RESTful API 返回統一JSON數據格式 說明了統一返回的處理,這是請求一切正常的情形;這篇文章將說明如何統一處理異常,以及其背後的實現原理,老套路,先實現,後說明原理,有了上一篇文章的鋪底,相信,理解這篇文章就駕輕就熟了

實現

新建業務異常

新建 BusinessException.class 類表示業務異常,注意這是一個 Runtime 異常

@Data  @AllArgsConstructor  public final class BusinessException extends RuntimeException {        private String errorCode;        private String errorMsg;    }

添加統一異常處理靜態方法

在 CommonResult 類中添加靜態方法 errorResult 用於接收異常碼和異常消息:

public static <T> CommonResult<T> errorResult(String errorCode, String errorMsg){      CommonResult<T> commonResult = new CommonResult<>();      commonResult.errorCode = errorCode;      commonResult.errorMsg = errorMsg;      commonResult.status = -1;      return commonResult;  }

配置

同樣要用到 @RestControllerAdvice 註解,將統一異常添加到配置中:

@RestControllerAdvice("com.example.unifiedreturn.api")  static class UnifiedExceptionHandler{        @ExceptionHandler(BusinessException.class)      public CommonResult<Void> handleBusinessException(BusinessException be){          return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg());      }  }

三部搞定,到這裡無論是 Controller 還是 Service 中,只要拋出 BusinessException, 我們都會返回給前端一個統一數據格式

測試

將 UserController 中的方法進行改造,直接拋出異常:

@GetMapping("/{id}")  public UserVo getUserById(@PathVariable Long id){      throw new BusinessException("1001", "根據ID查詢用戶異常");  }

瀏覽器中輸入: http://localhost:8080/users/1

在 Service 中拋出異常:

@Service  public class UserServiceImpl implements UserService {        /**       * 根據用戶ID查詢用戶       *       * @param id       * @return       */      @Override      public UserVo getUserById(Long id) {          throw new BusinessException("1001", "根據ID查詢用戶異常");      }  }

運行是得到同樣的結果,所以我們儘可能的拋出異常吧 (作為一個程序猿這種心理很可拍)

解剖實現過程

解剖這個過程是相當糾結的,為了更好的說(yin)明(wei)問(wo)題(lan),我要說重中之重了,真心希望看該文章的童鞋自己去案發現場發現線索
還是在 WebMvcConfigurationSupport 類中實例化了 HandlerExceptionResolver Bean

@Bean  public HandlerExceptionResolver handlerExceptionResolver() {      List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();      configureHandlerExceptionResolvers(exceptionResolvers);      if (exceptionResolvers.isEmpty()) {          addDefaultHandlerExceptionResolvers(exceptionResolvers);      }      extendHandlerExceptionResolvers(exceptionResolvers);      HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();      composite.setOrder(0);      composite.setExceptionResolvers(exceptionResolvers);      return composite;  }

和上一篇文章一毛一樣的套路,ExceptionHandlerExceptionResolver 實現了 InitializingBean 接口,重寫了 afterPropertiesSet 方法:

@Override  public void afterPropertiesSet() {      // Do this first, it may add ResponseBodyAdvice beans      initExceptionHandlerAdviceCache();      ...  }    private void initExceptionHandlerAdviceCache() {      if (getApplicationContext() == null) {          return;      }        List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());      AnnotationAwareOrderComparator.sort(adviceBeans);        for (ControllerAdviceBean adviceBean : adviceBeans) {          Class<?> beanType = adviceBean.getBeanType();          if (beanType == null) {              throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);          }          // 重點看這個構造方法          ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);          if (resolver.hasExceptionMappings()) {              this.exceptionHandlerAdviceCache.put(adviceBean, resolver);          }          if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {              this.responseBodyAdvice.add(adviceBean);          }      }  }

重點看上面我用注釋標記的構造方法,代碼很好懂,仔細看看吧,其實就是篩選出我們用 @ExceptionHandler 註解標記的方法並放到集合當中,用於後續全局異常捕獲的匹配

/**   * A constructor that finds {@link ExceptionHandler} methods in the given type.   * @param handlerType the type to introspect   */  public ExceptionHandlerMethodResolver(Class<?> handlerType) {      for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {          for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {              addExceptionMapping(exceptionType, method);          }      }  }      /**   * Extract exception mappings from the {@code @ExceptionHandler} annotation first,   * and then as a fallback from the method signature itself.   */  @SuppressWarnings("unchecked")  private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {      List<Class<? extends Throwable>> result = new ArrayList<>();      detectAnnotationExceptionMappings(method, result);      if (result.isEmpty()) {          for (Class<?> paramType : method.getParameterTypes()) {              if (Throwable.class.isAssignableFrom(paramType)) {                  result.add((Class<? extends Throwable>) paramType);              }          }      }      if (result.isEmpty()) {          throw new IllegalStateException("No exception types mapped to " + method);      }      return result;  }    private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {      ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);      Assert.state(ann != null, "No ExceptionHandler annotation");      result.addAll(Arrays.asList(ann.value()));  }

到這裡,我們用 @RestControllerAdvice@ExceptionHandler 註解就會被 Spring 掃描到上下文,供我們使用

讓我們回到你最熟悉的調用的入口 DispatcherServlet 類的 doDispatch 方法:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {      ...        try {          ModelAndView mv = null;          Exception dispatchException = null;            try {              ...              // 當請求發生異常,該方法會通過 catch 捕獲異常              mv = ha.handle(processedRequest, response, mappedHandler.getHandler());          ...            }          catch (Exception ex) {              dispatchException = ex;          }          catch (Throwable err) {              // As of 4.3, we're processing Errors thrown from handler methods as well,              // making them available for @ExceptionHandler methods and other scenarios.              dispatchException = new NestedServletException("Handler dispatch failed", err);          }          // 調用該方法分析捕獲的異常          processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);      }      ...  }

接下來,我們來看 processDispatchResult 方法,這裡只要展示調用棧你就會眼前一亮了,又是為了返回統一格式數據:

總結

上一篇文章的返回統一數據格式是基礎,當異常情況發生時,只不過需要將異常信息提取出來。本文主要為了說明問題,剖析原理,好多地方設計方式是不可取,比如我們最好將異常封裝在一個 Enum 類,通過 enum 對象拋出異常等,如果你用到這些,去完善你的設計方案吧

回復 「demo」,打開鏈接,查看文件夾 「unifiedreturn」下內容,獲取完整代碼,更好閱讀體驗:
https://fraseryu.github.io/2019/08/09/ru-he-tong-yi-chu-li-yi-chang-bing-fan-hui-tong-yi-ge-shi/

附加說明

之前看到的一本書對異常的分類讓我印象深刻,在此摘錄一小段分享給大家:

結合出國旅行的例子說明異常分類:

  • 機場地震,屬於不可抗力,對應異常分類中的 Error,在制訂出行計劃時,根本不需要把這個部分的異常考慮進去
  • 堵車屬於 checked 異常,應對這種異常,我們可以提前出發,或者改簽機票。而飛機延誤異常,雖然也需要 check,但我們無能為力,只能持續關注航班動態
  • 沒有帶護照,明顯屬於可提前預測的異常,只要出發前檢查即可避免;去機場路上車子拋錨,這個異常是突發的,雖然難以預料,但是必須處理,屬於需要捕捉的異常,可以通過更換交通工具;應對檢票機器故障屬於 可透出異常,交由航空公司處理,我們無須關心

靈魂追問

  1. 這兩篇文章,你學到了哪些設計模式?
  2. 你能熟練的使用反射嗎?當看源碼是會看到很多反射的應用
  3. 你了解 Spring CGLIB 嗎?它的工作原理是什麼?

提高效率工具

JSON-Viewer

JSON-Viewer 是 Chrome 瀏覽器的插件,用於快速解析及格式化 json 內容,在 Chrome omnibox(多功能輸入框)輸入json-viewer + TAB ,將 json 內容拷貝進去,然後輸入回車鍵,將看到結構清晰的 json 數據,同時可以自定義主題

另外,前端人員打開開發者工具,雙擊請求鏈接,會自動將 response 中的 json 數據解析出來,非常方便

推薦閱讀


歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具匯總 | 回復「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回復「資料」

以讀偵探小說思維輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注……