一、什么是异常处理
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() {
// 这个视图返回的其实就是我们的组件1
return this.defaultErrorView;
}
/**
* 组件2.2:视图解析器
* 容器中放一个视图解析器,这个视图解析器是BeanNameViewResolver,可以通过视图的名字来解析视图
* 这个就是和上面这个defaultErrorView配合工作的,他按照名字error查找到这个视图,然后渲染出来
* 返回,所以我们可以来替代这个视图,我们可以自己定义一个名字叫做error的视图。而他的主要实现
* 代码如下:
* return context.getBean(viewName, View.class);就是简单的传入一个视图名字,然后他从容器
* 中去取出来而已,其实就是封装了一个方法,用来从容器里面取我们注入进去的视图的。你说巧了不是
* 我们的组件2.1刚在容器里面放了一个白页的视图,这里其实就是用来取白页视图用的。配套方法而已。
*/
@Bean
@ConditionalOnMissingBean
public 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
*/
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
// 这里构建那个json
Map<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异常,比如404
SERVER_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;
// 省略构造函数
@Override
public 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、ExceptionHandlerExceptionResolver
3、ResponseStatusExceptionResolver
4、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
*/
@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, 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其实没走他的渲染,其实原理是一样的。后面我会补一张图,并且给出开发中的一些异常的操作。