Spring Boot深度课程系列
14 峰哥说技术:Spring Boot异常处理方案源码解析与实践
虽然我们可以@ControllerAdvice注解配置@ExceptionHandler来处理全局异常。但是也可以有自己的方案,在Spring Boot中对异常的处理有一些默认的策略,我们可以通过一个案例来看。
案例:构造一个工程,访问http://localhost:8080/hello.在接口方法如下。
步骤:
1)创建包,编写HelloController.代码如下
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
int i=1/0;
return "hello,Spring boot!";
}
}
|
2)在浏览器中输入:http://localhost:8080/hello.结果如下:
错误分析,明显的可以看到当除数为零的时候出现了异常,自动映射到/error路径,而在程序中没有/error路径。所以到了整个页面。不过在 Spring Boot 中,提供 /error 路径实际上是下下策,Spring Boot 本身在处理异常时,也是当所有条件都不满足时,才会去找 /error 路径。那么我们就先来看看,在 Spring Boot 中,如何自定义 error 页面,整体上来说,可以分为两种,一种是静态页面,另一种是动态页面。那么该怎么使用?相信经过这么多次课程的学习,我们想到的一定是找源码。
其实在Spring Boot中,有一个类ErrorMvcAutoConfiguration的配置类,在该类中我们首先看源码中的静态类DefaultErrorViewResolverConfiguration
@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);
}
}
|
红色字体部分的DefaultErrorViewResolver对象很显然定义的默认的错误视图解析器,那么错误的视图肯定跟这个解析器有关。下面研究这个类。在该类中有一个方法。
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
|
很显然是来解析错误视图的方法,如果去跟踪status.series()方法,在这里,首先以异常响应码作为视图名分别去查找动态页面和静态页面,如果没有查找到,则再以
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);
}
|
很显然,动态错误视图的名称是4xx.html或者5xx.html。然后再跟进第2处红色字体方法。代码如下:
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
|
第1处红色字体部分,很明显说明视图有前缀error,而且是固定格式。第2处红色部分是解析资源的代码。我们继续跟进去。
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
;
}
}
return null;
}
|
我们继续追踪第1处红色字体,很容易就进入了ResourceProperties类中。可以看到它实际就是前面的静态资源的位置。
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
|
那我们答案就呼之欲出了,我们定义的错误页面就是放上面静态资源的的路径下面的error文件夹下面。而且它的扩展名必须是.html.
A)自定义静态异常页面
步骤:
1)在static下面创建error文件夹,添加500.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h4>500错误页</h4>
</body>
</html>
|
2)在templates下面创建error文件夹,添加500.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h4>templates-500错误页</h4>
</body>
</html>
|
3)测试:在浏览器输入http://localhost:8080/hello
很显然使用的是static/error文件夹下面的页面。删除后,会使用templeates/error文件夹下面的页面。大家可以自行测试即可。
B)自定义动态异常页面
步骤:
1)在static下面创建error文件夹,添加5xx.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h4>5xx错误页</h4>
</body>
</html>
|
2)在templates下面创建error文件夹,添加5xx.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h4>templates-5xx错误页</h4>
</body>
</html>
|
3)测试:将static/error文件夹下的500.html改成500-bak.html,在浏览器输入http://localhost:8080/hello
最后得出结论:
发生了500错误-->查找动态 500.html 页面-->查找静态 500.html --> 查找动态 5xx.html-->查找静态 5xx.html。