Spring Boot深度课程系列
峰哥说技术—2020庚子年重磅推出、战胜病毒、我们在行动
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()方法,在这里,首先以异常响应码作为视图名分别去查找动态页面和静态页面,如果没有查找到,则再以 4xx 或者 5xx 作为视图名再去分别查找动态或者静态页面。然后观察第2处红色字体部分。我们发现有一段静态代码块,代码如下:
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。