在 SpringBoot 中,所有的默认配置都在这个包下面
默认跳转的静态错误页面地址
SpringBoot 会在服务器发生错误的时候,去静态资源路径下面的 error 文件夹来找对应的错误页面。比如发生了 404 错误,会去静态资源路径下面的 error 文件夹下找404.html,如果发生500错误,会去找500.html页面。SpringBoot 在没有找到对应错误码的页面时,回去找错误码第一个数字加上xx.html 页面,比如 4xx.html、5xx.html。
源码讲解
SpringBoot 的静态资源路径
SpringBoot 定义了4个静态资源路径,它们分别在:
-
classpath:/META-INF/resources/
-
classpath:/resources/
-
classpath:/static/
-
classpath:/public/
这四个地方.这是在哪里定义的呢? 可以看一看 SpringBoot 如何定义静态资源的( 这部分可以跳过,知道位置就好)
打开Spring Boot配置 web 模块的自动配置类:WebMvcAutoConfiguration
。找到静态内部类 WebMvcAutoConfigurationAdapter
。可以看到,这个类是实现了WebMvcConfigurer
接口的子类。在我的上一篇文章(点这–>Java配置Spring MVC<–点这)中也提到了,这个接口是用来在纯注解的情况下配置 Spring MVC 的。
在这个内部类中可以找到这个用来配置静态资源的方法:
public void addResourceHandlers(ResourceHandlerRegistry registry)
这个方法是用来替代 dispatcher.xml 中这个标签的:< mvc :resources location=”” mapping=”“/ >
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// 这些是用来配置 webjars 的,就是用 maven 来引入jQuery.js、Bootstrap.js 等资源的
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// <mvc :resources /> 标签的 mapping 属性的值
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))// 这是配置静态资源的路径
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
点进this.resourceProperties.getStaticLocations()
方法中,可以看到返回值 就是:org.springframework.boot.autoconfigure.web.ResourceProperties#CLASSPATH_RESOURCE_LOCATIONS
属性的值 :
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
异常页面的路径
现在我们已经知道了静态资源路径在哪,以及在哪定义的了。那如何确认异常页面的路径是在静态资源下的 /error/ 文件夹下的呢?来,进一步的深入源码。
配置 Web 模块有专门的配置类,同样的,配置异常处理也有专门的配置类,它就是 :ErrorMvcAutoConfiguration
。
找到ErrorMvcAutoConfiguration
类,关于错误页面跳转及错误页面信息,我们先看这个 Bean。
@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的名字可以看出,这是一个处理错误信息的Controller。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
可以看到类上面有个非常熟悉的注解:@RequestMapping
只不过和我们平时写的不太一样,我们平时只是写一个地址,这里却用了 ${}来取值。${server.error.path:${error.path:/error}}
这段代码的意思是:如果配置文件中有 server.error.path 这个属性,那么 @RequestMapping
的值就是这个属性的值,如果没有,就用 error.path 的值,如果 error.path 还没有,那就默认处理 /error 请求。
该类中有一个方法,它就是:
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
看方法名就知道它是处理异常页面的,我们打个断点试确认下
创建一个接口,在其中抛出一个异常
// 测试异常接口
@GetMapping("/t")
public void error1(){
throw new NullPointerException("测试异常");
}
启动项目,假设端口是 8080。在浏览器中访问 localhost:8080/t 即可进行断点。证明我们的猜想是正确的。现在来好好看看这个方法。
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
// 这是返回的是 ModelAndView 中的 model 数据,后面会对这里做讲解。
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 返回个 ModelAndView ,这其中包含视图对象,要着重关注这里
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
在 ModelAndView modelAndView = resolveErrorView(request, response, status, model);
这一行打个断点,看下返回的 modelAndView 里面有什么?
可以看到 视图对象已经指定到了 error/5xx 了。step into 到 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;
}
遍历所有的 ErrorViewResolver
,并调用其resolveErrorView
方法。在这个方法中也打上断点,可以发现this.errorViewResolvers
只有一个成员,就是在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration.DefaultErrorViewResolverConfiguration
中注入的,是ErrorViewResolver
的实现类DefaultErrorViewResolver
注入组件不是重点,就不给这的代码复制了。
再次进入 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;
}
可以看到流程:
- 先调用本类的
private ModelAndView resolve(String viewName, Map<String, Object> model)
方法,将请求状态码作为视图名传入 resolve 方法中,返回 ModelAndView。
- 如果返回 ModelAndView 为空并且SERIES_VIEWS(不知道是个什么东西,先不管它)中存储了有关这个错误的信息,则用另一种方法调用resolve方法。这里的 viewName 就是错误码第一个数字+xx
进入 resolve(String viewName, Map<String, Object> model)
方法进行调试。
private ModelAndView resolve(String viewName, Map<String, Object> model) {
// 就是在这里规定异常资源文件夹是在 error 文件夹下的
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
// 默认是在静态资源路径下找 error 文件夹,如果模板引擎也能处理,那就交给模板引擎处理。
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
// 模板引擎处理不了错误资源,就接着在静态资源路径下进行处理
return resolveResource(errorViewName, model);
}
在模板引擎无法处理错误资源的情况下,Spring Boot 调用resolveResource
方法去静态资源路径下找错误资源,该方法定义如下。
/*
*@param (viewName) : 调用该方法时传入的视图名,也就是 4xx或5xx或者错误码
*@param (model) : 也就是带到页面上的数据
*/
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 遍历所有静态资源位置
for (String location : this.resourceProperties.getStaticLocations()) {
try {
// 根据静态资源位置生成 Resource 对象,该对象代表静态资源路径的 Resource
Resource resource = this.applicationContext.getResource(location);
// viewName + ".html" 就是 错误码.html 页面,这里是创建静态资源位置加上文件名的 Resource
resource = resource.createRelative(viewName + ".html");
// 判断这个资源是否存在,如果存在就返回个记录了该页面资源的 ModelAndView。
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
源码流程总结
代码看完了,现在总结下默认的异常处理流程
- 服务器抛出异常,跳转到 /error 接口。也就是 BaseController
- 遍历所有 ErrorViewResolver,看哪个能处理错误请求
- 先根据错误码找到对应的视图
a. 将视图名拼成 “error/错误码”
b. 看模板引擎能否对该视图处理,如果能直接返回视图模型
c. 如果模板引擎处理不了,则先将viewName 后面再拼上".html"并在静态资源下找错误视图。 - 如果无法根据错误码找到对应的视图,则用错误码的第一个数字加上 xx 来查找对应的视图,流程和上面一样。
自定义异常视图
既然知道了 Spring Boot 会如何查找异常视图,那么扩展起来也就随心应手了。扩展异常视图有两周手段
-
直接在静态资源路径下创建 error 文件夹,放入错误码对应的静态页面
-
在模板引擎路径下创建按 error 文件夹,放入错误码对应的静态页面。在模板引擎中可以打印一些错误信息。
第一种非常简单,但是能够展示的信息过少,不做过多介绍,在静态资源路径下的/error 文件夹下创建状态码对应的html 文件即可。
第二种可以展示默认会展示如下信息:
-
timestamp 时间戳
-
status 错误码
-
error 错误信息类型描述
-
exception 异常全限定类名
-
message 就是 Throwable 接口的 getMessage()方法的返回值
-
path 就是哪个URI 报错了
以 Thymeleaf 模板引擎为例,默认 Thymeleaf 的模板是放在类路径下的 templates 路径下,那么就可以在 templates 路径下创建一个 error 文件夹,放入 4xx.html 和 5xx.html 用来测试。
先在浏览器地址栏中输入一个不存在的地址,会发现页面跳到了 4xx.html
再输入在上面定义的会抛出异常的接口,会跳到 5xx.html 。证明测试成功!
展示错误信息
4开头的错误码都是浏览器端造成的,不是地址错了就是请求参数有误,一般我们不用过于关心。我们主要是关注5开头的错误码,这才是我们服务器的问题。上面说过,在跳转至异常页面时,默认会携带6个信息,如果我们使用模板引擎处理异常视图,我们是可以直接把这些错误信息打印出来的。下面看一下 5xx 页面中的html代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>服务器异常页面</title>
</head>
<body>
<h2>哎呦~服务器崩溃了</h2>
<h2>timestamp: [[${timestamp}]]</h2>
<h2>URI: [[${path}]]</h2>
<h2>status: [[${status}]]</h2>
<h2 th:if="not ${#strings.isEmpty(exception)}">Exception:[[ ${exception}]]</h2>
<h2 th:if="not ${#strings.isEmpty(message)}">Exception Message:[[ ${message}]]</h2>
<h2 th:if="not ${#strings.isEmpty(trace)}">Exception trace:</h2>
<p th:if="not ${#strings.isEmpty(trace)}">[[ ${trace}]]</p>
</body>
</html>
这里先介绍下上面用到的 Thymeleaf 模板引擎语法,如果你很熟悉,可以跳过。
- [[]] 一个中括号内嵌一个中括号,是 Thymeleaf 的行内写法,在里面可以写 Thymeleaf 表达式而不会被 html 直接打印出来
- ${} 美元符 + 大括号可以取出 ModelAndView 中的 model
- th:if 用来做条件筛选,表达式返回 true 该行元素才会被渲染出来
- #strings # 号开头代表调用 Thymeleaf 内置对象,这是处理字符串的工具类
看一下页面效果
如果大家把我的 html 代码复制过去(前提是你用的是 Thymeleaf 模板引擎,否则会报错或者把thymeleaf的取值符号打印出来),会发现只能打印时间戳、URI和错误码。这是为什么呢?
在讲解原理之前,我们要清楚一件事:我们的项目最终都会发布到生产环境上的,在平时的开发环境我们可以把方法调用栈、异常信息打印到页面上方便定位异常产生的位置。但是这些信息绝对不能暴漏在开发环境,尤其是方法调用栈,相当于暴漏了项目结构,非常危险!所以,Spring Boot 在默认的情况下,只携带一些不重要的参数到前端的。
理解这个概念之后,我们来查看一下如何把所有错误信息都展示出来。
源码调试
再次来到 BasicErrorController 的 errorHtml 方法中,打断点在这一行
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
// 主要关注这个 getErrorAttributes 方法,在这里打上断点。
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
一层一层的调试,先看看 getErrorAttributeOptions
返回个什么东西
啥也没有,那就不管了,再进入 getErrorAttributes
方法看看用getErrorAttributeOptions
的返回值做了些什么操作。
进入getErrorAttributeOptions
方法后,先将 HttpServletRequest 包装成了 WebRequest
,然后调了ErrorAttributes
的getErrorAttributes
方法。直接进入到ErrorAttributes
的getErrorAttributes
方法中,跳过ErrorController中的getErrorAttributes
方法。(只做了包装对象和调用其他方法的操作,没必要复制过来浪费篇幅)
进入DefaultErrorAttributes
类中,它是ErrorAttributes
接口的默认实现类,查看getErrorAttributes
方法
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
if (this.includeException != null) {
options = options.including(Include.EXCEPTION);
}
if (!options.isIncluded(Include.EXCEPTION)) {
errorAttributes.remove("exception");
}
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
errorAttributes.put("message", "");
}
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
return errorAttributes;
}
可以看到第二个ErrorAttributeOptions
类型的参数是用来确认显示哪些异常信息的,在该方法的第一行会把6个异常信息全部取出来,在后面的 if 判断中,如果不包含这些信息就给删除掉或者清空。
如果想保留一些信息,可以创建一个继承自DefaultErrorAttributes
的子类,然后重写getErrorAttributes
方法,这样我们就可以操控要打印哪些异常信息了。
@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
private ErrorAttributeOptions.Include[] includes;
{
// 将想要现实的信息添加到数组中
includes = new ErrorAttributeOptions.Include[]{
ErrorAttributeOptions.Include.STACK_TRACE,
ErrorAttributeOptions.Include.EXCEPTION,
ErrorAttributeOptions.Include.MESSAGE,
};
}
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
// 调用ErrorAttributeOptions的includeing 方法把我们要展示的信息包含进去
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options.including(includes));
return errorAttributes;
}
}
这样就可以展示出所有的异常信息了。但是,这么做有个缺点,在任何环境中都是展示这几个信息,没办法根据环境切换。还得换个写法。
在 项目中添加一个配置类,注入一个 ErrorAttributeOptions.Include
类型的数组,再利用 @Profile 属性进行动态切换
@Configuration
public class Beans {
@Bean("includes")
@Profile("dev") // 开发环境显示方法调用栈、异常类名和异常信息
public ErrorAttributeOptions.Include[] exceptionDetailIncludesDev() {
return new ErrorAttributeOptions.Include[]{
ErrorAttributeOptions.Include.STACK_TRACE,
ErrorAttributeOptions.Include.EXCEPTION,
ErrorAttributeOptions.Include.MESSAGE,
};
}
@Bean("includes")
@Profile("prod") // 生产环境只显示错误信息
public ErrorAttributeOptions.Include[] exceptionDetailIncludesProd() {
return new ErrorAttributeOptions.Include[]{
ErrorAttributeOptions.Include.MESSAGE,
};
}
}
回到我们重写的DefaultErrorAttributes
,这个ErrorAttributeOptions.Include[]
数组不能写死了,得动态注入。
@Component
// 这里比较关键,Spring 在 IOC容器初始化的时候有可能先加载这个Bean 再加载上面定义的
// 配置类 Beans ,这就会造成 includes 注入失败。所以要指明在 Beans加载之后再加载这个类
@DependsOn("beans")
public class MyErrorAttribute extends DefaultErrorAttributes {
@Autowired
private ErrorAttributeOptions.Include[] includes;
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options.including(includes));
return errorAttributes;
}
}
在 SpringBoot 配置文件 application.yml 中,指定当前环境为开发环境( dev )
spring:
profiles:
active: dev
访问抛出异常的接口,就可以看到所有的错误信息啦!!!
再将环境调整为生产环境(prod)
就只能看见三个错误信息。
结束语
上一次写博客是 6月11日,10天过去了,学了一个新知识,消化了之后就又来写博客了。之前也是知道 SpringBoot 会在静态资源下的找错误页面,但是并不了解流程。这次跟进源码调试之后才理解了具体的流程。不得不感叹到 SpringBoot 的强大,自动配置基本满足开发需求,即使需要扩展,也只需要写少量代码即可做到。配置简介,可扩展性高,真的是Java界的典范