一、什么是异常处理
1、文档定义
首先我们先来看springboot官方对于异常处理的定义。springboot异常处理
在文档的描述中,我们首先可以看到的一个介绍如下:
By default, Spring Boot provides an /error mapping that handles all errors in a sensible way,
and it is registered as a “global” error page in the servlet container. For machine clients,
it produces a JSON response with details of the error, the HTTP status, and the exception message.For browser clients, there is a “whitelabel” error view that renders the same data in HTML format (to customize it, add a View that resolves to error). To replace the default behavior completely, you can implement ErrorController and register a bean definition of that type or add a bean of type ErrorAttributes to use the existing mechanism but replace the contents.
默认情况下,Spring Boot提供了一个/error映射,以合理的方式处理所有错误,并且它在servlet容器中注册为“全局”错误页面。
对于机器客户端,它生成一个JSON响应,其中包含错误、HTTP状态和异常消息的详细信息。对于浏览器客户端,有一个“白标签”错误视图,在中呈现相同的数据HTML格式(要自定义它,请添加一个解析为错误的视图)。
要完全替换默认行为,可以实现ErrorController并注册该类型的bean定义,或添加ErrorAttributes类型的bean以供使用
现有的机制,但取代了内容。
我们看到这里描述的是,当我们发生错误的时候,他默认提供了一个/error的映射(其实就是一个controller方法),他会给你转到这个映射上面,然后返回不同的视图。其中对于机器客户端请求(比如postman这种)就会返回一个json的响应,自然是包含了你的异常信息的。如果对于浏览器客户端的请求,就会返回一个空白的异常页面,在浏览器端渲染出来。
而且你也可以替换默认行为,自己实现ErrorController。这里我们先不说自定义,我们先来看看,默认行为是不是真的是这样的。
@RestController
@RequestMapping("/test")
public class TestController {@GetMapping("/testError")public String testError() {int a = 1 / 0;return "error";}
}
我们声明了一个TestController ,里面有一个get请求,故意制造了一个错误,很经典的错误1/0。
我们分别用postman和浏览器来请求测试一下。
-
postman模拟机器客户端
-
浏览器模拟浏览器客户端
所以我们看到默认情况是没问题的。
2、定制异常返回页面
我们再来看文档的下面一部分。
我们看到这里说的是,你要是觉得那种白页太丑了,确实也太丑了,啥也没有。我们可以自己定制页面。定制页面的方式也很简单,就是在静态资源目录下面放一个目录error,然后目录下面放404的html用来返回404请求,可以放一个5xx的页面用来返回异常的页面,OK,我们就来试试。
我的结构如下:
我自己的页面其实就是显示一个一级标题,404 5XX这样。我们来试试。
我们看到没毛病,完全OK。
二、源码分析
1、组件功能
我们先来看一下源码,而源码的整体流程实现基于这些组件的能力串联起来,最后形成了一个处理流程。
在springboot中我们没有处理过异常,他就给我们提供给了这些能力,那一定是自动装配机制提供的。那我们就去autoconfigure这个包下面去找。
而这个功能其实是web开发才有的,于是就在自动装配的web包下面看看。
最终我们找到一个很像error包:org/springframework/boot/autoconfigure/web/servlet/error
我们看到这个包下面有一个ErrorMvcAutoConfiguration的类,这个一看就是自动注入的核心类,springboot底层各种AutoConfiguration结尾的类都是做自动装配能力的。
于是我们点进去看看,我们看到他注入了很多组件,下面我们一一来分析一下。
鉴于理解顺序,我会从源码位置的从下到上来分析,但是都是在这个类里面的。
而且这个自动配置类有一些生效条件如下。
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 属性绑定,你可以在配置文件配置这些内容来替代默认值
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
组件0:DefaultErrorAttributes视图里面有哪些内容
我们之前在页面看到过,视图返回的不管是浏览器看到的异常白页还是postman看到的异常json,都会有一些属性封装。
我们看到有时间,有异常信息,状态码500等等。这个组件就是决定了有哪些内容的,我们来看下。
@Bean
// 默认的异常处理,如果用户没有配置,就使用这个,你可以自己配置一个ErrorAttributes注入来替换他这个
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {return new DefaultErrorAttributes();
}
但是我们还是要进去看看,他到底干嘛的。点进去我就后悔了,太TM长了,我们就从这个方法可以看到,他其实就是组装了一个map,里面确定了你能放的属性,也就是最后返回视图的内容。注意这里他组装了一个map,里面放着我们那些异常信息。
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));if (Boolean.TRUE.equals(this.includeException)) {options = options.including(Include.EXCEPTION);}// 放异常if (!options.isIncluded(Include.EXCEPTION)) {errorAttributes.remove("exception");}// 放异常堆栈if (!options.isIncluded(Include.STACK_TRACE)) {errorAttributes.remove("trace");}if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {errorAttributes.put("message", "");}if (!options.isIncluded(Include.BINDING_ERRORS)) {errorAttributes.remove("errors");}return errorAttributes;
}
组件1:StaticView 静态视图
这是一个名为静态视图的类,他实现了springmvc中的视图接口view。
其中的render为该视图长啥样的渲染实现,我们就主要来看看这个render方法。注意这个render方法需要一个map为他的静态视图添加异常信息。
private static class StaticView implements View {// http返回类型为html这种页面类型
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response){// 设置response返回类型为页面类型,因为视图渲染的就是页面response.setContentType(TEXT_HTML_UTF8.toString());// html内容的字符串拼接StringBuilder builder = new StringBuilder();// 取出当前时间,这个取出来的就是我们组件0放进去的,来这里拼接页面Object timestamp = model.get("timestamp");// 取出异常信息Object message = model.get("message");// 取出异常堆栈Object trace = model.get("trace");if (response.getContentType() == null) {response.setContentType(getContentType());}// 下面拼接的就是那个白页,中间可能通过htmlEscape()方法去除了一些标签之类的,用map中的异常信息填补异常页的信息。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());
}
所以我们这里得到第一个组件,就是这里渲染了一个页面视图。
组件2:WhitelabelErrorViewConfiguration 白页组装
在第一个组件有了之后,我们要在这个组件里面定义一套组件,来实现白页的组装。这是个静态内部类,里面注入了一系列组件来完成这件事。
// 开启lite模式
@Configuration(proxyBeanMethods = false)
/**生效条件:当你在配置文件中配置了server.error.whitelabel以下配置才会生效。但是如果你没配置,matchIfMissing = true也会决定你依然生效,其实就是他自己有默认值,你就是不配人家也有个值,也能生效,但是你配置了,就按你的来了。
*/
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {// 这里定义了一个我们的组件1,其实就是准备好那个白页了private final StaticView defaultErrorView = new StaticView();/*** 组件2.1:定义白页视图* 容器中还会放置一个名字叫error的视图,这个视图生效的条件是当容器中没有叫做error的视图的时候* 源码这个就会生效,换言之,你可以自己定义一个来替换掉他的这个。*/@Bean(name = "error")@ConditionalOnMissingBean(name = "error")public View defaultErrorView() {// 这个视图返回的其实就是我们的组件1return this.defaultErrorView;}/*** 组件2.2:视图解析器* 容器中放一个视图解析器,这个视图解析器是BeanNameViewResolver,可以通过视图的名字来解析视图* 这个就是和上面这个defaultErrorView配合工作的,他按照名字error查找到这个视图,然后渲染出来* 返回,所以我们可以来替代这个视图,我们可以自己定义一个名字叫做error的视图。而他的主要实现* 代码如下:* return context.getBean(viewName, View.class);就是简单的传入一个视图名字,然后他从容器* 中去取出来而已,其实就是封装了一个方法,用来从容器里面取我们注入进去的视图的。你说巧了不是* 我们的组件2.1刚在容器里面放了一个白页的视图,这里其实就是用来取白页视图用的。配套方法而已。*/@Bean@ConditionalOnMissingBeanpublic BeanNameViewResolver beanNameViewResolver() {BeanNameViewResolver resolver = new BeanNameViewResolver();resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);return resolver;}
}
组件3:BasicErrorController 默认跳转
我们前面看文档的时候看到,当异常发生,他会跳转去一个controller的error请求,来转发异常是给机器客户端的json还是浏览器客户端的白页,这个就是干这个功能的。
/**生效条件,当不存在ErrorController的时候就用这个,要是你自己定义了,就用你的,所以这里也是扩展点以后我们可以自己定义来取代他这个。
*/
@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,所以这个controller和我们写的没啥区别,我们看到他的请求路径是这样的*${server.error.path:${error.path:/error}} 表达式的意思是:首先尝试解析 server.error.path 属性。如果该属性未定义,* 则使用 error.path 属性。如果 error.path 也未定义,则使用默认路径 /error。所以我们这里就可以知道,* 他的异常处理默认请求的controller大路径是/error,当然我们也可以通过配置文件来修改这个默认的请求路径。你改了就用你的了*/
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {private final ErrorProperties errorProperties;// 省略没用的....../*** 我们看到这里是异常html的处理,所以当你请求的异常是在页面的时候produces = MediaType.TEXT_HTML_VALUE* 此时就会进入这个方法,然后返回一个ModelAndView对象,这个对象里面包含了错误信息,并且给你跳转去* 错误页面,所以这个方法就是处理异常的。浏览器请求接口的异常来这里,然后通过ModelAndView跳去异常视图*/@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)// "text/html"public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {HttpStatus status = getStatus(request);Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));response.setStatus(status.value());// 构建异常视图,给前端返回,这里就用到了我们的组件2.2,他取到了error视图,返回到这里ModelAndView modelAndView = resolveErrorView(request, response, status, model);// 页面响应响应error这个视图,其实就是我们的白页视图return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}/*** 非页面的响应,在这里处理,直接返回json数据,比如我们用postman测试的时候,就会进入这个方法,不是给html页面响应* ResponseEntity返回类型就是字符串类型,其实就是个json*/@RequestMappingpublic ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {HttpStatus status = getStatus(request);if (status == HttpStatus.NO_CONTENT) {return new ResponseEntity<>(status);}// 这里构建那个jsonMap<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));return new ResponseEntity<>(body, status);}
}
组件4:DefaultErrorViewResolverConfiguration默认的视图解析器
奇怪了,我们上面在组件2.2已经有了一个视图解析器了,为啥这里又来一个,不知道你有没有印象,我们2.2组件是解析的默认的白页。但是我们还有一个场景是我们自己定制了异常页面,就是我们的400.html和5xx.html。然后他就生效了,所以这个解析器,是为了我们自己定制那个场景生效的。
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {private final ApplicationContext applicationContext;private final ResourceProperties resourceProperties;// 省略构造....../*** 注入bean* @ConditionalOnBean(DispatcherServlet.class):当你是DispatcherServlet才生效,* 其实就是web环境。我们这分析的就是web,你说尼玛呢。* * @ConditionalOnMissingBean(ErrorViewResolver.class):当容器中没有ErrorViewResolver* 的时候他生效,所以你依然可以自定义代替他。*/@Bean@ConditionalOnBean(DispatcherServlet.class)@ConditionalOnMissingBean(ErrorViewResolver.class)DefaultErrorViewResolver conventionErrorViewResolver() {return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);}}
我们再来看看这个默认视图解析器的能力。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {private static final Map<Series, String> SERIES_VIEWS;/**这里初始化了静态map,放了一个映射,我们看到其实放的就是异常码CLIENT_ERROR=4->4xx, 代表4xx异常,比如404SERVER_ERROR=5->5xx; 代表5xx异常,比如500*/ static {Map<Series, String> views = new EnumMap<>(Series.class);views.put(Series.CLIENT_ERROR, "4xx");views.put(Series.SERVER_ERROR, "5xx");SERIES_VIEWS = Collections.unmodifiableMap(views);}private ApplicationContext applicationContext;private final ResourceProperties resourceProperties;private final TemplateAvailabilityProviders templateAvailabilityProviders;private int order = Ordered.LOWEST_PRECEDENCE;// 省略构造函数@Overridepublic ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);// 根据异常的code来获得对应的视图if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);}return modelAndView;}/**根据异常的code来返回一个视图,viewName就是4xx还是5xx*/private ModelAndView resolve(String viewName, Map<String, Object> model) {// 视图的名字进一步拼接,我们看到他是去静态资源目录下获取error目录下的视图的。是不是对上了String errorViewName = "error/" + viewName;TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext);if (provider != null) {return new ModelAndView(errorViewName, model);}return resolveResource(errorViewName, model);}private ModelAndView resolveResource(String viewName, Map<String, Object> model) {for (String location : this.resourceProperties.getStaticLocations()) {try {// 拿到资源解析类Resource resource = this.applicationContext.getResource(location);// 取出我们的4xx或者5xx页面resource = resource.createRelative(viewName + ".html");if (resource.exists()) {// 如果取到了,就返回我们自己定制的视图return new ModelAndView(new HtmlResourceView(resource), model);}}catch (Exception ex) {}}return null;}// 省略不是核心的代码......
}
OK,至此,我们一共六个组件就全部登场了,而springboot的异常处理流程也就是在这六个组件的配合下完成的,下面我们就来看看他们是怎么合作来完成的这个功能。
2、异常处理流程
OK,我们来操作一下关于异常处理流程,我们首先老套路,所有的操作都位于org.springframework.web.servlet.DispatcherServlet#doDispatch
然后既然他是在我们方法执行之后的异常处理,那么我们就先找到方法执行。
看注释你也知道,这行代码就是真正的目标方法执行,我们把断点打在这里。然后发起请求。
然后我们在浏览器发出请求。
不出意外,我们看到了异常抛出,并且随后在catch中捕获,把异常保存在了一个变量里面。因为是处理异常的,所以这里就拿到了异常。
dispatchException = ex;
紧接着往下走来到了这行代码:
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
看名字也知道是处理结果的,而且,他传入了几个参数,分别是:
1、方法执行返回的结果。
2、response。
3、mappedHandler是谁处理的,哪个handler。
4、mv,也就是处理的返回结果视图。
然后我们进入这个方法。他的实现如下。
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {// 是否处理过了,就是个处理过没处理过的标识boolean errorView = false;// 异常是不是空,我们来到这里肯定不是空,因为已经抛出了除数为0的异常了,所以肯定会进来if (exception != null) {// 异常类型是不是ModelAndViewDefiningException,我们没定义过,所以不是这个if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}// 于是来到这里else {// 判断mappedHandler 是不是空,其实就是谁处理了我们这个方法,// 不为空,获取到Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);/**这里是真正处理我们的异常的地方,我们来看这个方法,这个方法经过解析器之后,什么也没干,就把异常抛出去了。**/mv = processHandlerException(request, response, handler, exception);// 所以这里必然mv这个视图没被渲染,他还是空的,errorView = (mv != null);}}......省略没用的
}
我们这里分析一下,processHandlerException()来看这个真正处理异常的地方
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,@Nullable Object handler, Exception ex) throws Exception {// ......省略没用的// 这里开始声明了一个视图,其实就是我们要返回的视图ModelAndView exMv = null;// 遍历所有的异常解析器,看那个一个能处理我们的异常,就交给哪个处理,根据下面的截图我们可以看到,这个视图解析器集合里面一共有四个解析器。/**1、DefaultErrorAttributes,如果你眼熟的话,其实可以看到,这就是我们上一小章看到的组件0我们跟着这个组件0进去看看他做啥了。其实就是在request域里面放了一下这个异常request.setAttribute(ERROR_ATTRIBUTE, ex);然后返回了一个空视图。下面还有三个解析器,很遗憾的告诉大家,这三个解析器都不符合解析要求,所以他们其实啥也没干。2、ExceptionHandlerExceptionResolver3、ResponseStatusExceptionResolver4、DefaultHandlerExceptionResolver*/if (this.handlerExceptionResolvers != null) {for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {exMv = resolver.resolveException(request, response, handler, ex);if (exMv != null) {break;}}}// 经过上面的解析我们得知其实只有我们的组件0生效了,但是返回了一个空的视图// 所以下面的都不会走,直接走到最后一步if (exMv != null) {if (exMv.isEmpty()) {request.setAttribute(EXCEPTION_ATTRIBUTE, ex);return null;}// We might still need view name translation for a plain error model...if (!exMv.hasView()) {String defaultViewName = getDefaultViewName(request);if (defaultViewName != null) {exMv.setViewName(defaultViewName);}}if (logger.isTraceEnabled()) {logger.trace("Using resolved error view: " + exMv, ex);}else if (logger.isDebugEnabled()) {logger.debug("Using resolved error view: " + exMv);}WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());return exMv;}// 我们看到,他什么都不会做,只会在这里把这个异常原模原样的抛出throw ex;
}
我们看到上面,在经过一系列异常解析器之后,他并没有一个解析器能处理它,他在最后就抛出了一个异常。当前请求就结束了,啊?你结束了,?那我那一堆组件都白给了?不会的,我们放行这一步请求就会看到一个现象。
他再次来到了入口处的org.springframework.web.servlet.DispatcherServlet#doDispatch这个方法,而且这次的请求路径是/error,这其实是servlet的规范,在无法处理异常之后,会抛出异常,再次发起一次请求,而请求的路径就是/error,不知道你有没有想起来,我们的组件3就是一个controller,并且他处理的请求路径,就是/error。
然后再次经过org.springframework.web.servlet.DispatcherServlet#doDispatch的派发,会得知我们这个controller可以处理这个/error
注意这个/error也是一次请求,所以也要走之前请求的路程,包括派发,拦截器等等。最终来到BasicErrorController 。
于是,我们这个第二次请求就会来到这个controller里面被处理。org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {private final ErrorProperties errorProperties;// 省略没用的......// 当请求打到/error这里的时候,会根据请求来的类型是html的还是postman这种类型的,走入不同的接口,因为我这次是页面请求的,所以我以这个方法为例。/*** 我们看到这里是异常html的处理,所以当你请求的异常是在页面的时候produces = MediaType.TEXT_HTML_VALUE* 此时就会进入这个方法,然后返回一个ModelAndView对象,这个对象里面包含了错误信息,并且给你跳转去* 错误页面,所以这个方法就是处理异常的。浏览器请求接口的异常来这里,然后通过ModelAndView跳去异常视图*/@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)// "text/html"public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {// 因为前面在当前request域中封装了异常,所以这里可以通过当前request获取到我们的异常// 包括异常的code和信息,封装在HttpStatus HttpStatus status = getStatus(request);/**这里是获取我们的DefaultErrorAttributes也就是组件0,来获取他里面能放的异常属性,然后扔到一个map里面。注意这个map,我们前面说过,空白页的异常新秀填补需要一个map,而这个map就是在这里弄出来的。*/Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));response.setStatus(status.value());// 构建异常视图,给前端返回/**解析异常视图,他会去通过我们的组件4,去静态目录下面获取是不是有我们的异常code对应的html,如果找到了,就包装为视图返回。*/ModelAndView modelAndView = resolveErrorView(request, response, status, model);// 页面响应响应error这个页面/**这里存在两个逻辑。1、我们的组件4解析的视图是不是为空,如果不是空,那就返回我们组件4解析的视图,也就是我们自己定义的那些4xx 5xx。2、如果为空,那么就返回一个new ModelAndView("error", model),返回了一个叫做error的ModelAndView。而同时把这个拥有异常信息的map放进去了。*/return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}/*** 非页面的响应,在这里处理,直接返回json数据,比如我们用postman测试的时候,就会进入这个方法,不是给html页面响应* ResponseEntity返回类型就是字符串类型,其实就是个json*/@RequestMappingpublic 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, getErrorAttributeOptions(request, MediaType.ALL));return new ResponseEntity<>(body, status);}@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {HttpStatus status = getStatus(request);return ResponseEntity.status(status).build();}protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {ErrorAttributeOptions options = ErrorAttributeOptions.defaults();if (this.errorProperties.isIncludeException()) {options = options.including(Include.EXCEPTION);}if (isIncludeStackTrace(request, mediaType)) {options = options.including(Include.STACK_TRACE);}if (isIncludeMessage(request, mediaType)) {options = options.including(Include.MESSAGE);}if (isIncludeBindingErrors(request, mediaType)) {options = options.including(Include.BINDING_ERRORS);}return options;}// 省略没用的......
}
所以到这里我们这个error的请求也就在doDispatch的
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
执行完了,我们接着往下看会看到这么一行代码。
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
这里就是error最后走到这里处理他的视图。
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception {// 这次就不是异常了,所以这里不走if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// 处理视图解析,这里就开始了,if (mv != null && !mv.wasCleared()) {/**最终会来到这里org.springframework.web.servlet.DispatcherServlet#resolveViewName这里面我们的组件2.2会登场,在容器中找到名字叫做error的视图,也就是我们的组件1,并且用我们前面构造的拥有异常信息的map来填补这个视图。并且经过组件2.1之后,把我们前面在mv里面塞的那些异常都给到组件2.1此时就返回了我们的那个白页。于是这样就返回了,我们的东西。所以,他是早就注入了空白页视图,然后拿到异常装在map里面,后面通过空白页视图解析器从容器找到这个视图,把map中的异常信息塞进去,就返回了。*/render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}
}
我们看到这个过程,组件0-4依次登场完成最后的处理。
因为我没有用4xx 5xx定制,所以组件4其实没走他的渲染,其实原理是一样的。后面我会补一张图,并且给出开发中的一些异常的操作。