Spring Boot 中關於自定義異常處理的套路!

純潔的微笑發表於2019-04-19

在 Spring Boot 專案中 ,異常統一處理,可以使用 Spring 中 @ControllerAdvice 來統一處理,也可以自己來定義異常處理方案。Spring Boot 中,對異常的處理有一些預設的策略,我們分別來看。

預設情況下,Spring Boot 中的異常頁面 是這樣的:

Spring Boot 中關於自定義異常處理的套路!

我們從這個異常提示中,也能看出來,之所以使用者看到這個頁面,是因為開發者沒有明確提供一個 /error 路徑,如果開發者提供了 /error 路徑 ,這個頁面就不會展示出來,不過在 Spring Boot 中,提供 /error 路徑實際上是下下策,Spring Boot 本身在處理異常時,也是當所有條件都不滿足時,才會去找 /error 路徑。那麼我們就先來看看,在 Spring Boot 中,如何自定義 error 頁面,整體上來說,可以分為兩種,一種是靜態頁面,另一種是動態頁面。

靜態異常頁面

自定義靜態異常頁面,又分為兩種,第一種 是使用 HTTP 響應碼來命名頁面,例如 404.html、405.html、500.html ....,另一種就是直接定義一個 4xx.html,表示400-499 的狀態都顯示這個異常頁面,5xx.html 表示 500-599 的狀態顯示這個異常頁面。

預設是在 classpath:/static/error/ 路徑下定義相關頁面:

Spring Boot 中關於自定義異常處理的套路!

此時,啟動專案,如果專案丟擲 500 請求錯誤,就會自動展示 500.html 這個頁面,發生 404 就會展示 404.html 頁面。如果異常展示頁面既存在 5xx.html,也存在 500.html ,此時,發生500異常時,優先展示 500.html 頁面。

動態異常頁面

動態的異常頁面定義方式和靜態的基本 一致,可以採用的頁面模板有 jsp、freemarker、thymeleaf。動態異常頁面,也支援 404.html 或者 4xx.html ,但是一般來說,由於動態異常頁面可以直接展示異常詳細資訊,所以就沒有必要挨個列舉錯誤了 ,直接定義 4xx.html(這裡使用thymeleaf模板)或者 5xx.html 即可。

注意,動態頁面模板,不需要開發者自己去定義控制器,直接定義異常頁面即可 ,Spring Boot 中自帶的異常處理器會自動查詢到異常頁面。

頁面定義如下:

Spring Boot 中關於自定義異常處理的套路!

頁面內容如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>5xx</h1>
<table border="1">
    <tr>
        <td>path</td>
        <td th:text="${path}"></td>
    </tr>
    <tr>
        <td>error</td>
        <td th:text="${error}"></td>
    </tr>
    <tr>
        <td>message</td>
        <td th:text="${message}"></td>
    </tr>
    <tr>
        <td>timestamp</td>
        <td th:text="${timestamp}"></td>
    </tr>
    <tr>
        <td>status</td>
        <td th:text="${status}"></td>
    </tr>
</table>
</body>
</html>
複製程式碼

預設情況下,完整的異常資訊就是這5條,展示 效果如下 :

Spring Boot 中關於自定義異常處理的套路!

如果動態頁面和靜態頁面同時定義了異常處理頁面,例如 classpath:/static/error/404.htmlclasspath:/templates/error/404.html 同時存在時,預設使用動態頁面。即完整的錯誤頁面查詢方式應該是這樣:

發生了500錯誤-->查詢動態 500.html 頁面-->查詢靜態 500.html --> 查詢動態 5xx.html-->查詢靜態 5xx.html。

自定義異常資料

預設情況下,在Spring Boot 中,所有的異常資料其實就是上文所展示出來的5條資料,這5條資料定義在 org.springframework.boot.web.reactive.error.DefaultErrorAttributes 類中,具體定義在 getErrorAttributes 方法中 :

@Override
public Map<String, Object> getErrorAttributes(ServerRequest request,
                boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        Throwable error = getError(request);
        HttpStatus errorStatus = determineHttpStatus(error);
        errorAttributes.put("status", errorStatus.value());
        errorAttributes.put("error", errorStatus.getReasonPhrase());
        errorAttributes.put("message", determineMessage(error));
        handleException(errorAttributes, determineException(error), includeStackTrace);
        return errorAttributes;
}
複製程式碼

DefaultErrorAttributes 類本身則是在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration 異常自動配置類中定義的,如果開發者沒有自己提供一個 ErrorAttributes 的例項的話,那麼 Spring Boot 將自動提供一個ErrorAttributes 的例項,也就是 DefaultErrorAttributes 。

基於此 ,開發者自定義 ErrorAttributes 有兩種方式 :

  1. 直接實現 ErrorAttributes 介面
  2. 繼承 DefaultErrorAttributes(推薦),因為 DefaultErrorAttributes 中對異常資料的處理已經完成,開發者可以直接使用。

具體定義如下:

@Component
public class MyErrorAttributes  extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        if ((Integer)map.get("status") == 500) {
            map.put("message", "伺服器內部錯誤!");
        }
        return map;
    }
}
複製程式碼

定義好的 ErrorAttributes 一定要註冊成一個 Bean ,這樣,Spring Boot 就不會使用預設的 DefaultErrorAttributes 了,執行效果如下圖:

Spring Boot 中關於自定義異常處理的套路!

自定義異常檢視

異常檢視預設就是前面所說的靜態或者動態頁面,這個也是可以自定義的,首先 ,預設的異常檢視載入邏輯在 org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController 類的 errorHtml 方法中,這個方法用來返回異常頁面+資料,還有另外一個 error 方法,這個方法用來返回異常資料(如果是 ajax 請求,則該方法會被觸發)。

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request,
                HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                        request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
複製程式碼

在該方法中 ,首先會通過 getErrorAttributes 方法去獲取異常資料(實際上會呼叫到 ErrorAttributes 的例項 的 getErrorAttributes 方法),然後呼叫 resolveErrorView 去建立一個 ModelAndView ,如果這裡建立失敗,那麼使用者將會看到預設的錯誤提示頁面。

正常情況下, resolveErrorView 方法會來到 DefaultErrorViewResolver 類的 resolveErrorView 方法中:

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
                Map<String, Object> model) {
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
                modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
}
複製程式碼

在這裡,首先以異常響應碼作為檢視名分別去查詢動態頁面和靜態頁面,如果沒有查詢到,則再以 4xx 或者 5xx 作為檢視名再去分別查詢動態或者靜態頁面。

要自定義異常檢視解析,也很容易 ,由於 DefaultErrorViewResolver 是在 ErrorMvcAutoConfiguration 類中提供的例項,即開發者沒有提供相關例項時,會使用預設的 DefaultErrorViewResolver ,開發者提供了自己的 ErrorViewResolver 例項後,預設的配置就會失效,因此,自定義異常檢視,只需要提供 一個 ErrorViewResolver 的例項即可:

@Component
public class MyErrorViewResolver extends DefaultErrorViewResolver {
    public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
        super(applicationContext, resourceProperties);
    }
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        return new ModelAndView("/aaa/123", model);
    }
}
複製程式碼

實際上,開發者也可以在這裡定義異常資料(直接在 resolveErrorView 方法重新定義一個 model ,將引數中的model 資料拷貝過去並修改,注意引數中的 model 型別為 UnmodifiableMap,即不可以直接修改),而不需要自定義MyErrorAttributes。定義完成後,提供一個名為123的檢視,如下圖:

Spring Boot 中關於自定義異常處理的套路!

如此之後,錯誤試圖就算定義成功了。

總結

實際上也可以自定義異常控制器 BasicErrorController ,不過鬆哥覺得這樣太大動干戈了,沒必要,前面幾種方式已經可以滿足我們的大部分開發需求了。

關注公眾號牧碼小子,專注於 Spring Boot+微服務,定期視訊教程分享,關注後回覆 Java ,領取鬆哥為你精心準備的 Java 乾貨!

Spring Boot 中關於自定義異常處理的套路!

相關文章