目錄
一、默認處理機制
1.瀏覽器
當瀏覽器發送錯誤請求時,SpringBoot會有一個默認的錯誤處理,例如:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8iCAEfAq-1588145231789)(C:\Users\four and ten\Desktop\筆記\JAVA\Spring Boot\32.png)]
SpringBoot會返回一個錯誤頁面,這個錯誤頁面裏有狀態碼和錯誤信息
2.postman
會返回json數據
二、默認錯誤處理機制代碼分析
錯誤處理機制的自動配置都在ErrorMvcAutoConfiguration這個類裏,這個
1.ErrorPageCustomizer
ErrorMvcAutoConfiguration添加了ErrorPageCustomizer這個組件
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties);
}
ErrorPageCustomizer這個組件是添加錯誤映射
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
protected ErrorPageCustomizer(ServerProperties properties) {
this.properties = properties;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
+ this.properties.getError().getPath());
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
那麼一但發生錯誤式,會去到那個頁面呢,就在getPath()方法裏找答案
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
也就是說,如果瀏覽器一但發送錯誤請求,會發送/error請求
2.BasicErrorController
當服務器收到/error請求時,來到BasicErrorController來處理/error請求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
很顯然,這個Controller就是專門用來處理/error請求的,根據一開始。當使用不同的客戶端時,響應的數據格式不一樣,那麼具體代碼就在這個Controller中
@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);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
當使用瀏覽器發生錯誤頁面時:
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public static final String TEXT_HTML_VALUE = "text/html";
瀏覽器的請求頭帶有test/html,進入第一個RequestMapping,返回ModelAndView
當使用其他客戶端時,進入另外一個RequestMapping中處理請求,返回鍵值對集合,也就是json數據。
3.DefaultErrorViewResolver
錯誤頁面的Controller是帶有text/html的請求頭的請求
@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);
}
實現resolveErrorView的具體實現類是DefaultErrorViewResolver,打開這個類
@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;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默認SpringBoot去找一個頁面 error/狀態碼
String errorViewName = "error/" + viewName;
//如果模板引擎可以解析這個頁面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情況下返回到errorViewName指定的視圖地址
return new ModelAndView(errorViewName, model);
}
////模板引擎不可用,就在靜態資源文件夾下找errorViewName對應的頁面 error/狀態碼.html
return resolveResource(errorViewName, model);
}
到這裏就可以得出定製錯誤頁面的方法,可以在靜態資源文件夾下創建error/狀態碼.html或者在template文件夾下創建error/狀態碼.html
4.DefaultErrorAttributes
在BasicErrorController調用了getErrorAttributes方法,而這個方法的具體實現是DefaultErrorAttributes中的,作用就是共享錯誤信息
@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;
}
5.總結
當瀏覽器(或者其他客戶端)發送了錯誤請求時,會來到/error請求,這個/error請求映射是在ErrorPageCustomizer這個組件中完成的,BasicErrorController收到/error請求開始處理這個請求,然後根據請求頭來判斷是瀏覽器還是其他客戶端,如果是瀏覽器,那麼返回ModelAndView;如果是其他客戶端返回json數據
DefaultErrorViewResolver這個組件便是生成錯誤頁面的,返回一個ModelAndView對象,而這些視圖對象中有一些共享的數據,例如timestamp,status等等,DefaultErrorAttributes完成了這些共享數據的解析。
三、定製錯誤頁面
DefaultErrorViewResolver的resolve方法告訴我們如何去定製自己的錯誤頁面
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//默認SpringBoot去找一個頁面 error/狀態碼
String errorViewName = "error/" + viewName;
//如果模板引擎可以解析這個頁面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情況下返回到errorViewName指定的視圖地址
return new ModelAndView(errorViewName, model);
}
////模板引擎不可用,就在靜態資源文件夾下找errorViewName對應的頁面 error/狀態碼.html
return resolveResource(errorViewName, model);
}
1.使用模板引擎
有模板引擎的情況下,將錯誤頁面放在templates(thymeleaf的默認解析文件夾)的error文件夾下並命名爲 狀態碼.html,也可以使用4xx和5xx作爲錯誤頁面的文件名來匹配這種類型的錯誤
DefaultErrorAttribute共享了一些錯誤信息,意味着我們可以在模板引擎中使用這些信息,
- timestamp:時間戳
- status:狀態碼
- error:錯誤提示
- exception:異常對象
- message:異常消息
- errors:JSR303數據校驗的錯誤都在這裏
這些錯誤信息在DefaultErrorAttributes都能找到
2.不使用模板引擎
如果不使用模板引擎,那麼DefaultErrorAttributes中的信息無法使用。方法還是一樣的,在靜態資源文件夾下找error/狀態碼.html
3.都不用
如果都不用,也就是返回的ModelAndView爲空
@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);
}
那麼,默認去找"error"這個視圖對象,在ErrorMvcAutoConfiguration這個類中加入了這個組件
private final StaticView defaultErrorView = new StaticView();
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
private static class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Log logger = LogFactory.getLog(StaticView.class);
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Date timestamp = (Date) model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp).append("</div>")
.append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
那麼錯誤頁面的默認顯示就完成了