Spring Boot 错误页面解析原理(超级无敌详细)

本文详细解析了Spring Boot错误页面的工作原理,包括DefaultErrorAttributes、BasicErrorController和ErrorPageCustomizer等组件的作用。介绍了在浏览器和非浏览器客户端下不同响应形式的原因,并探讨了如何自定义错误页面,包括模板引擎下的错误视图解析和静态资源的查找顺序。最后总结了错误页面的配置策略和展示信息。

5.SpringBoot 错误页面

1.默认效果

当我们在运行SpringBoot的时候,访问一个不存在的页面,SpringBoot默认为我们返回一个空白页面,如下所示

这个空白页面主要包括 默认错误路径、时间戳、错误提示消息 和错误状态码


但如果我们使用其他的客户端(非浏览器),例如 Postman工具 发送 http://127.0.0.1:8080/noPage 请求时,默认响应客户端的是JSON数据,如下图所示

2.原理

出现上面的两种默认效果的原因是 SpringBoot为我们自动配置了 错误处理自动配置的 控制器ErrorMvcAutoConfiguration

首先,我们ErrorMvcAutoConfiguration 为我们配置了这几个组件

  • DefaultErrorAttributes
  • BasicErrorController
  • ErrorPageCustomizer
  • DefaultErrorViewResolver
	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
	}

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

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

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

	}

ErrorPageCustomizer

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

首先,它会实例化出一个 ErrorPageCustomizer对象,在ErrorPageCustomizer对象中有一个重要的方法registerErrorPages

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

然后就会调用该方法,注册一个错误页面, 通过 getPath()获取到路径,getPath存在默认的值为"/error",如果在配置文件中配值了``server.error.path=/errors那就会从配置文件中取出error.path`的值

	@Value("${error.path:/error}")
	private String path = "/error";

image-20200427145107137

  • 总结
    • ErrorPageCustomizer组件的作用就是 当 用户请求一旦发生错误时,就会获得 path (错误页面的路径),然后将我们转发到 path 路径下。 并将该路径信息发给BasicErrorController 进行处理

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, 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);
	}

它是用来处理 配置文件下的 server.error.path的请求,如果 server.error.path 未配置时,就会处理 配置文件下的 error.path的请求,如果也没配置,就会处理/error

在这个控制器组件下面 存在着两个 @RequestMapping ,都是处理同一个请求路径的 方法。只是这两个方法的返回值不一样

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 产生html类型的数据
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)

    
@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
 TEXT_HTML_VALUE = "text/html

但是竟然这两种方式都是用于处理一个请求的,为什么浏览器获取的数据和postMan获取的会不一样呢?

原因是浏览器发送的请求的请求头包含这串信息

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

也就是说 浏览器优先接收 text/html类型的数据。

而postMan发送请求时 的请求头如下,它没有指明 希望接收的数据类型,所以服务端给它返回一个JSON类型的数据


那重点来了,返回页面时,又是如何实现的呢?

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // TEXT_HTML_VALUE = "text/html
	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);
	}

当用户发送请求,发送错误时

  • 首先,它会通过请求获取响应状态码和一些model数据,,然后返回一个 modelAndViews , 通过第7行,我们可以知道 modelAndViews 是通过调用 resolveErrorView() 方法获得的。modelAndViews 包含了需要返回的地址,页面等信息
  • 响应页面是调用了 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;
}

第2行:遍历所有的 ErrorViewResolver ,调用resolver. resolveErrorView(request, status, model) 如果存在着 一个 modelAndView 就返回.

在之前,Spring Boot 为我们自动注册了一个 DefaultErrorViewResolver 到容器中,也就是说resolveErrorView() 方法将会由 DefaultErrorViewResolver 进行调用。

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");
		SERIES_VIEWS = Collections.unmodifiableMap(views);
	}
    
    @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;
	}

	private ModelAndView resolve(String viewName, Map<String, Object> model) {
		String errorViewName = "error/" + viewName;
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		return resolveResource(errorViewName, model);
	}

首先,我们可以发现 第5-10行 ,为map中添加了一下客户端错误,用"4xx"表示;服务端错误,用"5xx"表示。

然后就会调用 第13-19行的 resolveErrorView()方法,解析错误视图。

因为发生了错误,所以modelAndView 自然就是等于 null 了,那么就会调用第16行的resolve()方法进行解析,并返回一个modelAndView对象。

resolve(SERIES_VIEWS.get(status.series()), model)
private ModelAndView resolve(String viewName, Map<String, Object> model)

调用resolve()方法时,传入了两个参数,分别为视图名称和model数据

第23行:this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext)如果模板引擎 根据这个错误视图名称能找到对应的模板时,就用模板引擎进行解析

如果解析的结果不为空时,即解析成功过,就将模板引擎解析的 modelAndView的结果进行返回,否者就调用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);
				resource = resource.createRelative(viewName + ".html");
				if (resource.exists()) {
					return new ModelAndView(new HtmlResourceView(resource), model);
				}
			}
			catch (Exception ex) {
			}
		}
		return null;
	}

调用resolveResource方法的时候,从第2行,我们可以发现它会在静态资源下找一个资源名为 错误状态码.html的静态资源(如果我们的错误状态码为404,那么它就会 静态资源文件夹下找 404.html

3.错误页面总结:

1)、有模板引擎的情况下;error/状态码; 【将错误页面命名为 错误状态码.html 放在模板引擎文件夹里面的 error文件夹下】,发生此状态码的错误就会来到 对应的页面;

2)我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先(优先寻找精确的状态码.html);

  1. 有模板引擎的情况下,首先会在templates/error/文件下获取 状态码.html文件

  2. 如果状态码.html文件 不存在的情况下,templates/error/文件夹下获取 状态码对应的 **“4xx.html”或"5xx.html"**文件。

    image-20200427161921074

  3. 如果都不存在的话会在一下静态资源文件夹 获取资源, 精确优先(优先寻找精确的状态码.html),否则查早对应的 **“4xx.html”或"5xx.html"**文件

        private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
                "classpath:/META-INF/resources/", "classpath:/resources/",
                "classpath:/static/", "classpath:/public/"};
    
  4. 如果以上都不能获取,则返回默认的 Whitelabel Error Page

  5. 页面能获取的信息;

​ timestamp:时间戳

​ status:状态码

​ error:错误提示

​ exception:异常对象

​ message:异常消息

​ errors:JSR303数据校验的错误都在这里

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值