一、用法简介
前面一篇内容中,我们学习了SpringBoot默认的异常处理机制,但是在实际的工作中,这种方式肯定是无法满足个性化的需求的,如何实现自定义错误页面呢?我们这里将会使用一种最简单的方式来实现,即通过在src/main/resources/templates 目录下创建 error.html 页面实现,具体用法如下:
本篇内容旨在学习该用法背后的实现机制,所以我们先简单演示用法,重点在后面的分析。
首先,因为这里我们使用到了thymeleaf视图组件,所以需要引入该依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
然后,我们创建error.html页面(默认需要使用该名称),内容如下(用户自定义):
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>自定义异常</title>
</head>
<body>
<h1>自定义错误页面!</h1>
<!--SpringBoot默认存储异常信息的key为exception-->
<span th:text="${error}" />
</body>
</html>
经过上述的简单配置,我们自定义的错误页面就生效了,当访问不存在的页面时,就会出现如下界面:
该页面可以根据需求进行丰富,这里仅为学习其中的原理,所以比较简单。
至此,我们就完成了自定义错误页面的配置了,接下来将分析其中的原理。
二、原理分析
经过前面《SpringBoot默认的处理异常机制,默认错误页面是怎么产生的呢?》内容,我们知道解析视图的地方是DispatcherServlet的processDispatchResult()方法中通过调用render(mv, request, response);方法实现的,在render()方法中,又调用了resolveViewName()方法,进行视图解析,我们这里就从resolveViewName()方法开始分析。
首先,我们了解一下resolveViewName()方法的实现,如下所示:
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
在resolveViewName()方法中,通过迭代视图解析器,分别调用视图解析器的resolveViewName()方法进行视图解析,如果可以解析到合适的视图,就会直接返回,即后续的解析器将不会再继续处理。
我们通过debug可以发现,这个时候viewResolvers变量中有如下四个视图解析器:
首先,会进入ContentNegotiatingViewResolver视图解析器的resolveViewName()方法进行处理,实现如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
//省略 ……
}
在resolveViewName()方法中,首先通过调用getCandidateViews()方法获取候选的视图,然后再通过getBestView()方法获取合适的视图。我们这里先分析一下getCandidateViews()方法,实现如下:
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
List<View> candidateViews = new ArrayList<>();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
在getCandidateViews()方法中,通过迭代变量viewResolvers中的视图解析器,并调用resolveViewName()方法,筛选可能的视图,这里可用的视图解析器有如下几个:
这个和项目的配置有关系,我这里为了避免干扰,特意创建了一个最简单的项目进行分析,所以仅有如下几个视图解析器。
通过这几个视图解析,最终找到以下几个可用的视图,并返回到上层方法中,如下图所示:
然后,上述返回的视图,又通过getBestView()方法,找到最佳视图,这里返回了ThymeleafView视图,即我们配置的视图。
至此,我们配置的视图就生效了,通过后续的处理就渲染到了前端页面。
三、默认的StaticView视图跑哪里去了呢?
通过前面的分析,我们好像没有看到StaticView视图出现过,那么为什么配置了自定义错误视图后,StaticView视图就消失了呢?带着疑问,我们把自定义视图的“error.html”页面删除了,然后按照上面的过程再debug了一遍代码,发现如下不同:
首先,在DispatcherServlet对象的resolveViewName()方法中,viewResolvers变量中的视图解析器多了一个BeanNameViewResover实例,如下所示:
然后,在ContentNegotiatingViewResolver视图解析器的getCandidateViews()方法中,viewResolvers变量中同样多了一个BeanNameViewResover实例,如下所示:
因为这个BeanNameViewResover视图解析器,获取到的候选视图,就有了默认的StaticView视图,如下所示,说明BeanNameViewResover视图解析器加载了该默认视图,而当我们进行了开篇的自定义错误视图配置,就没有了BeanNameViewResover视图解析器实例。
至此,我们就需要继续分析为什么BeanNameViewResover视图解析器实例当配置了自定义错误配置视图后就消失了呢?其实,这个需要在SpringBoot初始化的阶段来寻找答案。
在上一篇博文中,我们学习了默认的一些配置实在ErrorMvcAutoConfiguration类中进行配置的,其中内部还有一个配置子类WhitelabelErrorViewConfiguration ,实现逻辑如下:
@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;
}
}
当我们看到beanNameViewResolver()方法上的@ConditionalOnMissingBean注解,这个时候我们就该恍然大悟了,究其根本原因,就是因为该注解,因为已经加载了thymeleafViewResolver解析器,所以beanNameViewResolver解析器就不会再加载了。
当我再次Debug的时候,发现当引入thymeleaf依赖, 但是不创建对应的error页面时,这个时候thymeleaf视图解析器也会被被注入,不过这个时候BeanNameViewResolver也会被注入,因此这应该不是beanNameViewResolver()方法上的@ConditionalOnMissingBean注解生效造成的原因。这时看到类上的@Conditional(ErrorTemplateMissingCondition.class)注解,根据类的命名我们基本上就可以猜到,是因为该注解引起的,当没有对应的错误页面定义时,才会进行WhitelabelErrorViewConfiguration 配置,即注入默认的StaticView 视图和BeanNameViewResolver 解析器。