七、Spring Boot 錯誤處理原理 & 定製錯誤頁面
【1】錯誤預設處理機制、
1)瀏覽器,返回一個預設的錯誤頁面
請求頭:
2) 其他客戶端訪問,預設響應 JSON 資料
請求頭:
為什麼會產生這樣的預設效果?
原理:可以參照 ErrorMvcAutoConfiguration;錯誤處理的自動配置;
ErrorMvcAutoConfiguration 給容器中添加了以下元件
1)、DefaultErrorAttributes
@Bean //@ConditionalOnMissingBean: 容器中沒有 ErrorAttributes 元件 時,會新增 DefaultErrorAttributes 元件 @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); }
ErrorMvcAutoConfiguration 原始碼:
@Order(Ordered.HIGHEST_PRECEDENCE) public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR"; @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { storeErrorAttributes(request, ex); return null; } private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_ATTRIBUTE, ex); } @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { //頁面能獲取的資訊 Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; } private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { Integer status = getAttribute(requestAttributes, "javax.servlet.error.status_code"); if (status == null) { errorAttributes.put("status", 999); errorAttributes.put("error", "None"); return; } errorAttributes.put("status", status); try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception ex) { // Unable to obtain a reason errorAttributes.put("error", "Http Status " + status); } } private void addErrorDetails(Map<String, Object> errorAttributes, RequestAttributes requestAttributes, boolean includeStackTrace) { Throwable error = getError(requestAttributes); if (error != null) { while (error instanceof ServletException && error.getCause() != null) { error = ((ServletException) error).getCause(); } errorAttributes.put("exception", error.getClass().getName()); addErrorMessage(errorAttributes, error); if (includeStackTrace) { addStackTrace(errorAttributes, error); } } Object message = getAttribute(requestAttributes, "javax.servlet.error.message"); if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) { errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message); } } private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) { BindingResult result = extractBindingResult(error); if (result == null) { errorAttributes.put("message", error.getMessage()); return; } if (result.getErrorCount() > 0) { errorAttributes.put("errors", result.getAllErrors()); errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount()); } else { errorAttributes.put("message", "No errors"); } } private BindingResult extractBindingResult(Throwable error) { if (error instanceof BindingResult) { return (BindingResult) error; } if (error instanceof MethodArgumentNotValidException) { return ((MethodArgumentNotValidException) error).getBindingResult(); } return null; } private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); stackTrace.flush(); errorAttributes.put("trace", stackTrace.toString()); } private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri"); if (path != null) { errorAttributes.put("path", path); } } @Override public Throwable getError(RequestAttributes requestAttributes) { Throwable exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE); if (exception == null) { exception = getAttribute(requestAttributes, "javax.servlet.error.exception"); } return exception; } @SuppressWarnings("unchecked") private <T> T getAttribute(RequestAttributes requestAttributes, String name) { return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); } }
2)、BasicErrorController: 處理預設/error請求
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; /** * Create a new {@link BasicErrorController} instance. * @param errorAttributes the error attributes * @param errorProperties configuration properties */ public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { this(errorAttributes, errorProperties, Collections.<ErrorViewResolver>emptyList()); } /** * Create a new {@link BasicErrorController} instance. * @param errorAttributes the error attributes * @param errorProperties configuration properties * @param errorViewResolvers error view resolvers */ public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) { super(errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorProperties = errorProperties; } @Override public String getErrorPath() { return this.errorProperties.getPath(); } @RequestMapping(produces = "text/html")//產生html型別的資料;瀏覽器傳送的請求來到這個方法處理 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(request, response, status, model); 裡執行的程式碼 protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { //拿到所有的 異常檢視解析器 得到 ModelAndView for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } } @RequestMapping @ResponseBody //產生json資料,其他客戶端來到這個方法處理; public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } /** * Determine if the stacktrace attribute should be included. * @param request the source request * @param produces the media type produced (or {@code MediaType.ALL}) * @return if the stacktrace attribute should be included */ protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) { IncludeStacktrace include = getErrorProperties().getIncludeStacktrace(); if (include == IncludeStacktrace.ALWAYS) { return true; } if (include == IncludeStacktrace.ON_TRACE_PARAM) { return getTraceParameter(request); } return false; } /** * Provide access to the error properties. * @return the error properties */ protected ErrorProperties getErrorProperties() { return this.errorProperties; } }
3)、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) {
//發生錯誤以後, 來到 error 請求進行處理 --> this.properties.getError().getPath() == "/error"
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath());
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
4)、DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new HashMap<Series, String>();
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), 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) {
//Spring Boot 可以找到 error/404.html
String errorViewName = "error/" + viewName;
//如果模板引擎可以解析地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
//模板引擎可用的情況下返回到 errorViewName 指定的檢視
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,在靜態資原始檔夾下找 errorViewName 對應的頁面
return resolveResource(errorViewName, model);
}
步驟:
一但系統出現4xx或者5xx之類的錯誤;ErrorPageCustomizer就會生效(定製錯誤的響應規則);就會來到/error請求;就會被BasicErrorController處理;
【2】定製錯誤響應
1.如何定製錯誤頁面
1)、有模板引擎的情況下, error/404.html 【將錯誤頁面命名為 錯誤狀態碼.html 放在模板引擎資料夾裡面的error資料夾下】,發生此狀態碼的錯誤就會來到 對應的頁面;
路徑圖:
效果圖
我們可以使用4xx和5xx作為錯誤頁面的檔名來匹配這種型別的所有錯誤,精確優先(優先尋找精確的狀態碼.html);
頁面能獲取的資訊;
timestamp:時間戳
status:狀態碼
error:錯誤提示
exception:異常物件
message:異常訊息
errors:JSR303資料校驗的錯誤都在這裡
2)、沒有模板引擎(模板引擎找不到這個錯誤頁面),靜態資原始檔夾下找;
3)、以上都沒有錯誤頁面,就是預設來到SpringBoot預設的錯誤提示頁面;
Spring Boot 預設提示頁面原始碼:
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean(BeanNameViewResolver.class)
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
2.如何定製錯誤的 JSON 資料
第一種,使用SpringMVC的異常處理器
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String,Object> handlerException(Exception e, HttpServletRequest request) {
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message","使用者出錯啦");
return map;
}
}
這樣無論瀏覽器還是客戶端返回的都是JSON!
第二種,轉發到/error請求進行自適應效果處理
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//傳入我們自己的錯誤狀態碼 4xx 5xx
/**
* Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
*/
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message","使用者出錯啦");
//轉發到/error
return "forward:/error";
}
但是此時沒有將自定義 code message傳過去!
第三種,註冊MyErrorAttributes繼承自DefaultErrorAttributes(推薦)
BasicErrorController 中對 /error 請求有兩種方式,兩種方式的錯誤資料都是通過DefaultErrorAttributes.getErrorAttributes()方法獲取,如下所示:
//BasicErrorController 類部分程式碼
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
}
//DefaultErrorAttributes 類部分程式碼
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
我們可以編寫一個MyErrorAttributes繼承自DefaultErrorAttributes重寫其getErrorAttributes方法將我們的錯誤資料新增進去。
示例如下:
//給容器中加入我們自己定義的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回值的map就是頁面和json能獲取的所有欄位
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
//DefaultErrorAttributes的錯誤資料
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
map.put("company","SpringBoot");
//① 我們的異常處理器攜帶的資料
Map<String,Object> ext = (Map<String, Object>) requestAttributes.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
①我們的異常處理器攜帶的資料
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//傳入我們自己的錯誤狀態碼 4xx 5xx
/**
* Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
*/
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message","使用者出錯啦");
//將自定義錯誤資料放入request中
request.setAttribute("ext",map);
//轉發到/error
return "forward:/error";
}
5xx.html頁面程式碼如下:
瀏覽器測試效果如下:
客戶端測試效果如下: