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.结果如下:

 

spring boot error 告警 spring boot错误处理_html

错误分析,明显的可以看到当除数为零的时候出现了异常,自动映射到/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

 

spring boot error 告警 spring boot错误处理_ide_02

很显然使用的是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

 

spring boot error 告警 spring boot错误处理_html_03

最后得出结论:

发生了500错误-->查找动态 500.html 页面-->查找静态 500.html --> 查找动态 5xx.html-->查找静态 5xx.html。