SpringBoot默认的处理异常机制,默认错误页面是怎么产生的呢?

导读:本篇文章讲解 SpringBoot默认的处理异常机制,默认错误页面是怎么产生的呢?,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、前言

  在基于SpringBoot开发的过程中,经常遇见“Whitelabel Error Page”的错误页面,比如:

404错误:
在这里插入图片描述
500错误:
在这里插入图片描述
  在实际开发中,不止上面的这两种情况,还有很多其他情况,这里不再一一列举,这些错误页面是如何产生的,是如果工作的呢?其实这就是SpringBoot提供的默认的异常处理机制。我们这一节就来学习SpringBoot默认的异常处理机制。

二、主要接口或类

  在SpringBoot实现的异常处理逻辑中,有几个常用的接口或类,我们这里先简单了解一下:

  1. ErrorMvcAutoConfiguration 配置类,初始化异常处理的配置类
  2. ErrorViewResolver 异常视图解析器接口,默认的实现类是DefaultErrorViewResolver。
  3. ErrorAttributes 获取异常属性的接口,默认实现类是DefaultErrorAttributes。
  4. ErrorController 异常处理器接口类,标记类,提供了一个抽象实现类AbstractErrorController,而该抽象类又有一个实现类BasicErrorController,该类就是SpringBoot提供的默认异常处理器。

  SpringBoot实现异常处理的逻辑,就是通过ErrorMvcAutoConfiguration 实现初始化,然后交由异常处理器ErrorController 实现,而ErrorController 处理器则借助ErrorAttributes获取异常中的相关信息,而借助ErrorViewResolver 实现错误视图的解析。

三、初始化

  在前面《基于@ControllerAdvice注解实现全局异常处理用法和原理的探究》一篇内容中,我们使用到了@ControllerAdvice注解实现全局异常处理,在很多博文中,都把这个归类到了SpringBoot处理异常的方式,其实,该方法不仅可以在SpringBoot项目中可以使用,而且可以在只使用了SpringMVC(和Spring)的地方使用,只不过初始化方式和基于SpringBoot实现的有所差异而已。

  ErrorViewResolver 、ErrorAttributes 和ErrorController 三个的实现类的初始化过程都是在ErrorMvcAutoConfiguration 配置类中完成的。

  首先,ErrorAttributes的实现类DefaultErrorAttributes、ErrorController 的实现类
BasicErrorController都通过@Bean注解的方式实现,如下所示:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
	
	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}

	@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()));
	}
}

  然后ErrorViewResolver 实现类DefaultErrorViewResolver 是在内部配置类DefaultErrorViewResolverConfiguration 中进行注入的,也是通过@Bean注解实现的,如下所示:

//ErrorMvcAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {

	private final ApplicationContext applicationContext;

	private final ResourceProperties resourceProperties;

	DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
			ResourceProperties resourceProperties) {
		this.applicationContext = applicationContext;
		this.resourceProperties = resourceProperties;
	}
	@Bean
	@ConditionalOnBean(DispatcherServlet.class)
	@ConditionalOnMissingBean(ErrorViewResolver.class)
	DefaultErrorViewResolver conventionErrorViewResolver() {
		return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
	}
}

  除了上述配置的视图解析器DefaultErrorViewResolver外,还配置了默认视图,这个也是我们在开篇时候,提到的异常页面的生成的地方,配置如下:

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

	private final StaticView defaultErrorView = new StaticView();

	@Bean(name = "error")
	@ConditionalOnMissingBean(name = "error")
	public View defaultErrorView() {
		return this.defaultErrorView;
	}

	@Bean
	@ConditionalOnMissingBean
	public BeanNameViewResolver beanNameViewResolver() {
		BeanNameViewResolver resolver = new BeanNameViewResolver();
		resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
		return resolver;
	}
}

  在配置类WhitelabelErrorViewConfiguration 中使用了StaticView视图,该视图就是SpringBoot定义的默认视图,实现逻辑如下:

》》》看着下面那些HTML标签内容是不是有种很熟悉的感觉?这就是开篇出现错误页面的视图。

private static class StaticView implements View {

	private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

	private static final Log logger = LogFactory.getLog(StaticView.class);

	@Override
	public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		if (response.isCommitted()) {
			String message = getMessage(model);
			logger.error(message);
			return;
		}
		response.setContentType(TEXT_HTML_UTF8.toString());
		StringBuilder builder = new StringBuilder();
		Object timestamp = model.get("timestamp");
		Object message = model.get("message");
		Object trace = model.get("trace");
		if (response.getContentType() == null) {
			response.setContentType(getContentType());
		}
		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());
	}

	private String htmlEscape(Object input) {
		return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
	}

	private String getMessage(Map<String, ?> model) {
		Object path = model.get("path");
		String message = "Cannot render error page for request [" + path + "]";
		if (model.get("message") != null) {
			message += " and exception [" + model.get("message") + "]";
		}
		message += " as the response has already been committed.";
		message += " As a result, the response may have the wrong status code.";
		return message;
	}
	@Override
	public String getContentType() {
		return "text/html";
	}
}

  至此,在SpringBoot异常处理过程中需要的实例对象,都已经完成了初始化的过程。

四、触发时机

  在前面已经完成了初始化的过程,那么当发生异常的时候,是如何触发异常的处理呢?其实这里的逻辑和上一篇《基于@ControllerAdvice注解实现全局异常处理用法和原理的探究》中的逻辑类似,我们这里简单梳理一下。

  首先,我们还是从DispatcherServlet的doDispatch()方法开始进行分析,然后无论是否发生异常,都会调用processDispatchResult()方法(区别在于dispatchException变量是否为空),如果dispatchException不为空(发生了异常);
  在processDispatchResult()方法中就会继续调用processHandlerException()方法进行异常处理,在processHandlerException()方法中,会迭代变量handlerExceptionResolvers中的异常解析器,然后会调用其中HandlerExceptionResolverComposite组合类型的解析器的resolveException()方法进行处理;
  在上一篇中执行HandlerExceptionResolverComposite实例的resolveException()方法的时候,因为我们通过@ControllerAdvice注解定义了全局的异常处理器类,所以调用的是ExceptionHandlerExceptionResolver实例对象的resolveException()方法进行处理,如果我们没有自定义异常解析器类,这个时候就会调用DefaultHandlerExceptionResolver实例对象的resolveException()方法,而resolveException()方法是定义在抽象类AbstractHandlerExceptionResolver中的方法,而ExceptionHandlerExceptionResolver和DefaultHandlerExceptionResolver都继承了该抽象类,所以执行的resolveException()方法还是抽象类中的方法,而在resolveException()方法中调用的doResolveException()方法就不一样了,这里个方法是在子类中实现的,所以这里将会调用DefaultHandlerExceptionResolver类中的实现方法,逻辑如下:

@Override
@Nullable
protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

	try {
		if (ex instanceof HttpRequestMethodNotSupportedException) {
			return handleHttpRequestMethodNotSupported(
					(HttpRequestMethodNotSupportedException) ex, request, response, handler);
		}
		// 省略……
		else if (ex instanceof NoHandlerFoundException) {
			return handleNoHandlerFoundException(
					(NoHandlerFoundException) ex, request, response, handler);
		}
		// 省略……
	}
	catch (Exception handlerEx) {
		if (logger.isWarnEnabled()) {
			logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
		}
	}
	return null;
}

  在上述doResolveException()方法中,根据不同类型的异常,调用对应的handlerXXX()方法进行处理异常,并返回ModelAndView对象,我们这里以NoHandlerFoundException异常为例(没有对应处理器,一般会抛出状态为404的异常),分析handleNoHandlerFoundException()方法如何生成对应的视图,实现如下:

protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {

	pageNotFoundLogger.warn(ex.getMessage());
	response.sendError(HttpServletResponse.SC_NOT_FOUND);
	return new ModelAndView();
}

  通过handleNoHandlerFoundException()方法我们知道返回了一个空的ModelAndView对象,即在processHandlerException()方法中,经过HandlerExceptionResolverComposite组合类型的解析器的resolveException()方法进行处理,返回了一个空的ModelAndView对象,但是虽然该对象是空对象,但是并不是空(exMv != null),所以则是就跳出了循环处理过程,并经过后续判断,直接返回了null。

//DispatcherServlet.java
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
		@Nullable Object handler, Exception ex) throws Exception {

	// Success and error responses may use different content types
	request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

	// HandlerExceptionResolverComposite组合类型的解析器的resolveException()方法进行处理
	ModelAndView exMv = null;
	if (this.handlerExceptionResolvers != null) {
		for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
			exMv = resolver.resolveException(request, response, handler, ex);
			if (exMv != null) {
				break;
			}
		}
	}
	//
	if (exMv != null) {
		//当发生NoHandlerFoundException异常时,exMV != null 但是exMv.isEmpty()==true,所以该方法直接返回了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;
}

  经过上述方法处理后,又回到了processDispatchResult()方法,进行后续处理,又回到了doDispatch()方法继续处理,最后到了processRequest()方法,重新出发了一次新的request请求,如下所示:

//FrameworkServlet.java
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

	//省略……

	try {
		doService(request, response);
	}
	catch (ServletException | IOException ex) {
		failureCause = ex;
		throw ex;
	}
	catch (Throwable ex) {
		failureCause = ex;
		throw new NestedServletException("Request processing failed", ex);
	}

	finally {
		resetContextHolders(request, previousLocaleContext, previousAttributes);
		if (requestAttributes != null) {
			requestAttributes.requestCompleted();
		}
		logResult(request, response, failureCause, asyncManager);
		//触发ServletRequestHandledEvent事件,重新开始一次新的请求
		publishRequestHandledEvent(request, response, startTime, failureCause);
	}
}

  这次新的请求,因为是没有对应的处理器,所以触发的ServletRequestHandledEvent事件,statusCode状态码为404,内容如下:
在这里插入图片描述
  因为statusCode状态码为404,经过Servlet容器转发,这次请求就会请求”/error”错误页面,SpringMVC根据该url就会找到对应的处理器BasicErrorController,进行处理,实现如下:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}
}

  其实,到这个方法,就说明我们已经找到了SpringBoot默认的处理错误的处理器BasicErrorController,这个就是我们非常熟悉的用法了。不过细心一看,好像前面提到的StaticView 视图没有提到了,其实这就是SpringMVC的处理机制了,我们再深入了解一下。

  其实,新的请求还是通过DispatcherServlet的doDispatch()进行处理,然后进入
processDispatchResult()方法继续处理,这次exception 就等于null了,所以就不会再走异常处理逻辑了,直接进行视图处理逻辑,即执行render()方法。

//DispatcherServlet.java
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
		@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
		@Nullable Exception exception) throws Exception {
	//下面这部分就是第一次请求时,处理异常的逻辑,但是因为返回的mv是空的,所以触发了第二次请求,这个时候exception 就等于null了,所以就不会再走异常处理逻辑了
	boolean errorView = false;
	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()) {
		render(mv, request, response);
		if (errorView) {
			WebUtils.clearErrorRequestAttributes(request);
		}
	}
	else {
		if (logger.isTraceEnabled()) {
			logger.trace("No view rendering, null ModelAndView returned.");
		}
	}

	if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
		// Concurrent handling started during a forward
		return;
	}

	if (mappedHandler != null) {
		// Exception (if any) is already handled..
		mappedHandler.triggerAfterCompletion(request, response, null);
	}
}

  render()方法处理视图,其中又通过resolveViewName()方法进行视图解析,逻辑如下:

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {

	//省略……
	
	View view;
	String viewName = mv.getViewName();
	if (viewName != null) {
		// We need to resolve the view name.
		view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
		if (view == null) {
			throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
					"' in servlet with name '" + getServletName() + "'");
		}
	}
	
	//省略……
	
}

  在resolveViewName()方法中,根据viewName实现了视图的解析,最终得到了对应的View实例,这里其实就是前面提到的StaticView,实现如下:
在这里插入图片描述
  自此,我们就把SpringBoot默认的异常处理机制中,默认错误页面是如何使用的分析完成了。后续我们将继续深入学习关于异常处理的其他内容。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68731.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!