前面完成了Day40——员工删除——删除完成,今天学习错误处理原理以及定制错误页面
有时候点击添加按钮,跳转页面失败(显示报错找不到路径)。或者输入日期信息,点击提交按钮,发生4xx状态报错。然后会跳转到下图这个错误页面(这是SpringBoot默认的错误处理页面),这个就是错误处理。如下:
SpringBoot默认配置的错误处理都是由ErrorMvcAutoConfiguration类配置的。之前说过一切的xxxAutoConfiguration都是SpringBoot的自动配置类,配置的值是从xxxProperties类中获取的。
核心组件都是在ErrorMvcAutoConfiguration配置的。有如下组件:
总结:SpringBoot自动配置错误处理是由ErrorMvcAutoConfiguration决定的。其中有4个核心组件起着重要作用,分别是:DefaultErrorAttributes、DefaultErrorViewResolver、BasicErrorController、ErrorPageCustomizer
后面会对这四个组件详细讲解
一旦系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(ErrorPageCustomizer是定制错误的响应规则)
ErrorMvcAutoConfiguration里面是这样注册ErrorPageCustomizer的,如下:
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
点进去看ErrorPageCustomizer的内容是什么,如下:
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
并且发现ErrorPageCustomizer类是ErrorMvcAutoConfiguration的静态内部类。
可以看到它有一个重载方法registerErrorPages(),如下:
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
发生错误以后,系统会去哪个路径呢?这个是由registerErrorPages()中的getPath()决定的,getPath()方法如下:
public String getPath() {
return this.path;
}
再点击path,查看它是怎么定义的,如下:
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
path是从配置文件中的error.path属性获取值,缺省值为/error
从上面path变量的定义可以得出,系统发生4xx/5xx错误后,会来到/error请求(也就是去到@RequestMapping(value="/error")
的controller中进行处理),请求处理此错误。
上面谈到,系统一发生4xx/5xx就会默认去/error的controller中处理请求。
SpringBoot自动配置类ErrorMvcAutoConfiguration中的BasicErrorController组件,如下:
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
点进去看看BasicErrorController是什么内容,如下:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
可以看到它是一个controller,而且处理的请求是配置文件中server.error.path属性值,缺省值是error.path属性值,error.path的缺省值是/error。从ErrorPageCustomizer类中,我们学习到系统一发生4xx/5xx就会默认去/error的controller中处理请求。也就是默认会去BasicErrorController请求处理。
再看到BasicErrorController有2个处理请求的核心方法,分别是 errorHtml()、error(),如下:
@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);
}
第一个方法errorHtml()返回值为ModelAndView,而且该方法会产生html类型的数据。这个方法就是在浏览器中展示错误页面的处理方法。比如浏览器访问某个地址,发生4xx错误,那么controller方法响应给它的是一个浏览器能显示的视图。
第二个方法返回值为ResponseEntity。这个方法就是在某些客户端展示错误的数据 的 方法。比如Postman访问某个地址,发生了4xx错误,那么controller方法响应给它的是一组json数据
是根据什么来区分到底用errorHtml()来处理/error还是用error()处理呢?
答:是根据请求头Request Headers的Accept属性来区分的。
解释:
在浏览器出现错误后,页面跳转到SpringBoot默认的错误页面,按F12,选择network可以看到如下:
因此浏览器发送的/error请求,BasicErrorController会使用errorHtml()来处理,因为errorHtml()是会产生html类型的数据。
在其他客户端出现错误后,发送的/error请求中,会有如下:
所以除了浏览器以外发送的请求,都会由BasicErrorController的error()方法处理。
总结1:浏览器发送的请求,会来到BasicErrorController的errorHtml()方法处理;其他客户端则来到BasicErrorController的error()方法处理。
总结2:一旦系统发生4xx/5xx之类的错误,ErrorPageCustomizer就会生效,就会发送/error请求,来到BasicErrorController的errorHtml()或error()进行处理。
现在分析是浏览器发送请求,所以详细看errorHtml()的内容,如下:
@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);
}
从倒数第二行看到,最终返回的是modelAndView或者new ModelAndView(x, model),而modelAndView是由resolveErrorView(x,x,x, model)。(注意,这里无论是由resolveErrorView()得到的modelAndView,还是new ModelAndView(),他们都使用了model这个数据。后面需要知道这个知识点)
先了解resolveErrorView(),它的作用是去哪个页面作为错误页面:包含页面地址以及页面内容。
resolveErrorView()方法如下:
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response,
HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
resolveErrorView()方法是拿到所有的errorViewResolvers,其中一个就是DefaultErrorViewResolver(后面会详细讲此组件)。这里的遍历使用了设计模式中的策略模式。拿到xxxErrorViewResolver后,就会执行resolverErrorView(),并将值赋给modelAndView。如果不为空,则把这个modelAndView返回出去,否则继续遍历。遍历完后,modelAndView仍为空,则返回null。所以结合上面errorHtml()方法返回值,当modelAndView为空的时候,它会new一个ModelAndView。
总结:errorHtml()方法里面是会调用到resolveErrorView()方法,而resolveErrorView()方法里面又会拿到所有的xxxErrorViewResolver,其中一个ErrorViewResolver就是DefaultErrorViewResolver。拿到这个DefaultErrorViewResolver的实例后,又会调用它的resolveErrorView()方法。 因此后面讲解DefaultErrorViewResolver
上面提到errorHtml()中最终会调用到DefaultErrorViewResolver的resolveErrorView()方法。
首先我们从根源找DefaultErrorViewResolver,在ErrorMvcAutoConfiguration中有如下:
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
点击DefaultErrorViewResolver,看看内容是什么,如下:(代码中的注解就是解释,注意看)
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");//所有错误页面都是用各自的状态码命名,太麻烦了
views.put(Series.SERVER_ERROR, "5xx");//这里使用4xx,5xx命名
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
可以看到它会处理4xx以及5xx的错误。
再来看看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;
}
从代码可知,最终返回的是modelAndView,它是由 resolve() 方法得来的。
再来看看rsolve()方法,如下:(代码中的注解就是解释,注意看)
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//SpringBoot默认可以找到一个error/404之类的页面。
String errorViewName = "error/" + viewName;
//模板引擎可以解析这个页面就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
//模板引擎可用的情况下,返回 error/viewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用,就在静态资源文件夹下找error/ViewName对应的页面,比如error/404
return resolveResource(errorViewName, model);
}
第二行代码可以看出,SpringBoot默认可以去找到一个页面,路径是error/viewName,而viewName是由入参得来的,而resolveErrorView传递进的入参是一个枚举类型,里面有很多状态码(比如1xx,2xx,3xx,4xx,5xx)。也就是SpringBoot可以找到一个error/404之类的页面。
最后一行调用了resolveResource(),如下:(代码中的注解就是解释,注意看)
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
//获取所有的存放静态资源的地址
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//创建错误处理的响应页面的名称,命名格式为xxx.html,比如为4xxx.html 或者 5xxx.html等等
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
//返回ModelAndView
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
总结:
从上面分析errorHtml()处理请求是怎么样的,我们可以自己定制错误页面。
错误状态码.html
,放在模板引擎文件夹里面的error文件夹下,发生此状态的错误就会来到对应的页面。由于错误状态码有太多太多,因此我们可以使用4xx/5xx匹配这种类型的所有错误,精确优先(优先寻找 精确的状态码.html)了解完SpringBoot是怎么寻找错误页面后,我们继续了解错误页面上的数据内容是从哪里获取的,以及可以获取什么数据。
错误页面上的数据肯定在modelAndView中,所以我们从controller中找起,前面我们了解过BasicErrorController,因此到BasicErrorController的errorHtml()找, 如下:(注意代码中的注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
//2.model是由getErrorAttributes()获取的
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//1. 这里model被传入modelAndView
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
从上面看到model被传入modelAndView,model是由getErrorAttributes()获取的。点进去看看getErrorAttributes(),如下:
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
点进去看看errorAttributes,如下:
public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
点进去看看ErrorAttributes,如下:
public interface ErrorAttributes {
Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);
Throwable getError(WebRequest webRequest);
}
ErrorAttributes是一个接口,我们将光标放在ErrorAttributes,按ctrl+alt+b,就会打开到如下:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
最终找到DefaultErrorAttributes,这个类在ErrorMvcAutoConfiguration中是有注册的。
DefaultErrorAttribute中最关键的一个属性是includeException,它是一个布尔值而且缺省值是false(所以它会导致某些异常信息不能被存储到某个Map中),通过它来决定是否添加某些异常信息到errorAttributes(这是一个Map,存储异常信息)中。 遇到过的问题有:SpringBoot获取不到${exception}
private final boolean includeException;
public DefaultErrorAttributes() {
this(false);
}
在DefaultErrorAttributes中找到getErrorAttributes(),如下:
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;
}
使用getErrorAttributes()方法,错误页面最终能获取到信息有如下:
总结:错误页面最终能获取到信息有timestamp:时间戳, status:状态码,error:错误提示, exception:异常对象,message:异常信息, eerrors:JSR303数据校验都在这里
在ErrorMvcAutoConfiguration中,有WhitelabelErrorViewConfiguration静态内部类,如下:
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();//这里存储了默认的错误页面内容
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;//返回默认错误视图
}
点击StaticView,可以看到如下:
private static class StaticView implements View {
它也是一个静态内部类,它有一个render()方法。
render()方法有部分代码如下:
builder.append("Whitelabel Error Page
").append(
"This application has no explicit mapping for /error, so you are seeing this as a fallback.
")
.append("").append(timestamp).append("")
.append("There was an unexpected error (type=").append(htmlEscape(model.get("error")))
.append(", status=").append(htmlEscape(model.get("status"))).append(").");
上面的内容就是SpringBoot的默认错误页面
四个核心组件,指的是前面介绍的ErrorPageCustomizer、BasicErrorController、DefaultErrorAttributes、DefaultErrorViewResolver。
整理SpringBoot寻找错误页面的路径,如下: