一、起因
我們已經知道如果動態頁面和靜態頁面同時定義了異常處理頁面,例如 classpath:/static/error/404.html
和 classpath:/templates/error/404.html
同時存在時,默認使用動態頁面
即完整的錯誤頁面查找方式應該是這樣:發生了400錯誤--》查找動態 400.html 頁面--》查找靜態 400.html --》查找動態 4xx.html--》查找靜態 5xx.html
至於爲什麼有這樣的機制,本篇文章就從源碼來分析一下。
二、源碼分析
首先按兩下“shift”鍵,查詢ErrorMvcAutoConfiguration
類:
從其中挑選關鍵的代碼來說一下
首先是DefaultErrorAttributes
,如果我們沒有提供則使用系統自帶的,裏面提供了一些異常數據
@Bean
@ConditionalOnMissingBean(
value = {ErrorAttributes.class},
search = SearchStrategy.CURRENT
)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
另一個需要注意的是DefaultErrorViewResolver
,從名字就能看出來,這是一個錯誤視圖解析器,同樣自己沒有提供,則使用默認的,這個纔是我們分析的關鍵
@Bean
@ConditionalOnBean({DispatcherServlet.class})
@ConditionalOnMissingBean({ErrorViewResolver.class})
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
錯誤頁面的查找方式就是由這個方法控制的,進入DefaultErrorViewResolver
方法查看:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus
status, Map<String, Object> model) {
ModelAndView modelAndView =
this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series()))
{
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
這裏的HttpStatus status, Map<String, Object> model
,status 表示頁面狀態響應碼,model 表示頁面異常數據
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
將頁面響應狀態碼和異常數據傳入 resolve 方法中,
String errorViewName = "error/" + viewName;
解釋了爲什麼我們的錯誤頁面一定要放在error目錄下,因爲這裏默認加了error
前綴
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
查看是否有動態頁面模板,如果有則按動態頁面處理,沒有則按靜態頁面處理
這裏解釋了爲什麼先找動態頁面,再找靜態頁面
好的,接下來繼續進入resolveResource
,以此爲例,看看找到靜態頁面和動態頁面之後的下一步處理:
String[] var3 = this.resourceProperties.getStaticLocations();
查找靜態頁面位置:一篇文章帶你從源碼解析 SpringBoot 中的靜態資源存放位置
這就說明對於錯誤的頁面可以存放的位置有四個,別忘放在每個位置的 error 目錄下即可
這是靜態精確查找,如果沒有找到返回null
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
返回null ,表示精確查找不到,這裏的SERIES_VIEWS.containsKey(status.series()
就是模糊查找,比如400,定位的就是4xx,如果模糊匹配成功,則繼續調用resolve
,此時傳輸的頁面異常數據沒有改變,但是響應碼變了,分析如上。
三、自定義異常數據
已經分析了 DefaultErrorAttributes
中定義了一些異常數據,是一些默認設置,我們也可以根據需求自定義異常數據顯示
DefaultErrorAttributes
類本身則是在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
異常自動配置類中定義的,如果我們沒有提供一個 ErrorAttributes
的實例的話,那麼 Spring Boot 將自動提供一個ErrorAttributes
的實例,也就是 DefaultErrorAttributes
。
因此有兩種方式實現 ErrorAttributes :直接實現 ErrorAttributes 接口或者繼承 DefaultErrorAttributes
(推薦),因爲 DefaultErrorAttributes 中對異常數據的處理已經完成,可以直接使用。
MyErrorAttributes:
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
//首先拿到已經處理好的異常
Map<String, Object> map = super.getErrorAttributes(webRequest, options);
//然後自定義自己想要的部分異常信息
map.put("error", "這是自定的的信息");
return map;
}
}
4xx.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>4xx</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>
四、自定義異常視圖
前面已經進行了分析,當我們沒有自定義錯誤異常視圖時會使用SpringBoot默認的。
所以我們可以自定義類繼承 DefaultErrorViewResolver
,然後重寫這個 conventionErrorViewResolver
這個方法即可。
@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) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("yolo");
modelAndView.addAllObjects(model);
return modelAndView;
}
}