概述
自动配置ErrorMvcAutoConfiguration
针对基于Spring MVC
的Servlet Web
环境配置了一个MVC error
控制类用于渲染错误信息页面。
该配置类通过注解声明在如下条件满足时生效 :
- 必须是一个
Servlet Web
应用; - 类
Servlet
,DispatcherServlet
必须存在于classpath
;这一声明要求必须是基于
Spring MVC
的Servlet Web
应用;
另外该配置类通过注解声明自己的运行时机为 :
- 在
WebMvcAutoConfiguration
自动配置运行之后;
该配置类的具体运行效果如下 :
- 定义
bean DefaultErrorAttributes errorAttributes
在该
bean
不存在时才定义。
该bean
基于配置属性被创建,然后被bean basicErrorController
使用,用于从处理出错的用户请求中获取错误信息然后渲染到页面。 - 定义
bean BasicErrorController basicErrorController
在该
bean
不存在时才定义。
映射到URL
路径${server.error.path:${error.path:/error}}
用于处理用户请求出错时展示错误页面,内部会使用bean errorAttributes
从遇到错误的用户请求中分析错误信息然后展示到页面。 - 定义
bean ErrorPageCustomizer errorPageCustomizer
- 该
bean
的一个主要特征是实现了接口ErrorPageRegistrar
,因此它会被ErrorPageRegistrarBeanPostProcessor
用于往ErrorPageRegistry
注册错误页面。该bean
所提供的注册页面缺省是一个URL
路径为/error
的ErrorPage
对象。而这里ErrorPageRegistry
时当前Servlet
容器对象的的工厂,在Servlet
容器是Tomcat
的情况下,它其实就是TomcatServletWebServerFactory
。 - 关于
ErrorPageRegistrarBeanPostProcessor
,可以参考ServletWebServerFactoryAutoConfiguration
。
- 该
- 定义
bean PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor
该
bean
通过一个类静态方法定义,是一个BeanFactoryPostProcessor
,它的目的是为ErrorController bean
的定义设置属性preserveTargetClass:true
,从而确保ErrorController bean
如果有代理对象的话代理对象总是代理在类上而不是接口上。 - 内置配置类
DefaultErrorViewResolverConfiguration
- 定义
bean DefaultErrorViewResolver conventionErrorViewResolver
仅在该类型
bean
不存在,并且DispatcherServlet bean
存在的情况下才定义该bean
。
该ViewResolver
会基于一些常见约定解析错误视图,比如尝试根据HTTP
错误状态码解析出错误处理视图。
它会在目录/error
下针对提供的HTTP
错误状态码搜索模板或者静态资源,比如,给定了HTTP
状态码404
,
它会尝试搜索如下模板或者静态资源:
/<templates>/error/404.<ext>
– 这里<templates>
表示所配置的模板所在目录,<ext>
表示所用的模板的文件名
/<static>/error/404.html
– 这里<static>
表示静态资源文件所在路径
/<templates>/error/4xx.<ext>
/<static>/error/4xx.html
- 定义
- 内置配置类
WhitelabelErrorViewConfiguration
这里是著名的
Spring
Whitelable Error
被定义的地方。- 定义
bean View defaultErrorView
,指定bean
名称为error
定义一个
bean
,使用名称error
,这是一个视图bean
,该视图是一个HTML
生成模板,利用一个错误信息Model
生成一个HTML
错误页面。它使用的错误信息Model
其实就是basicErrorController
利用errorAttributes
从遇到错误的用户请求中获取的错误信息。
该bean
仅在不存在名称为error
的bean
时才定义。 - 定义
bean BeanNameViewResolver beanNameViewResolver
该
bean
仅在不存在同类型的bean
时才定义。
这样一个根据bean
名称解析视图的视图解析器bean
,用来确保上面定义的视图bean error
可以被解析到。
- 定义
另外,该自动配置类通过注解@EnableConfigurationProperties
确保存在对应相应配置参数的bean
:
bean 名称 | bean 类型 | 对应配置项前缀 |
---|---|---|
server-org.springframework.boot.autoconfigure.web.ServerProperties | ServerProperties | server |
spring.resources-org.springframework.boot.autoconfigure.web.ResourceProperties | ResourceProperties | spring.resources |
spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties | WebMvcProperties | spring.mvc |
注意 : 以上这些
bean
有可能在其他自动配置类中通过@EnableConfigurationProperties
被定义,根据@EnableConfigurationProperties
的工作逻辑,重复定义的bean
仅会在首次被处理时被注册到容器。具体的工作原理可以参考类EnableConfigurationPropertiesImportSelector
的实现。
源代码
源代码版本 : spring-boot-autoconfigure-2.1.3.RELEASE
package org.springframework.boot.autoconfigure.web.servlet.error;
// 省略 import 行
/**
* EnableAutoConfiguration Auto-configuration to render errors via an MVC error
* controller.
*
*/
@Configuration
// 仅在 Servlet Web 应用环境下生效
@ConditionalOnWebApplication(type = Type.SERVLET)
// 仅在 Servlet,DispatcherServlet 类存在时生效,也就是必须是使用了 Spring MVC 时才生效,
// 如果使用的是其他非 Spring MVC 机制,则不生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
// 在自动配置 WebMvcAutoConfiguration 之前执行
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 确保如下前缀的属性被加载到相应的 bean :
// server.* => ServerProperties
// spring.resources.* => ResourceProperties
// spring.mvc.* => WebMvcProperties
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class,
WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
private final DispatcherServletPath dispatcherServletPath;
private final List<ErrorViewResolver> errorViewResolvers;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
DispatcherServletPath dispatcherServletPath,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
this.serverProperties = serverProperties;
this.dispatcherServletPath = dispatcherServletPath;
this.errorViewResolvers = errorViewResolvers.orderedStream()
.collect(Collectors.toList());
}
// 在 bean ErrorAttributes 不存在的情况下定义bean ErrorAttributes,使用实现类 DefaultErrorAttributes
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
// 是否包含异常信息取决于配置属性 server.error.includeException,
// 如果不存在该配置属性,使用缺省值 false
return new DefaultErrorAttributes(
this.serverProperties.getError().isIncludeException());
}
// 在 bean ErrorController 不存在的情况下定义bean ErrorController, 使用实现类 BasicErrorController
// 映射到URL路径 : ${server.error.path:${error.path:/error}}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
// 定义 bean ErrorPageCustomizer
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
}
// 定义 bean PreserveErrorControllerTargetClassPostProcessor,
// 该 BeanFactoryPostProcessor 确保 ErrorController bean 不会被创建相应的 AOP 代理对象
@Bean
public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
return new PreserveErrorControllerTargetClassPostProcessor();
}
// 内嵌配置类 : 定义缺省的 ViewResolver
@Configuration
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
// 1. 在 bean DispatcherServlet 存在的前提下定义该bean
// 2. 在DefaultErrorViewResolver类型的 bean 未被定义的前提下定义该bean,
// 使用实现类 DefaultErrorViewResolver, 该 ViewResolver 会基于一些常见
// 约定,尝试根据HTTP错误状态码解析出错误处理视图。它会在目录/error下针
// 对提供的HTTP错误状态码搜索模板或者静态资源,比如,给定了HTTP状态码404,
// 它会尝试搜索如下模板或者静态资源:
// /<templates>/error/404.<ext> – 这里<templates>表示所配置的模板所在目录,<ext>表示所用的模板的文件名
// /<static>/error/404.html – 这里<static>表示静态资源文件所在路径
// /<templates>/error/4xx.<ext>
// /<static>/error/4xx.html
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,
this.resourceProperties);
}
}
// 嵌套配置类
// 配置著名的 Spring Whitelabel Error View,
// 在开发人员没有通过其他方式提供 Error View Template 的情况下却省总是配置
// Whitelable Error View,
// 通过配置项 server.error.whitelabel.enabled=false 可以禁用该配置,也就是
// 不使用 Whitelabel Error View 机制
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
// 在名称为 error 的 bean View 不存在的情况下定义该 View bean,名字使用 error,
// 该 View bean 的实现类是 StaticView,定义如下,该 View 实现对应产生一个
// HTML 页面展示错误信息
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
// 如果 bean BeanNameViewResolver 不存在则定义该bean,
// 该 bean是一个通过 bean 名称解析 View 的视图解析器,可以保障上面的
// 名字为 error 的 defaultErrorView 被解析成功
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
/**
* SpringBootCondition that matches when no error template view is detected.
*/
private static class ErrorTemplateMissingCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("ErrorTemplate Missing");
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(
context.getClassLoader());
TemplateAvailabilityProvider provider = providers.getProvider("error",
context.getEnvironment(), context.getClassLoader(),
context.getResourceLoader());
if (provider != null) {
return ConditionOutcome
.noMatch(message.foundExactly("template from " + provider));
}
return ConditionOutcome
.match(message.didNotFind("error template view").atAll());
}
}
/**
* Simple View implementation that writes a default HTML error page.
*/
private static class StaticView implements View {
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;
}
StringBuilder builder = new StringBuilder();
Date timestamp = (Date) 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";
}
}
/**
* 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;
}
// 根据错误有关配置属性构建一个 ErrorPage 对象,注册到 errorPageRegistry,
// errorPageRegistry,这里 ErrorPageRegistry 其实就是 Spring MVC 提供的
// 针对具体 Servlet Web 服务器的工厂类,比如对于 Tomcat Servlet 容器,
// 它其实就是 TomcatServletWebServerFactory
@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;
}
}
/**
* BeanFactoryPostProcessor to ensure that the target class of ErrorController
* MVC beans are preserved when using AOP.
*
* 确保 ErrorController bean 被AOP创建代理对象的话,代理对象总是代理目标类而不是接口,
* 具体做法是对该 bean定义设置属性 :
* org.springframework.aop.framework.autoproxy.AutoProxyUtils.preserveTargetClass = true
*/
static class PreserveErrorControllerTargetClassPostProcessor
implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
String[] errorControllerBeans = beanFactory
.getBeanNamesForType(ErrorController.class, false, false);
for (String errorControllerBean : errorControllerBeans) {
try {
beanFactory.getBeanDefinition(errorControllerBean).setAttribute(
AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
}
catch (Throwable ex) {
// Ignore
}
}
}
}
}