MVC下的错误处理核心类是ErrorMvcAutoConfiguration类;
这里先看一下ErrorMvcAutoConfiguration类的类声明:
@Configuration( proxyBeanMethods = false ) @ConditionalOnWebApplication( type = Type.SERVLET ) @ConditionalOnClass({Servlet.class, DispatcherServlet.class}) @AutoConfigureBefore({WebMvcAutoConfiguration.class}) @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class, WebMvcProperties.class}) public class ErrorMvcAutoConfiguration |
ErrorMvcAutoConfiguration类里面有下面几个重要的类:
①DefaultErrorAttributes类:
public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); } |
②BasicErrorController类:
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList())); } |
③DefaultErrorViewResolver类:
static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, ResourceProperties resourceProperties) { this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; }
@Bean @ConditionalOnBean({DispatcherServlet.class}) @ConditionalOnMissingBean({ErrorViewResolver.class}) DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); } } |
DefaultErrorAttributes类
DefaultErrorAttributes(this.serverProperties.
getError().isIncludeException());该类是通过ServerProperties的ErrorProperties来创建的;而在ErrorProperties中path指定了显示异常的文件路径(默认为/error目录),ErrorProperties配置的属性都是以server.error开头:
public class ErrorProperties { @Value("${error.path:/error}") private String path = "/error"; private boolean includeException; private ErrorProperties.IncludeStacktrace includeStacktrace; private final ErrorProperties.Whitelabel whitelabel; |
而ServerProperties对应的属性前缀都是server开头的:
在创建DefaultErrorAttributes对象时,includeException属性为false;
通过查看该类的resolveException、storeErrorAttributes、getErrorAttributes、addStatus、addErrorDetails、addErrorMessage、addStackTrace、addPath等方法,我们会发现该类的主要功能是:向WebRequest对象中添加timestamp时间戳、status状态码、error错误提示、exception异常对象、message异常消息、errors(JSR303数据校验错误)、trace、path属性;
下面是该类的源码:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR"; private final boolean includeException;
public DefaultErrorAttributes() { this(false); }
public DefaultErrorAttributes(boolean includeException) { this.includeException = includeException; }
public int getOrder() { return -2147483648; }
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { this.storeErrorAttributes(request, ex); return null; }
private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_ATTRIBUTE, ex); }
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap(); errorAttributes.put("timestamp", new Date()); this.addStatus(errorAttributes, webRequest); this.addErrorDetails(errorAttributes, webRequest, includeStackTrace); this.addPath(errorAttributes, webRequest); return errorAttributes; }
private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { Integer status = (Integer)this.getAttribute(requestAttributes, "javax.servlet.error.status_code"); if (status == null) { errorAttributes.put("status", 999); errorAttributes.put("error", "None"); } else { errorAttributes.put("status", status);
try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception var5) { errorAttributes.put("error", "Http Status " + status); }
} }
private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace) { Throwable error = this.getError(webRequest); if (error != null) { while(true) { if (!(error instanceof ServletException) || error.getCause() == null) { if (this.includeException) { errorAttributes.put("exception", error.getClass().getName()); }
this.addErrorMessage(errorAttributes, error); if (includeStackTrace) { this.addStackTrace(errorAttributes, error); } break; }
error = error.getCause(); } }
Object message = this.getAttribute(webRequest, "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 = this.extractBindingResult(error); if (result == null) { errorAttributes.put("message", error.getMessage()); } else { if (result.hasErrors()) { 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; } else { return error instanceof MethodArgumentNotValidException ? ((MethodArgumentNotValidException)error).getBindingResult() : 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 = (String)this.getAttribute(requestAttributes, "javax.servlet.error.request_uri"); if (path != null) { errorAttributes.put("path", path); }
}
public Throwable getError(WebRequest webRequest) { Throwable exception = (Throwable)this.getAttribute(webRequest, ERROR_ATTRIBUTE); if (exception == null) { exception = (Throwable)this.getAttribute(webRequest, "javax.servlet.error.exception"); }
return exception; }
private <T> T getAttribute(RequestAttributes requestAttributes, String name) { return requestAttributes.getAttribute(name, 0); } } |
BasicErrorController与DefaultErrorViewResolver类
跟踪该类的构造器,我们发现在其父类AbstractErrorController构造器时初始化了ErrorView的视图解析器List<ErrorViewResolver> errorViewResolvers;
这里重点查看下BasicErrorController.errorHtml方法:
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } |
通过resolveErrorView方法来返回视图模型,如果有则返回该视图模型,否则返回一个名字叫做“error”视图模型;
继续跟踪resolveErrorView方法源码:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { Iterator var5 = this.errorViewResolvers.iterator(); ModelAndView modelAndView; do { if (!var5.hasNext()) { return null; } ErrorViewResolver resolver = (ErrorViewResolver)var5.next(); modelAndView = resolver.resolveErrorView(request, status, model); } while(modelAndView == null); return modelAndView; } |
这里遍历了errorViewResolvers变量(参考第一自然段),然后转成ErrorViewResolver接口,接着我们查看该接口的实现类DefaultErrorViewResolver的resolveErrorView方法:
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; }
static { 这里,如果找不到对应的status.html文件,则默认就把status的第1位数字与SEIES_VIEWS来匹配,直接使用4xx.html或者5xx.html文件; |
继续跟踪resolve方法:
private ModelAndView resolve(String viewName, Map<String, Object> model) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); } |
这里继续跟踪resolveResource方法:
private ModelAndView resolveResource(String viewName, Map<String, Object> model) { String[] var3 = this.resourceProperties.getStaticLocations(); int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { String location = var3[var5]; try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model); } } catch (Exception var8) { } } return null; } |
我们可以得出如下结论:
①默认情况下去找/error/status.html文件;
②获取对应的Provider,如果有,则直接返回;
③如果没有对应的Provider,则去分别遍历如下目录classpath:/META-INF/resources/、classpath:/resources/、 classpath:/static/、classpath:/public/,去查找名叫status.html的文件:
总结,Spring Boot异常处理流程:
①有模板引擎的情况下,首先查找/error/status.html文件(将错误页面命名为:错误状态码.html,然后放到/templates/error文件夹下),当发生此状态码的错误时,Spring Boot就会找到该错误码.html文件;
②没有模板引擎的情况下,再去classpath:/META-INF/resources/、classpath:/resources/、 classpath:/static/、classpath:/public/查找status.html文件;
注意:这里的静态资源无法解析,只能原样展示HTML文件,也无法解析timestamp时间戳、status状态码、error错误提示、exception异常对象、message异常消息、errors(JSR303数据校验错误)、trace、path这些属性;
③如果还是没有找到对应的status.html,则Spring Boot根据状态码第1位,来用4xx和5xx来作为错误页面的文件名来匹配这种类型的所有错误;
④如果连4xx和5xx都匹配不上,则只能使用Spring Boot已经定义好的error视图;
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()); } |