Day41——错误处理原理&定制错误页面

文章目录

  • 一. 回顾
  • 二. 错误处理
    • 2.1 什么是错误处理?
    • 2.2 错误处理原理
      • 2.2.1 错误处理的核心组件
      • 2.2.2 ErrorPageCustomizer
      • 2.2.3 BasicErrorController
      • 2.2.4 DefaultErrorViewResolver
      • 2.2.5 DefaultErrorAttributes
      • 2.2.6 SpringBoot默认的错误提示
      • 2.2.7 四个核心组件之间的关系
  • 三. 定制错误页面

一. 回顾

前面完成了Day40——员工删除——删除完成,今天学习错误处理原理以及定制错误页面

二. 错误处理

2.1 什么是错误处理?

有时候点击添加按钮,跳转页面失败(显示报错找不到路径)。或者输入日期信息,点击提交按钮,发生4xx状态报错。然后会跳转到下图这个错误页面(这是SpringBoot默认的错误处理页面),这个就是错误处理。如下:
Day41——错误处理原理&定制错误页面_第1张图片

2.2 错误处理原理

SpringBoot默认配置的错误处理都是由ErrorMvcAutoConfiguration类配置的。之前说过一切的xxxAutoConfiguration都是SpringBoot的自动配置类,配置的值是从xxxProperties类中获取的。

2.2.1 错误处理的核心组件

核心组件都是在ErrorMvcAutoConfiguration配置的。有如下组件:

  • DefaultErrorAttributes
  • BasicErrorController
  • ErrorPageCustomizer
  • DefaultErrorViewResolver

总结:SpringBoot自动配置错误处理是由ErrorMvcAutoConfiguration决定的。其中有4个核心组件起着重要作用,分别是:DefaultErrorAttributes、DefaultErrorViewResolver、BasicErrorController、ErrorPageCustomizer

后面会对这四个组件详细讲解

2.2.2 ErrorPageCustomizer

一旦系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(ErrorPageCustomizer是定制错误的响应规则)

ErrorMvcAutoConfiguration里面是这样注册ErrorPageCustomizer的,如下:

@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
	return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

点进去看ErrorPageCustomizer的内容是什么,如下:

/**
	 * {@link WebServerFactoryCustomizer} that configures the server's error pages.
	 */
	private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

		private final ServerProperties properties;

		private final DispatcherServletPath dispatcherServletPath;

		protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
			this.properties = properties;
			this.dispatcherServletPath = dispatcherServletPath;
		}

		@Override
		public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
			ErrorPage errorPage = new ErrorPage(
					this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
			errorPageRegistry.addErrorPages(errorPage);
		}

		@Override
		public int getOrder() {
			return 0;
		}

	}

并且发现ErrorPageCustomizer类是ErrorMvcAutoConfiguration的静态内部类。

可以看到它有一个重载方法registerErrorPages(),如下:

@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
	ErrorPage errorPage = new ErrorPage(
         this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
	errorPageRegistry.addErrorPages(errorPage);
}

发生错误以后,系统会去哪个路径呢?这个是由registerErrorPages()中的getPath()决定的,getPath()方法如下:

public String getPath() {
	return this.path;
}

再点击path,查看它是怎么定义的,如下:

/**
	 * Path of the error controller.
	 */
	@Value("${error.path:/error}")
	private String path = "/error";

path是从配置文件中的error.path属性获取值,缺省值为/error

从上面path变量的定义可以得出,系统发生4xx/5xx错误后,会来到/error请求(也就是去到@RequestMapping(value="/error")的controller中进行处理),请求处理此错误。

2.2.3 BasicErrorController

上面谈到,系统一发生4xx/5xx就会默认去/error的controller中处理请求。

SpringBoot自动配置类ErrorMvcAutoConfiguration中的BasicErrorController组件,如下:

@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
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

可以看到它是一个controller,而且处理的请求是配置文件中server.error.path属性值,缺省值是error.path属性值,error.path的缺省值是/error。从ErrorPageCustomizer类中,我们学习到系统一发生4xx/5xx就会默认去/error的controller中处理请求。也就是默认会去BasicErrorController请求处理。

再看到BasicErrorController有2个处理请求的核心方法,分别是 errorHtml()、error(),如下:

@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
	response.setStatus(status.value());
	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
	return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@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, isIncludeStackTrace(request, MediaType.ALL));
	return new ResponseEntity<>(body, status);
}

  • 第一个方法errorHtml()返回值为ModelAndView,而且该方法会产生html类型的数据。这个方法就是在浏览器中展示错误页面的处理方法。比如浏览器访问某个地址,发生4xx错误,那么controller方法响应给它的是一个浏览器能显示的视图。

  • 第二个方法返回值为ResponseEntity>,它返回的是json数据。这个方法就是在某些客户端展示错误的数据 的 方法。比如Postman访问某个地址,发生了4xx错误,那么controller方法响应给它的是一组json数据


是根据什么来区分到底用errorHtml()来处理/error还是用error()处理呢?
:是根据请求头Request Headers的Accept属性来区分的。

解释
在浏览器出现错误后,页面跳转到SpringBoot默认的错误页面,按F12,选择network可以看到如下:
Day41——错误处理原理&定制错误页面_第2张图片
因此浏览器发送的/error请求,BasicErrorController会使用errorHtml()来处理,因为errorHtml()是会产生html类型的数据。

在其他客户端出现错误后,发送的/error请求中,会有如下:
Day41——错误处理原理&定制错误页面_第3张图片
所以除了浏览器以外发送的请求,都会由BasicErrorController的error()方法处理。


总结1:浏览器发送的请求,会来到BasicErrorController的errorHtml()方法处理;其他客户端则来到BasicErrorController的error()方法处理。

总结2:一旦系统发生4xx/5xx之类的错误,ErrorPageCustomizer就会生效,就会发送/error请求,来到BasicErrorController的errorHtml()或error()进行处理。


现在分析是浏览器发送请求,所以详细看errorHtml()的内容,如下:

@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
	response.setStatus(status.value());
	ModelAndView modelAndView = resolveErrorView(request, response, status, model);
	return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

从倒数第二行看到,最终返回的是modelAndView或者new ModelAndView(x, model),而modelAndView是由resolveErrorView(x,x,x, model)(注意,这里无论是由resolveErrorView()得到的modelAndView,还是new ModelAndView(),他们都使用了model这个数据。后面需要知道这个知识点


先了解resolveErrorView(),它的作用是去哪个页面作为错误页面:包含页面地址以及页面内容。

resolveErrorView()方法如下:

protected ModelAndView resolveErrorView(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        HttpStatus status, Map<String, Object> model) {
	for (ErrorViewResolver resolver : this.errorViewResolvers) {
		ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
		if (modelAndView != null) {
			return modelAndView;
		}
	}
	return null;
	}

resolveErrorView()方法是拿到所有的errorViewResolvers,其中一个就是DefaultErrorViewResolver(后面会详细讲此组件)。这里的遍历使用了设计模式中的策略模式。拿到xxxErrorViewResolver后,就会执行resolverErrorView(),并将值赋给modelAndView。如果不为空,则把这个modelAndView返回出去,否则继续遍历。遍历完后,modelAndView仍为空,则返回null。所以结合上面errorHtml()方法返回值,当modelAndView为空的时候,它会new一个ModelAndView。


总结:errorHtml()方法里面是会调用到resolveErrorView()方法,而resolveErrorView()方法里面又会拿到所有的xxxErrorViewResolver,其中一个ErrorViewResolver就是DefaultErrorViewResolver。拿到这个DefaultErrorViewResolver的实例后,又会调用它的resolveErrorView()方法。 因此后面讲解DefaultErrorViewResolver

2.2.4 DefaultErrorViewResolver

上面提到errorHtml()中最终会调用到DefaultErrorViewResolver的resolveErrorView()方法。

首先我们从根源找DefaultErrorViewResolver,在ErrorMvcAutoConfiguration中有如下:

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

点击DefaultErrorViewResolver,看看内容是什么,如下:(代码中的注解就是解释,注意看

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		views.put(Series.CLIENT_ERROR, "4xx");//所有错误页面都是用各自的状态码命名,太麻烦了
		views.put(Series.SERVER_ERROR, "5xx");//这里使用4xx,5xx命名
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}

可以看到它会处理4xx以及5xx的错误。

再来看看DefaultErrorViewResolver的 resolveErrorView() 方法,如下:

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
                                      Map<String, Object> model) {
	ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
	if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
		modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
	}
	return modelAndView;
}

从代码可知,最终返回的是modelAndView,它是由 resolve() 方法得来的。

再来看看rsolve()方法,如下:(代码中的注解就是解释,注意看

private ModelAndView resolve(String viewName, Map<String, Object> model) {
    //SpringBoot默认可以找到一个error/404之类的页面。
	String errorViewName = "error/" + viewName;
	//模板引擎可以解析这个页面就用模板引擎解析
	TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
			this.applicationContext);
	if (provider != null) {
	    //模板引擎可用的情况下,返回 error/viewName指定的视图地址
		return new ModelAndView(errorViewName, model);
	}
	//模板引擎不可用,就在静态资源文件夹下找error/ViewName对应的页面,比如error/404
	return resolveResource(errorViewName, model);
}

第二行代码可以看出,SpringBoot默认可以去找到一个页面,路径是error/viewName,而viewName是由入参得来的,而resolveErrorView传递进的入参是一个枚举类型,里面有很多状态码(比如1xx,2xx,3xx,4xx,5xx)。也就是SpringBoot可以找到一个error/404之类的页面。

最后一行调用了resolveResource(),如下:(代码中的注解就是解释,注意看

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    //获取所有的存放静态资源的地址
	for (String location : this.resourceProperties.getStaticLocations()) {
		try {
			Resource resource = this.applicationContext.getResource(location);
			//创建错误处理的响应页面的名称,命名格式为xxx.html,比如为4xxx.html 或者 5xxx.html等等
			resource = resource.createRelative(viewName + ".html");
			if (resource.exists()) {
			    //返回ModelAndView
				return new ModelAndView(new HtmlResourceView(resource), model);
			}
		}
		catch (Exception ex) {
		}
	}
	return null;
}

总结:
从上面分析errorHtml()处理请求是怎么样的,我们可以自己定制错误页面。

  • 有模模板引擎的情况下:使用error/状态码。将错误页面命名为错误状态码.html,放在模板引擎文件夹里面的error文件夹下,发生此状态的错误就会来到对应的页面。由于错误状态码有太多太多,因此我们可以使用4xx/5xx匹配这种类型的所有错误,精确优先(优先寻找 精确的状态码.html)
  • 没有模板引擎的情况下:会去静态资源文件夹下的error文件夹寻找

2.2.5 DefaultErrorAttributes

了解完SpringBoot是怎么寻找错误页面后,我们继续了解错误页面上的数据内容是从哪里获取的,以及可以获取什么数据

错误页面上的数据肯定在modelAndView中,所以我们从controller中找起,前面我们了解过BasicErrorController,因此到BasicErrorController的errorHtml()找, 如下:(注意代码中的注释

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
		HttpStatus status = getStatus(request);
		//2.model是由getErrorAttributes()获取的
		Map<String, Object> model = Collections
				.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
		//1. 这里model被传入modelAndView
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

从上面看到model被传入modelAndView,model是由getErrorAttributes()获取的。点进去看看getErrorAttributes(),如下:

protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
	WebRequest webRequest = new ServletWebRequest(request);
	return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}

点进去看看errorAttributes,如下:

public abstract class AbstractErrorController implements ErrorController {

	private final ErrorAttributes errorAttributes;

点进去看看ErrorAttributes,如下:

public interface ErrorAttributes {
    Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);

    Throwable getError(WebRequest webRequest);
}

ErrorAttributes是一个接口,我们将光标放在ErrorAttributes,按ctrl+alt+b,就会打开到如下:

public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

最终找到DefaultErrorAttributes,这个类在ErrorMvcAutoConfiguration中是有注册的。

DefaultErrorAttribute中最关键的一个属性是includeException,它是一个布尔值而且缺省值是false(所以它会导致某些异常信息不能被存储到某个Map中),通过它来决定是否添加某些异常信息到errorAttributes(这是一个Map,存储异常信息)中。 遇到过的问题有:SpringBoot获取不到${exception}

private final boolean includeException;

    public DefaultErrorAttributes() {
        this(false);
    }

在DefaultErrorAttributes中找到getErrorAttributes(),如下:

public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
	  Map<String, Object> errorAttributes = new LinkedHashMap();
	   errorAttributes.put("timestamp", new Date());
	   this.addStatus(errorAttributes, webRequest);
	   this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
	   this.addPath(errorAttributes, webRequest);
	   return errorAttributes;
	}

使用getErrorAttributes()方法,错误页面最终能获取到信息有如下:

  • timestamp:时间戳
  • status:状态码
  • error:错误提示
  • exception:异常对象
  • message:异常信息
  • eerrors:JSR303数据校验都在这里

总结:错误页面最终能获取到信息有timestamp:时间戳, status:状态码,error:错误提示, exception:异常对象,message:异常信息, eerrors:JSR303数据校验都在这里

2.2.6 SpringBoot默认的错误提示

在ErrorMvcAutoConfiguration中,有WhitelabelErrorViewConfiguration静态内部类,如下:

protected static class WhitelabelErrorViewConfiguration {

		private final StaticView defaultErrorView = new StaticView();//这里存储了默认的错误页面内容
       
		@Bean(name = "error")
		@ConditionalOnMissingBean(name = "error")
		public View defaultErrorView() {
			return this.defaultErrorView;//返回默认错误视图
		}

点击StaticView,可以看到如下:

private static class StaticView implements View {

它也是一个静态内部类,它有一个render()方法。

render()方法有部分代码如下:

builder.append("

Whitelabel Error Page

"
).append( "

This application has no explicit mapping for /error, so you are seeing this as a fallback.

"
) .append("
").append(timestamp).append("
"
) .append("
There was an unexpected error (type=").append(htmlEscape(model.get("error"))) .append(", status=").append(htmlEscape(model.get("status"))).append(").
"
);

上面的内容就是SpringBoot的默认错误页面

2.2.7 四个核心组件之间的关系

四个核心组件,指的是前面介绍的ErrorPageCustomizer、BasicErrorController、DefaultErrorAttributes、DefaultErrorViewResolver。

关系如下:
Day41——错误处理原理&定制错误页面_第4张图片

三. 定制错误页面

整理SpringBoot寻找错误页面的路径,如下:

Day41——错误处理原理&定制错误页面_第5张图片
所以 定制页面的方法 如下:

  1. 可以在template/error/下创建状态码.html文件
  2. 可以在静态资源文件夹/error/下创建状态码.html文件

你可能感兴趣的:(SpringBoot,SpringBoot)