一、前言
在 SpringBoot 項目中,對於異常的統一處理,可以採用 Spring 中@ControllerAdvice
註解標註的類來統一進行處理,也可以自定義異常處理的解決方案。在 SpringBoot 中,對異常的處理存在一些默認的策略,下面我們就分別來看一下。
默認情況下,SpringBoot項目異常頁面如下:
其實通過這個默認的提示頁面,能夠看出來,之所以能夠看到這個默認的異常提示頁面,是因爲開發者沒有提供明確映射路徑/error
,如果開發者提供了這個路徑,則此頁面則不會顯示,不過在 SpringBoot 項目中提供/error
路徑實屬下下策,爲什麼這麼說?因爲 SpringBoot 本身在處理異常時,即當所有條件都不滿足時,纔會去尋找/error
路徑,爲何要這麼複雜,這不是降低效率嗎?
現在,先來看看在 SpringBoot 中如何自定義異常頁面,就頁面屬性而言,可以分爲兩類,一類是靜態異常頁面,另一類是動態異常頁面。
二、靜態異常頁面
常見的異常可以分爲兩個派系,分別是 400 系列和 500 系列。自定義異常頁面也可以分爲兩類,一類是以 HTTP 響應碼來命名的,例如:402.html、404.html、500.html 等等,另一類則是直接定義一個 4xx.html,狀態響應碼在 400-499 範圍內都顯示 4xx.html 異常頁面,5xx.html 包含 500-599 範圍內的狀態響應碼都顯示 5xx.html 異常頁面。
默認情況下,是在classpath:/static/error/
下定義異常頁面,如下:
【劃重點啦】
如果項目中拋出狀態碼爲 500 的錯誤,則自動展示 500.html 異常頁面,若拋出的狀態碼爲 404,則自動展示 404.html 異常頁面。如果項目中存在 500.html 頁面,同時也存在 5xx.html 頁面,若此時發生 500 錯誤,則優先展示 500.html 頁面。這裏有個優先級原則:精確 > 模糊。
三、動態異常頁面
其實動態異常頁面定義與靜態異常頁面的方式相同,可以採用的模板技術包含:jsp、freemarker、thymeleaf。動態異常頁面命名可以是404、500等等精確的狀態碼命名方式,一般情況下,由於動態異常頁面可以直接展示異常詳細的信息,所以沒有必要挨個枚舉了,這裏就直接定義爲4xx.html、5xx.html。(這裏採用的是thymeleaf)
動態異常頁面定義如下:
【5xx.html】
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>templates-5xx</h1>
<tr>
<td>path</td>
<td th:text="${path}"></td>
</tr>
<tr>
<td>timestamp</td>
<td th:text="${timestamp}"></td>
</tr>
<tr>
<td>message</td>
<td th:text="${message}"></td>
</tr>
<tr>
<td>error</td>
<td th:text="${error}"></td>
</tr>
<tr>
<td>status</td>
<td th:text="${status}"></td>
</tr>
</body>
</html>
默認情況下,會展示5條異常相關信息,如下:
【劃重點啦】
如果動態頁面和靜態頁面都定義了異常處理頁面,例如:classpath:/templates/error/404.html
和classpath:/static/error/404.html
兩者同時存在,如果拋出異常,默認使用動態異常頁面。這裏也有一個優先級原則:動態 > 靜態。
【小結一下】
從前面可以看出,共有兩個優先級原則,分別是:精確 > 模糊、動態 > 靜態,但是這兩者的優先級原則是:前者 > 後者,我想這並不難理解。
四、源碼解讀
瞭解了前面的兩類異常頁面,下面,我將對上面兩組原則通過源碼的方式進行解釋。
【第一步】找到ErrorMvcAutoConfiguration
這個類,我們需要找到這個類中的conventionErrorViewResolver
方法,如下:
可以看到conventionErrorViewResolver
方法的返回值是DefaultErrorViewResolver
,即默認異常視圖解釋器,下面我們就進入DefaultErrorViewResolver
這個類中一探究竟。
【第二步】找到DefaultErrorViewResolver
類中的resolveErrorView
方法,如下:
首先我們來看看這個方法中的三個參數,request
表示請求的數據,status
表示狀態碼,model
表示異常數據,接下看我們一步一步往下看,該方法中第一行通過調用了resolve
方法來定義了視圖,進入resolve
方法中:
從第一行可以看到error/
路徑,這個便是SpringBoot中默認異常頁面的路徑,所有異常頁面可以放在該目錄下便會自動尋找,viewName
就是狀態碼,下面幾行的意思是,尋找動態模板提供者,如果有則返回,反之則進入resolveResource
方法中,如下:
前面resolve
方法中是尋找動態異常頁面,而resolveResource
方法則相反,則是尋找靜態異常頁面,從這個for
循環可以看到,有個東西很眼熟,就是getStaticLocations
方法了,這個我在以前的博客中講過,SpringBoot中靜態資源訪問方案,這裏還是說一下,裏面定義了4個靜態頁面訪問默認路徑,分別是:classpath:/META-INF/resources/
、classpath:/resources/
、classpath:/static/
、classpath:/public/
,所以靜態異常頁面放在這4個目錄下的error/
目錄下即可,下面就是在這幾個目錄下去尋找靜態異常頁面,若找到返回,反之則返回null
,然後再回到resolveErrorView
方法中,如下:
可以看到if
中條件的第一個已滿足,我們來看看第二個條件,SERIES_VIEWS
其實是該類前面定義的 4xx 和 5xx 的一個枚舉類型的Map
,而status.series()
返回的值就是具體的狀態碼,在400-499之間屬於4xx,在500-599之間則屬於5xx。如果兩個條件都滿足,會再次調用resolve
方法。
【小結一下】
通過對源碼的解讀,是不是就明白了爲什麼異常頁面要放在error/
目錄下呢?明白了異常頁面命名的方式呢?理解了兩個優先級原則呢?
五、自定義異常數據
一般情況下,在 SpringBoot 中,異常信息就是下面展示的 5 條,如下:
這 5 條異常數據定義在org.springframework.boot.web.servlet.error.DefaultErrorAttributes
中的getErrorAttributes
方法中,如下:
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
因爲該方法裏面還存在方法,現在我來整理成簡化版本的,如下:
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("status", status);
errorAttributes.put("error",HttpStatus.valueOf(status).getReasonPhrase());
errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message);
errorAttributes.put("path", path);
return errorAttributes;
}
其實,DefaultErrorAttributes
類本來定義在ErrorMvcAutoConfiguration
異常自動配置類中的errorAttributes
方法中定義,如下:
如果開發者未自己提供 ErrorAttributes
實例,則 SpringBoot 會默認提供一個ErrorAttributes
實例,即DefaultErrorAttributes
。
基於該原則,開發者自定義ErrorAttributes
實例有兩種方式,如下:
- 直接實現
ErrorAttributes
接口。 - 繼承
DefaultErrorAttributes
(推薦),因爲 DefaultErrorAttributes 中對異常數據的處理已經完成,開發者可以直接使用。
具體定義如下:
@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("myerror","這是我自定義的異常!");
return map;
}
}
自定義ErrorAttributes
後,記得使用@Component
註解成一個Bean
,這樣SpringBoot就不會使用默認的DefaultErrorAttributes
了,大家可以使用debug試一下。
【運行結果】
六、自定義異常視圖
異常視圖頁面默認的就是前面提到的靜態異常頁面和動態異常頁面,當然這個也是可以自定義的。首先,默認異常頁面加載邏輯在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 再分別去查找動態異常頁面和靜態異常頁面。
其實,要自定義異常視圖解析,也比較容易,由於ErrorMvcAutoConfiguration
類中默認提供了默認的視圖解析實例DefaultErrorViewResolver
,如果開發者未提供相關實例,則使用默認實例,若提供了相關實例,則默認實例就會失效。因此,自定義異常視圖,只需要提供一個ErrorViewResolver
即可,如下:
@Component
public class MyErrorViewResolver extends DefaultErrorViewResolver {
/**
* Create a new {@link DefaultErrorViewResolver} instance.
*
* @param applicationContext the source application context
* @param resourceProperties resource properties
*/
public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
super(applicationContext, resourceProperties);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView view = new ModelAndView();
view.setViewName("mangoError");
view.addAllObjects(model);
return view;
}
}
mangoError
就是自定義異常視圖名,這裏也可以自定義異常數據(直接在resolveErrorView
方法中定義一個model,然後將參數中的model拷貝過去即可,可以在新的model中添加或刪除異常數據,值得注意的是參數中的model類型是UnmodifiableMap
,即不可直接修改的Map),而不需要自定義MyErrorAttributes
。自定義完成後,提供一個名爲mangoError
的視圖,如下:
如此一來,自定義異常視圖就算成功了。
七、總結
其實還可以自定義異常控制器BasicErrorController
,但是我覺得太大動干戈了,沒必要,前面幾種方式已經可以滿足開發者大部分需求了。