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

news2024/11/21 11:37:12

一、什么是异常处理

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2064400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优思学院|如何在30分钟内评审一家供应商?SQE必需知道的11点

在供应商评审中&#xff0c;特别是时间有限的情况下&#xff0c;SQE&#xff08;供应商质量工程师&#xff09;需要通过高效的观察和分析来快速评估供应商的能力。在《哈佛商业评论》中&#xff0c;R. Eugene Goodson 的一篇“Read a Plant—Fast”文章正好提供了一个极为实用的…

python实现指数平滑法进行时间序列预测

python实现指数平滑法进行时间序列预测 一、指数平滑法定义 1、指数平滑法是一种常用的时间序列预测算法,有一次、二次和三次平滑,通过加权系数来调整历史数据权重; 2、主要思想是:预测值是以前观测值的加权和,且对不同的数据给予不同的权数,新数据给予较大的权数,旧数…

基于x86 平台opencv的图像采集和seetaface6的人脸识别功能

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境三、开发流程3.1 编写测试3.2 配置资源文件3.2 验证功能一、概述 本文档是针对x86 平台opencv的图像采集和seetaface6的人脸识别功能,opencv通过读取本地图像,将采集的本地图像送给seetaface6的人脸识别模块从而实现人脸识…

FreeRTOS学习笔记(四)——延时函数,列表,软件定时器,低功耗模式,内存管理

FreeRTOS学习笔记&#xff08;四&#xff09;——延时函数&#xff0c;列表&#xff0c;软件定时器&#xff0c;低功耗模式&#xff0c;内存管理 文章目录 FreeRTOS学习笔记&#xff08;四&#xff09;——延时函数&#xff0c;列表&#xff0c;软件定时器&#xff0c;低功耗模…

尚硅谷Java面试题第四季-MySQL面试题

1.如何建立复合索引&#xff0c;一般加在哪些字段&#xff1f;建索引的理论依据或者经验 2.Innodb的行锁到底锁了什么? 结论&#xff1a; InnoDB的行锁&#xff0c;是通过锁住索引来实现的&#xff0c;如果加锁查询的时候没有使用到索引&#xff0c;会将整个聚簇索引都锁住&am…

【python报错已解决】`Traceback (most recent call last)`

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引言 你是否在运行Python程序时遇到了Traceback (most recent call last)的错误&#xff1f;这个错误通常表明你的程序中有一个…

为什么大负载通电瞬间电压跌落,前级MOS开关如何设计

文章目录 1.1 前言1.2 简单典型电路1.3 分析优劣性1.4 优化后的开关电路1.5 具体原理分析1.6 实验验证效果1.7 适用应用场景 1.1 前言 电子产品设计电路某负载需要断电省下或异常下电复位&#xff0c;这时候会考虑在负载供电前端增加一个开关对其进行供电做控制&#xff0c;典…

超声波模块HC_SR04(hal库)

超声波模块HC_SR04 原理 1.触发信号&#xff1a;拉高至少10us的高电平 2.回响信号处理&#xff1a;计算高电平时长 3.计算距离&#xff1a;时间*速度&#xff08;声速&#xff09;/2&#xff08;注意单位问题&#xff09; 代码实现 方法1.下拉输入 配置 用于延时微秒us和…

第四届机械制造与智能控制国际学术会议(ICMMIC 2024)

目录 重要信息 大会介绍 主办单位 协办单位 大会主席 主讲嘉宾 征稿主题 会议日程 参会方式 重要信息 会议时间&#xff1a;2024年9月27-29日 大会官网&#xff1a;www.icmmic.com&#xff08;点击查看&#xff0c;大会信息&#xff0c;报名&#xff0c;投稿&#x…

【数据结构初阶】单链表接口实现超详解

文章目录 1. 顺序表问题与思考2.单链表2. 1 概念与结构2.1.1 结点2.1.2 链表的性质 3. 单链表实现3. 1 单链表初始化3. 2 单链表的打印3. 3 头插3. 4 创建新节点3. 5 尾插3. 6 头删和尾删3. 7 查找3. 8 在指定位置之后插入或删除3. 9 在指定位置前面插入或删除指定位置3. 10 销…

推荐一款好用的mac解压缩软件

文章目录 介绍软件功能安装下载使用注意事项1.打开系统设置选择隐私与安全性2.点击添加扩展3.勾选访达扩展 小结 介绍 FastZip for Mac集压缩、解压、预览、加密压缩、分卷压缩、固实压缩、右键压缩解压、多线程压缩等功能于一体&#xff0c;绝佳的设计、便捷的操作&#xff0…

设计师AI神器!AnyDesign,一张照片加简单描述就可以编辑时尚图像!

在AI时代很多行业都被颠覆了&#xff0c;建议大家在业余时间也尽量多学习一些AI工具的使用&#xff0c;提高效率的同时也去探索更多好玩的应用。 今天给大家介绍一个非常好用的图像编辑方法-AnyDesign&#xff0c;适合时尚设计师以及普通用户使用的&#xff0c;可以让你能够更…

Python编程:从入门到实践书籍介绍

对于Python入门的书籍推荐&#xff0c;以下是五本详细讲解的书籍&#xff0c;它们各自具有不同的特点和适用对象&#xff1a; 1. 《Python编程:从入门到实践》 作者&#xff1a;埃里克马瑟斯&#xff08;Eric Matthes&#xff09;《Python编程:从入门到实践》是一本经典的Pyth…

C#实现数据采集系统-数据反写(1)MQTT订阅接收消息

C#实现数据采集系统-数据反写 实现步骤 MQTT订阅&#xff0c;接收消息反写内容写入通信类&#xff0c;添加到写入队列中 链接-消息内容处理和写入通信类队列实现Modbustcp通信写入 具体实现 1.MQTT订阅&#xff0c;接收消息 Mqtt实现采集数据转发 Mqtt控制类增加订阅方法…

【微信小程序】使用 npm 包 - Vant Weapp --定制主题

小程序对 npm 的支持与限制 1. 什么是 Vant Weapp 官方文档地址 &#xff1a;https://youzan.github.io/vant-weapp 2. 安装 Vant 组件库 详细的操作步骤&#xff0c;大家可以参考 Vant 官方提供的快速上手教程&#xff1a; https://youzan.github.io/vant-weapp/#/quickst…

Texio电源维修德士直流电源维修PSW-1080M160

Texio德士电源维修类型有&#xff1a;指针式小型直流电源、数字显示直流电源、多路输出直流电源、直流电源、相控电源、低噪声开关电源、宽幅电源、高电压宽幅电源、开关电源、超薄直流电源、4象限双电源、高电压直流电源 Texio电源维修常见系列如下 PSW-系列是一款单路输出、…

框架——Mybatis(!!!MyBatis 环境搭建步骤)

目录 一、Mybatis 概述 1.背景 2.简介 3.Mybatis 中文官网 二、MyBatis 环境搭建&#xff08;超全&#xff01;&#xff01;&#xff01;&#xff09; 1.创建一张表和表对应的实体类 2.导入 MyBatis jar包,mysql数据库驱动包 3.创建 MyBatis全局配置文件 4. 在接口中…

2024年4款高质量的英语翻译工具推荐!

英语作为一门应用非常广泛的语言&#xff0c;其影响力还是非常深远的。虽然现在学英语的人很多&#xff0c;但对于非英语母语的人来说&#xff0c;英语多多少少会是一个壁垒&#xff0c;所以翻译工具也变得重要了起来。这次&#xff0c;我便要跟大家分享几个很专业的英语翻译工…

连锁门店收银系统源码+电子发票

传统纸质开票模式&#xff0c;流程复杂、时间长&#xff0c;为解决商户开票难的问题&#xff0c;千呼新零售2.0上线了电子发票功能&#xff0c;开通方便&#xff0c;使用简便&#xff01;商户只需要简单配置&#xff0c;就可以实现门店实现开票自由&#xff01; 一、线下订单开…

黑马程序员|8天Python第13章面向对象

一 初识对象 1.生活中数据的组织 2.程序中数据的组织 3.使用对象组织数据 类的属性&#xff1a; 二 成员方法 1.类的定义和使用 2.成员变量和成员方法 类外面是函数&#xff0c;类里面是方法。 3.成员方法的定义语法 self 只是写在这里&#xff0c;传参的时候可以当作不存在。…