关于springboot的异常处理以及源码分析(一)

一、什么是异常处理

1、文档定义

首先我们先来看springboot官方对于异常处理的定义。springboot异常处理

在文档的描述中,我们首先可以看到的一个介绍如下:

markdown 复制代码
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。这里我们先不说自定义,我们先来看看,默认行为是不是真的是这样的。

java 复制代码
@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结尾的类都是做自动装配能力的。

于是我们点进去看看,我们看到他注入了很多组件,下面我们一一来分析一下。

鉴于理解顺序,我会从源码位置的从下到上来分析,但是都是在这个类里面的。

而且这个自动配置类有一些生效条件如下。

java 复制代码
@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等等。这个组件就是决定了有哪些内容的,我们来看下。

java 复制代码
@Bean
// 默认的异常处理,如果用户没有配置,就使用这个,你可以自己配置一个ErrorAttributes注入来替换他这个
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
	return new DefaultErrorAttributes();
}

但是我们还是要进去看看,他到底干嘛的。点进去我就后悔了,太TM长了,我们就从这个方法可以看到,他其实就是组装了一个map,里面确定了你能放的属性,也就是最后返回视图的内容。注意这里他组装了一个map,里面放着我们那些异常信息。

java 复制代码
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为他的静态视图添加异常信息。

java 复制代码
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 白页组装

在第一个组件有了之后,我们要在这个组件里面定义一套组件,来实现白页的组装。这是个静态内部类,里面注入了一系列组件来完成这件事。

java 复制代码
// 开启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还是浏览器客户端的白页,这个就是干这个功能的。

java 复制代码
/**
  生效条件,当不存在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得实现如下:

java 复制代码
/**
 * 这是一个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。然后他就生效了,所以这个解析器,是为了我们自己定制那个场景生效的。

java 复制代码
@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);
	}

}

我们再来看看这个默认视图解析器的能力。

java 复制代码
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中捕获,把异常保存在了一个变量里面。因为是处理异常的,所以这里就拿到了异常。

java 复制代码
dispatchException = ex;

紧接着往下走来到了这行代码:

java 复制代码
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

看名字也知道是处理结果的,而且,他传入了几个参数,分别是:

1、方法执行返回的结果。

2、response。

3、mappedHandler是谁处理的,哪个handler。

4、mv,也就是处理的返回结果视图。

然后我们进入这个方法。他的实现如下。

java 复制代码
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()来看这个真正处理异常的地方

java 复制代码
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

java 复制代码
@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的

java 复制代码
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

执行完了,我们接着往下看会看到这么一行代码。

java 复制代码
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

这里就是error最后走到这里处理他的视图。

java 复制代码
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其实没走他的渲染,其实原理是一样的。后面我会补一张图,并且给出开发中的一些异常的操作。

相关推荐
qq_4419960520 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼27 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring