springboot 对异常处理默认的自动配置都封装在 ErrorMvcAutoConfiguration 这个类中,在项目启动的过程中,会往容器中注入一些默认的组件、如果容器中已经存在了这些组件,那么就不会再注入这些默认的组件到 IOC 容器中.
项目启动的时候,会往 IOC 容器中注入下面这些默认的组件(我们这里挑比较重要的来说)
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
// 将 application.properties 中标签前缀为 server 的配置与 ServerProperties 属性绑定
private final ServerProperties serverProperties;
// 错误视图解析器
private final List<ErrorViewResolver> errorViewResolvers;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
this.serverProperties = serverProperties;
this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
}
@Configuration
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)
// 如果容器中不存在 DefaultErrorViewResolver ,就将该 bean 注入容器中
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
}
}
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties);
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
// 如果容器中不存在 ErrorAttributes 的实现类对象,则注入 DefaultErrorAttributes 这个 bean
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
// 如果容器中不存在 BasicErrorController 的实现类对象,则注入 BasicErrorController 这个 bean
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
@Configuration
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
@Bean(name = "error")
// 如果容器中不存在 beanName 为 error 的 View,那么注入 bean
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
.......
}
首先是 ErrorMvcAutoConfiguration 构造方法,这里面有两个赋值的动作,分别将 serverProperties、errorViewResolversProvider.getIfAvailable() 赋值给了 ErrorMvcAutoConfiguration 的成员变量,我们分别看一下这两个值到底是什么?
public ErrorMvcAutoConfiguration(ServerProperties serverProperties,
ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
this.serverProperties = serverProperties;
this.errorViewResolvers = errorViewResolversProvider.getIfAvailable();
}
serverProperties 这个变量的类型是 ServerProperties,点开 ServerProperties 这个类
// 将 spring 的配置文件 application.properties 中 server 开头的配置和 ServerProperties 类的属性绑定起来
// ignoreUnknownFields=true 的意思是,如果 ServerProperties 中有属性不能匹配到配置文件中的值时,不会抛出异常
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {
private Integer port;
private InetAddress address;
private String contextPath;
...
}
我们可以发现,原来 serverProperties 里面封装的是我们在 application.properties 中配置的以 server 为前缀的标签,当然如果不配置,它们会有默认值
接着看一下 errorViewResolversProvider.getIfAvailable() ,它的类型是 List<ErrorViewResolver> ,点开 ErrorViewResolver ,我们可以发现它是一个接口,并且只有一个实现类(DefaultErrorViewResolver),我们可以看一下是如何获取到 ErrorViewResolver 的
在 ErrorMvcAutoConfiguration 这个类中有一个静态内部类
@Configuration
static class DefaultErrorViewResolverConfiguration {
// 容器对象
private final ApplicationContext applicationContext;
// 静态资源路径对象
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,ResourceProperties resourceProperties) {
// 容器对象
this.applicationContext = applicationContext;
// 获取静态资源路径
this.resourceProperties = resourceProperties;
}
// 将 DefaultErrorViewResolver 注入到容器中 id 为 conventionErrorViewResolver
@Bean
@ConditionalOnBean(DispatcherServlet.class)
// 如果容器中不存在 DefaultErrorViewResolver,那么我们就往容器中注入该 bean
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
// 返回一个 DefaultErrorViewResolver
return new DefaultErrorViewResolver(this.applicationContext,this.resourceProperties);
}
}
我们看一下这个静态内部类的构造方法,这里面有一个 resourceProperties ,它就是 springboot 默认的静态资源信息,可以看出 springboot 默认的静态资源文件夹信息也封装在里面
接着看一下怎么返回 DefaultErrorViewResolver 对象的
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
// 默认的错误视图,如果是客户端错误对应 4xx ,如果是服务器端的错误对应 5xx
private static final Map<Series, String> SERIES_VIEWS;
// 静态代码块,随着类的加载而加载
static {
Map<Series, String> views = new HashMap<Series, String>();
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
// 默认的错误视图解析器优先级最低,如果我们自己定义了 ErrorViewResolver ,如果要生效,需要设置优先级高于 DefaultErrorViewResolver
// 数值越小,优先级越高,负数的优先级高于正数
private int order = Ordered.LOWEST_PRECEDENCE;
public DefaultErrorViewResolver(ApplicationContext applicationContext,ResourceProperties resourceProperties) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
// 获取模板对象,并将它赋值给 DefaultErrorViewResolver 类的成员变量
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}
...
}
返回了 DefaultErrorViewResolver 对象之后,就将该对象注入到了 IOC 容器中
接着来到错误页面定制器中
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
// 调用 ErrorPageCustomizer 的构造方法,参数是 ErrorMvcAutoConfiguration 的成员变量 serverProperties
return new ErrorPageCustomizer(this.serverProperties);
}
// ErrorMvcAutoConfiguration 的静态内部类
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
// 封装的是 application.properties 配置文件中带 server 前缀的属性
private final ServerProperties properties;
protected ErrorPageCustomizer(ServerProperties properties) {
this.properties = properties;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
// 返回一个错误页面
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());
// 注册错误页面
errorPageRegistry.addErrorPages(errorPage);
}
// 优先级
@Override
public int getOrder() {
return 0;
}
}
我们看一下如何返回错误页面的
ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()+ this.properties.getError().getPath());
这里有一个参数,是由两个表达式通过字符串拼接而成的,这个两个表达式是什么意思,作用是什么
this.properties.getServletPrefix(): 对应的是 application.properties 配置文件中的 server.servlet-path 配置(如果 application.properties 中没有配置,则默认值为 /)
public String getServletPrefix() {
// 获取 springboot 配置文件 application.properties 中 server.servlet-path 配置项的值
// 如果没有配置该配置项,默认值是 ""
String result = this.servletPath;
if (result.contains("*")) {
result = result.substring(0, result.indexOf("*"));
}
// 如果配置的是 server.servlet-path=/
if (result.endsWith("/")) {
// 截取掉 / ,最终保留的是 "" ,那么这样的话配置为 / 和不配置是同样的效果
result = result.substring(0, result.length() - 1);
}
return result;
}
通过上面的代码可以得出
如果配置了 server.servlet-path = /xiaomao ,那么访问路径就是 http://ip:port/xiaomao/
如果不配置或 server.servlet-path = / ,那么访问路径就是http://ip:port/
接着看一下另外一个表达式 this.properties.getError().getPath()
public class ErrorProperties {
// 如果 application.properties 配置了 error.path ,那么 path 就使用 error.path 的值
// 如果没有配置 error.path ,那么 path 就使用 /error
@Value("${error.path:/error}")
private String path = "/error";
private IncludeStacktrace includeStacktrace = IncludeStacktrace.NEVER;
public String getPath() {
return this.path;
}
...
}
由于我们没有在 application.properties 中配置 server.servert-path 和 error.path 的值,所以它们都是使用默认值,一个是空字符串,一个是 /error ,最终拼接的就是 "/error"
调用 ErrorPage 的构造方法生产 ErrorPage 对象,这里的 path 就是 /error
public ErrorPage(String path) {
// 设置状态码
this.status = null;
// 设置异常信息
this.exception = null;
// 设置错误页面路径
this.path = path;
}
然后就是保存错误页面到 AbstractConfigurableEmbeddedServletContainer 类中的一个 属性中
// 所有的错误页面都会保存到这个集合中
private Set<ErrorPage> errorPages = new LinkedHashSet<ErrorPage>();
再然后就是注册错误页面,这里注册的这个错误页面是干什么的,有什么用,具体的还不是很明白,例如我配置了 server.servert-path ,然后 error.path 的值不配置,最后的 ErrorPage 就是
/xiaomaomao/error 这个东西到底是谁的路径,还没有搞明白.
errorPageRegistry.addErrorPages(errorPage);
接着注册默认的视图 defaultErrorView ,默认的视图名称是 error
@Configuration
// application.properties 配置文件中不管是配置了、还是没有配置 server.error.whitelabel.enabled 节点,
// 判断条件都成立
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
// ErrorTemplateMissingCondition 的 matches(...) 方法,返回值为 true , 则判断条件成立
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
// 默认的错误视图页面,也就是我们在浏览器看到的 Whitelabel 页面
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
// 往 IOC 容器中注入一个 beanName 为 error 的 bean
@Bean(name = "error")
// 如果 IOC 容器中没有 error 这个 bean ,那么判断条件成立
@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
@ConditionalOnMissingBean(BeanNameViewResolver.class)
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
再接着是注册 DefaultErrorAttributes ,它里面主要是包括 时间戳、状态码、错误信息等内容
注册完了之后就是往容器中注入 BasicErrorController 了
@Bean
// 如果容器中不存在 ErrorController ,判断条件成立
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
上面说完了启动项目的时候注册的一些默认组件,现在就说一下,如果出现了错误之后, Springboot 默认是怎么处理的.
如果浏览器或者其它客户端访问资源的时候出现了错误,就会来到 /error 请求
// 浏览器发起请求时,如果出现错误时,对应的 Controller 处理逻辑
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,HttpServletResponse response) {
// 获取状态码
HttpStatus status = getStatus(request);
// 获取模型数据 model
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
// 响应数据中设置状态码
response.setStatus(status.value());
// 解析视图得到 ModelAndView 对象
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 如果 ModelAndView 不为空,直接返回 ModelAndView 对象,
// 如果为空,那么视图的名称为 error
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
我们先看一下是如何获取 model (模型数据)的
我们可以看出 Collections.unmodifiableMap(...) 方法的参数是通过 getErrorAttributes(...) 方法来获取的
下面我们就仔细看一下 getErrorAttributes(...) 方法到底做了什么
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {
// 创建一个 ServletRequestAttributes 对象,该对象主要是对 request、response、session 等进行了封装
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
// includeStackTrace 的值为 false
return this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace);
}
继续点开 this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace) ,这里的 this.errorAttributes 代表的是 ErrorAttributes 对象,点开 ErrorAttributes ,发现它是个接口
public interface ErrorAttributes {
Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace);
...
}
所以 this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace) 调用的方法实际上是它的实现类的 getErrorAttributes(...) ,而该接口只有一个实现类 DefaultErrorAttributes ,点进去可以看到如下的内容
// DefaultErrorAttributes 类中的方法
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
// 新建一个 Map 集合 errorAttributes , 用来存放 model 数据
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
// 存放时间戳
errorAttributes.put("timestamp", new Date());
// 存放状态码,这里会详细的说一下
// 它会去默认去 Request 域中寻找 javax.servlet.error.status_code 属性对应的值
// 如果找不到就去 Session 域中寻找, Request 域和 Session 域中如果都找不到,返回 null
// 如果返回值为 null ,则往 errorAttributes 这个 Map 集合中添加 status =999 ,error = None
// 如果返回值不为 null ,则往 errorAttributes 中添加 status
// 以及在 HttpStatus 中定义好的 status 对应的 error reason
addStatus(errorAttributes, requestAttributes);
// 存放错误的详细信息 message
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
// 存放访问路径 localhost:8080/abcde ====> /abcde
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
接着就是 resolveErrorView(...) 这个方法了
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
// 遍历循环所有的 ErrorViewResolver
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// 解析错误视图
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
// 如果 ModelAndView 对象为空,返回 null
return null;
}
解析错误视图
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
// 这里的参数是将状态码转成了 String 类型的字符串,另外一个参数是 model
// 1、存在 thymeleaf 模板引擎的情况下,就优先去去找 templates/error/status的值.html 这个视图
// 2、不存在 thymeleaf 模板引擎的情况下就去所有的静态资源文件下寻找 /error/status的值.html 视图
// 如果上面两种情况都找不到,那么 modelAndView 的值为 null
ModelAndView modelAndView = resolve(String.valueOf(status), model);
// modelAndView 的值为 null ,并且如果是 CLIENT_ERROR 或者是 SERVER_ERROR
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 假设这里是客户端错误,那么就按照 4xx 重复上面的步骤重新解析一次
// 如果是服务端错误,那么按照 5xx 重复上面的步骤重新解析一次
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
解析错误视图,返回 ModelAndView 对象
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// error/ 拼接 状态码的字符串就是错误的视图名称
String errorViewName = "error/" + viewName;
// 根据 errorViewName 获取可用的模板引擎
// 例如 thymeleaf 引擎,那么判断当前项目的 templates 下有没有 /error/viewName.html 这个视图
// 如果有 provider 就不为空,如果没有则为 null
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
// 如果有可用的模板引擎
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 如果没有模板引擎,那么去静态资源文件夹下面找 error/status的值.html
return resolveResource(errorViewName, model);
}
如果有 thymeleaf 模板引擎的情况下
// 如果存在模板引擎的情况下
public ModelAndView(String viewName, Map<String, ?> model) {
// ModelAndView.setView("viewName")
this.view = viewName;
// 将 model 中的数据作为 ModelAndView 对象的属性
if (model != null) {
getModelMap().addAllAttributes(model);
}
}
如果没有模板引擎的情况下
// 如果不存在模板引擎的情况下
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 挨个遍历所有的静态文件夹
// /META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/、/
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
// 查看上面所有的 5 个静态资源文件夹中是否存在 /error/status的值.html 这个视图
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
// 所有的静态资源文件夹下都不存在 /error/status的值.html 则返回 null
return null;
}
假设状态码为 404、如果在 templates/error/404.html、静态文件夹下 /error/404.html、templates/error/404.html、静态文件夹下 /error/404.html 这些目录都找不到视图,那么 ModelAndView 对象就为 null,接着就会继续去找templates/error/4xx.html、静态文件夹下 /error/4xx4.html、templates/error/4xx.html、静态文件夹下 /error/4xx.html 如果还找不到的情况下,就会创建一个新的视图,视图名称为 error ,那么这个 error 视图是什么呢?
@Configuration
// 如果 application.properties 中没有配置 server.error.whitelabel.enabled 标签,那么判断条件成立
// 这里要注意一下,如果配置了 server.error.whitelabel.enabled=false,那么判断条件是不成立的
// 要搞清楚 matchIfMissing=true 到底是什么意思(没有配置标签,判断条件才成立)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final SpelView defaultErrorView = new SpelView(
"<html><body><h1>Whitelabel Error Page</h1>"
+ "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
+ "<div id='created'>${timestamp}</div>"
+ "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
+ "<div>${message}</div></body></html>");
// 注册一个名称为 error 的视图对象
@Bean(name = "error")
// 如果容器中不存在一个名称为 error 的视图对象,条件成立
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
...
}
看到没 @Bean 注解注册一个 View 对象,它的名称就是 error,看一下这个视图不就是我们的 Whitelabel 吗.
总结一下上面的源码,这里假设状态码为 404 (客户端错误)
1、项目中引入了 thymeleaf 模板引擎,那么就去寻找 templates/error/404.html 视图,找不到执行步骤 2
2、去静态资源文件(5个静态资源文件夹)下找 /error/404.html 视图,找不到执行步骤 3
3、去找 templates/error/4xx.html 视图(如果是服务端错误,就找 /error/5xx.html),找不到执行步骤 4
4、去找找静态资源文件下的 /error/4xx.html 视图(如果是服务端错误,就是 /error/5xx.html)
5、如果上面的情况都不符合,那么就是找默认的视图 Whitelabel 了