1. 程式人生 > >Spring Boot 統一異常這樣處理和剖析,安否?

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

話說異常

「欲渡黃河冰塞川,將登太行雪滿天」,無論生活還是計算機世界難免發生異常,上一篇文章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 資料解析出來,非常方便

推薦閱讀

  • 只會用 git pull ?有時候你可以嘗試更優雅的處理方式
  • 雙親委派模型:大廠高頻面試題,輕鬆搞定
  • 面試還不知道BeanFactory和ApplicationContext的區別?
  • 如何設計好的RESTful API
  • 程式猿為什麼要看原始碼?

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

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

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

相關推薦

Spring Boot 統一異常這樣處理剖析

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

spring boot AOPspring boot統一異常處理

一,spring AOPspring boot使用AOP,程式碼如下,程式碼比較簡單就不細說了,直接上程式碼,可以使用AOP做日誌處理package com.qwrt.fire.sensor.aop; import com.alibaba.fastjson.JSONArra

Spring Boot? 統一異常處理

xtend import put itl ava advice efault ges spring 效果區: 代碼區: package com.wls.integrateplugs.exception.dto; public class ErrorI

spring boot 統一異常處理

res status fin erro throwable instance 拋出異常 方案 let 需求源自於任何一個業務的編寫總會有各種各樣的條件判斷,需要時時手動拋出異常,又希望讓接口返回友好的錯誤信息。 spring boot提供的幫助是自動將異常重定向到路由為/e

測試開發專題:spring-boot統一異常捕獲

# java異常介紹 異常時相對於return的一種退出機制,可以由系統觸發,也可由程式通過throw語句觸發,異常可以通過try/catch語句進行捕獲並處理,如果沒有捕獲,則會導致程式退出並輸出異常棧資訊,異常有不同的型別,所有異常類都有一個共同的父類Throwable,下面我們先從Throwable開

spring boot 架構問題 時間處理 (對映時區問題)

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 4.12日 http://blog.csdn.net/buzaiguihun/article/details/

spring boot 統一返回資料及全域性異常處理

記錄關於spring boot 統一返回資料及全域性異常處理的操作實現。 一、統一返回資料 1、定義一個超類:BaseResponseVo @Data @NoArgsConstructor public class BaseResponseVo{ protected Integer r

spring boot 系統異常統一處理

1.系統異常捕獲 @ControllerAdvice(annotations = {RestController.class}) public class GlobalExceptionHandler { private Logger logger = LoggerFactory.get

Spring Boot 統一返回資料結構以及全域性異常處理

前言 看了廖師兄的視訊後,結合自己以前的程式設計經驗總結下 : 在 web 開發過程中, 後端要統一返回的資料結構,便於前端處理。例如每個請求,我們都需要知道 : code : 伺服器返回的狀態碼(主要給程式設計師看)。例如 : 200 : 請求成功。

spring @ControllerAdvice統一異常處理 Ajax普通請求

import com.alibaba.fastjson.JSON; import com.zh.entity.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; im

詳解Spring MVC/Boot 統一異常處理最佳實踐

開發十年,就只剩下這套架構體系了! >>>   

使用Spring MVC統一異常處理實戰

tro 處理機制 tor attr 運行 target icon message 404錯誤 原文地址:http://cgs1999.iteye.com/blog/1547197 1 描述 在J2EE項目的開發中,不管是對底層的數據庫操作過程,還是業務層的處理過程,還是控

Spring Boot Environment的初始化處理

bst div 方法 ams jdb ram rep ntp table Spring Boot Environment的初始化和預處理實在啟動時完成的, 即SpringApplication的run方法中。 ConfigurableEnvironment environ

Spring Boot 全域性異常處理 與 Hibernate Validator校驗框架整合

Hibernate Validator校驗框架的使用 Spring boot已經集成了hibernate-validator,不需要引入maven,其他框架也可以自己引入: <dependency> <groupId>org.h

Spring MVC 統一異常處理的兩種方式

沒有廢話,直接來。 方式一 通過@ControllerAdvice 和 @ExceptionHandler 方法。 @ControllerAdvice 這個註解,可以將對於控制器的全域性配置放到註解了@ControllerAdvice的類上,它結合了 @Component 所以可

利用Spring進行統一異常處理的兩種方式

package com.jay.platform.exception.handler; import java.io.IOException; import java.net.ConnectException; import java.net.SocketTimeoutException; import

spring boot 全域性異常處理及自定義異常

全域性異常處理:定義一個處理類,使用@ControllerAdvice註解。@ControllerAdvice註解:控制器增強,一個被@Component註冊的元件。配合@ExceptionHandler來增強所有的@requestMapping方法。例如:@Exceptio

Spring MVC 統一異常處理總結

在一個Spring MVC專案中,使用統一異常處理,可以使維護程式碼變得容易。下面總結一下常用的3種方法。 實現HandlerExceptionResolver介面 實現HandlerExcep

9.玩轉Spring Boot 全域性異常處理@ControllerAdvice

玩轉Spring Boot 全域性異常處理@ControllerAdvice       在開發中出現異常後,可能需要一個統一處理的地方,來處理程式出現的異常,針對不同的異常做不同的處理,這裡我們通過@ExceptionHandler註解來實現。在WEB開發中,比如頁面出

Spring Boot 全域性異常處理

    當我們在開發一個專案時,往往需要對異常進行捕獲處理,以提供友好的資訊展示給使用者。但隨著業務