1. 默认异常处理

    在Web 开发中,往往需要一个统一的异常处理机制,来保证客户端能接收较为友好的提示。Spring Boot 同样提供了一套默认的异常处理机制。

    1) Spring Boot 默认异常处理机制

        Spring Boot 提供了一套默认的异常处理机制,一旦程序中出现了异常,Spring Boot 会自动识别客户端的类型(浏览器或客户端APP),并根据客户端的不同,以不同的形式展示异常信息。

        例如,访问一个不存在的页面,结果如下。

        (1) 浏览器,Spring Boot 会响应一个 “whitelabel” 错误视图,以 HTML 格式呈现错误信息
              
       

Whitelabel Error Page
            This application has no explicit mapping for /error, so you are seeing this as a fallback.

            Tue Apr 19 20:01:40 CST 2022
            There was an unexpected error (type=Not Found, status=404).



        (2) 客户端APP,Spring Boot 将生成 JSON 响应,来展示异常消息

          

{
                "timestamp": "2022-04-19T12:03:08.474+00:00",
                "status": 404,
                "error": "Not Found",
                "path": "/test"
            }

    2) Spring Boot 异常处理自动配置原理

        Spring Boot 通过配置类 ErrorMvcAutoConfiguration 对异常处理提供了自动配置,该配置类向容器中注入了以下 4 个组件。

            ErrorPageCustomizer:该组件会在在系统发生异常后,默认将请求转发到 “/error” 上;
            BasicErrorController:处理默认的 “/error” 请求;
            DefaultErrorViewResolver:默认的错误视图解析器,将异常信息解析到相应的错误视图上;
            DefaultErrorAttributes:用于页面上共享异常信息。

        (1) ErrorPageCustomizer

            ErrorMvcAutoConfiguration 向容器中注入了一个名为 ErrorPageCustomizer 的组件,它主要用于定制错误页面的响应规则。

        

@Bean
                public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
                    return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
                }


            ErrorPageCustomizer  通过 registerErrorPages() 方法来注册错误页面的响应规则。当系统中发生异常后,ErrorPageCustomizer  组件会自动生效,并将请求转发到 “/error”上,交给 BasicErrorController 进行处理,其部分代码如下。

             

@Override
                public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
                    // 将请求转发到 /errror(this.properties.getError().getPath())上
                    ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
                    // 注册错误页面
                    errorPageRegistry.addErrorPages(errorPage);
                }


        (2) BasicErrorController

            ErrorMvcAutoConfiguration 还向容器中注入了一个错误控制器组件 BasicErrorController,代码如下。

           

@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()));
                }


            Spring Boot 通过 BasicErrorController 进行统一的错误处理(例如默认的“/error”请求)。Spring Boot 会自动识别发出请求的客户端的类型(浏览器客户端或机器客户端),并根据客户端类型,将请求分别交给 errorHtml() 和 error() 方法进行处理。

方法

描述

ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)

浏览器访问返回 text/html(错误页面)

ResponseEntity<Map<String, Object>> error(HttpServletRequest request)

客户端APP(例如安卓、IOS、Postman 等等)访问返回 JSON 

     
        (3) DefaultErrorViewResolver

            ErrorMvcAutoConfiguration 还向容器中注入了一个默认的错误视图解析器组件 DefaultErrorViewResolver,代码如下。

@Bean
                @ConditionalOnBean(DispatcherServlet.class)
                @ConditionalOnMissingBean(ErrorViewResolver.class)
                DefaultErrorViewResolver conventionErrorViewResolver() {
                    return new DefaultErrorViewResolver(this.applicationContext, this.resources);
                }

            当发出请求的客户端为浏览器时,Spring Boot 会获取容器中所有的 ErrorViewResolver 对象(错误视图解析器),并分别调用它们的 resolveErrorView() 方法对异常信息进行解析,其中自然也包括 DefaultErrorViewResolver(默认错误信息解析器)。

            DefaultErrorViewResolver 解析异常信息的步骤如下:

                a) 根据错误状态码(例如 404、500、400 等),生成一个错误视图 error/status,例如 error/404、error/500、error/400;
                b) 尝试使用模板引擎解析 error/status 视图,即尝试从 classpath 类路径下的 templates 目录下,查找 error/status.html,例如 error/404.html、error/500.html、error/400.html;
                c) 若模板引擎能够解析到 error/status 视图,则将视图和数据封装成 ModelAndView 返回并结束整个解析流程,否则跳转到第 4 步;
                d) 依次从各个静态资源文件夹中查找 error/status.html,若在静态文件夹中找到了该错误页面,则返回并结束整个解析流程,否则跳转到第 5 步;
                e) 将错误状态码(例如 404、500、400 等)转换为 4xx 或 5xx,然后重复前 4 个步骤,若解析成功则返回并结束整个解析流程,否则跳转第 6 步;
                f) 处理默认的 “/error ”请求,使用 Spring Boot 默认的错误页面(Whitelabel Error Page)。

        (4) DefaultErrorAttributes

            ErrorMvcAutoConfiguration 还向容器中注入了一个组件默认错误属性处理工具 DefaultErrorAttributes,代码如下。

        

@Bean
                @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
                public DefaultErrorAttributes errorAttributes() {
                    return new DefaultErrorAttributes();
                }


            DefaultErrorAttributes 是 Spring Boot 的默认错误属性处理工具,它可以从请求中获取异常或错误信息,并将其封装为一个 Map 对象返回。

            在 Spring Boot 默认的 Error 控制器(BasicErrorController)处理错误时,会调用 DefaultErrorAttributes 的 getErrorAttributes() 方法获取错误或异常信息,并封装成 model 数据(Map 对象),返回到页面或 JSON 数据中。该 model 数据主要包含以下属性:

                timestamp:时间戳;
                status:错误状态码
                error:错误的提示
                exception:导致请求处理失败的异常对象
                message:错误/异常消息
                trace: 错误/异常栈信息
                path:错误/异常抛出时所请求的URL路径

                注:所有通过 DefaultErrorAttributes 封装到 model 数据中的属性,都可以直接在页面或 JSON 中获取。          

2. 全局异常处理

    Spring Boot 提供了一套默认的异常处理机制,但是 Spring Boot 提供的默认异常处理机制却并不一定适合我们实际的业务场景,因此,我们通常会根据自身的需要对 Spring Boot 全局异常进行统一定制,例如定制错误页面,定制错误信息等。

    我们可以通过以下 3 种方式定制 Spring Boot 错误页面:

        (1) 自定义 error.html
        (2) 自定义动态错误页面
        (3) 自定义静态错误页面

        注:前两种方式需要在 Spring Boot 整合 Thymeleaf 模板(或其它 Spring Boot 支持的模版)的基础上实现,第三种不需要整合 Thymeleaf 模板也可以实现。

    1) 自定义 error.html

        可以直接在模板文件夹 resources/templates 下创建 error.html ,覆盖 Spring Boot 默认的错误视图页面(Whitelabel Error Page)。

        示例,创建 src/main/resources/templates/error.html 文件

1             <!DOCTYPE html>
 2             <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3             <head>
 4                 <meta charset="UTF-8">
 5                 <title>自定义 error.html</title>
 6             </head>
 7             <body>
 8                 <h1>自定义 error.html</h1>
 9                 <p>status:<span th:text="${status}"></span></p>
10                 <p>error:<span th:text="${error}"></span></p>
11                 <p>timestamp:<span th:text="${timestamp}"></span></p>
12                 <p>message:<span th:text="${message}"></span></p>
13                 <p>path:<span th:text="${path}"></span></p>
14             </body>
15             </html>

    2) 自定义动态错误页面

        如果 Sprng Boot 项目使用了模板,当程序发生异常时,Spring Boot 的默认错误视图解析器(DefaultErrorViewResolver)就会解析模板文件夹 resources/templates 下 error 目录中的错误视图页面。

        (1) 精确匹配

            可以根据错误状态码(例如 404、500、400 等等)的不同,分别创建不同的动态错误页面(例如 404.html、500.html、400.html 等等),并将它们存放在模板引擎文件夹下的 error 目录中。当发生异常时,Spring Boot 会根据其错误状态码精确匹配到对应的错误页面上。

            示例,创建 src/main/resources/templates/error/404.html 文件

1                 <!DOCTYPE html>
 2                 <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3                 <head>
 4                     <meta charset="UTF-8">
 5                     <title>自定义动态错误页面 404.html</title>
 6                 </head>
 7                 <body>
 8                     <h1>自定义动态错误页面 404.html</h1>
 9                     <p>status:<span th:text="${status}"></span></p>
10                     <p>error:<span th:text="${error}"></span></p>
11                     <p>timestamp:<span th:text="${timestamp}"></span></p>
12                     <p>message:<span th:text="${message}"></span></p>
13                     <p>path:<span th:text="${path}"></span></p>
14                 </body>
15                 </html>

        (2) 模糊匹配

            可以使用 4xx.html 和 5xx.html  作为动态错误页面的文件名,并将它们存放在模板引擎文件夹下的 error 目录中,来模糊匹配对应类型的所有错误,例如 404、400 等错误状态码以“4”开头的所有异常,都会解析到动态错误页面 4xx.html 上。

            示例,创建 src/main/resources/templates/error/4xx.html 文件

1                 <!DOCTYPE html>
 2                 <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3                 <head>
 4                     <meta charset="UTF-8">
 5                     <title>自定义动态错误页面 4xx.html</title>
 6                 </head>
 7                 <body>
 8                     <h1>自定义动态错误页面 4xx.html</h1>
 9                     <p>status:<span th:text="${status}"></span></p>
10                     <p>error:<span th:text="${error}"></span></p>
11                     <p>timestamp:<span th:text="${timestamp}"></span></p>
12                     <p>message:<span th:text="${message}"></span></p>
13                     <p>path:<span th:text="${path}"></span></p>
14                 </body>
15                 </html>

    3) 自定义静态错误页面

        若 Sprng Boot 项目没有使用模板,当程序发生异常时,Spring Boot 的默认错误视图解析器(DefaultErrorViewResolver)则会解析静态资源文件夹下 error 目录中的静态错误页面。

        (1) 精确匹配

            可以根据错误状态码(例如 404、500、400 等等)的不同,分别创建不同的静态错误页面(例如 404.html、500.html、400.html 等等),并将它们存放在静态资源文件夹下的 error 目录中。当发生异常时,Spring Boot 会根据错误状态码精确匹配到对应的错误页面上。

            示例,创建 src/main/resources/static/error/404.html 文件

1                 <!DOCTYPE html>
 2                 <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3                 <head>
 4                     <meta charset="UTF-8">
 5                     <title>自定义静态错误页面 404.html</title>
 6                 </head>
 7                 <body>
 8                     <h1>自定义静态错误页面 404.html</h1>
 9                     <p>status:<span th:text="${status}"></span></p>
10                     <p>error:<span th:text="${error}"></span></p>
11                     <p>timestamp:<span th:text="${timestamp}"></span></p>
12                     <p>message:<span th:text="${message}"></span></p>
13                     <p>path:<span th:text="${path}"></span></p>
14                 </body>
15                 </html>

                注:这里的 404.html 是个静态页面,页面里不显示错误信息。

        (2) 模糊匹配

            可以使用 4xx.html 和 5xx.html 作为静态错误页面的文件名,并将它们存放在静态资源文件夹下的 error 目录中,来模糊匹配对应类型的所有错误,例如 404、400 等错误状态码以 “4” 开头的所有错误,都会解析到静态错误页面 4xx.html 上。

            示例,创建 src/main/resources/static/error/4xx.html 文件

1                 <!DOCTYPE html>
 2                 <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3                 <head>
 4                     <meta charset="UTF-8">
 5                     <title>自定义静态错误页面 4xx.html</title>
 6                 </head>
 7                 <body>
 8                     <h1>自定义静态错误页面 4xx.html</h1>
 9                     <p>status:<span th:text="${status}"></span></p>
10                     <p>error:<span th:text="${error}"></span></p>
11                     <p>timestamp:<span th:text="${timestamp}"></span></p>
12                     <p>message:<span th:text="${message}"></span></p>
13                     <p>path:<span th:text="${path}"></span></p>
14                 </body>
15                 </html>

    4) 错误页面优先级

        以上 5 种方式均可以定制 Spring Boot 错误页面,且它们的优先级顺序为:

            自定义动态错误页面(精确匹配)> 自定义静态错误页面(精确匹配)> 自定义动态错误页面(模糊匹配)> 自定义静态错误页面(模糊匹配)> 自定义 error.html

        当遇到错误时,Spring Boot 会按照优先级由高到低,依次查找解析错误页,一旦找到可用的错误页面,则直接返回客户端展示。

 

    5) 定制错误信息

        Spring Boot 提供了一套默认的异常处理机制,其主要流程如下:

            (1) 发生异常时,将请求转发到 “/error”,交由 BasicErrorController(Spring Boot 默认的 Error 控制器) 进行处理;
            (2) BasicErrorController 根据客户端的不同,自动适配返回的响应形式,浏览器返回错误页面,客户端APP返回 JSON 数据。
            (3) BasicErrorController 处理异常时,会调用 DefaultErrorAttributes(默认的错误属性处理工具) 的 getErrorAttributes() 方法获取错误数据。

        在默认的异常处理机制上,做一些调整,可以定制 Spring Boot 的错误信息,具体步骤如下。

            (1) 自定义异常处理类(使用 @ControllerAdvice 注解),将请求转发到 “/error”,交由 Spring Boot 底层(BasicErrorController)进行处理,自动适配浏览器客和客户端APP;
            (2) 通过继承 DefaultErrorAttributes 来定义一个错误属性处理工具,并在原来的基础上添加自定义的错误信息。

            注:被 @ControllerAdvice 注解的类可以用来实现全局异常处理,这是 Spring MVC 中提供的功能,在 Spring Boot 中可以直接使用。
        代码如下。

            (1) 创建 src/main/java/com/example/exception/PageNotFoundException.java 文件

1                 package com.example.exception;
2 
3                 public class PageNotExistException extends RuntimeException {
4                     public PageNotExistException() {
5                         super("页面不存在");
6                     }
7                 }

            (2) 创建 src/main/java/com/example/controller/PageNotExistExceptionHandler.java 文件

1                 package com.example.controller;
 2 
 3                 import java.util.HashMap;
 4                 import java.util.Map;
 5                 import javax.servlet.http.HttpServletRequest;
 6 
 7                 import org.springframework.web.bind.annotation.ControllerAdvice;
 8                 import org.springframework.web.bind.annotation.ExceptionHandler;
 9 
10                 import com.example.exception.PageNotExistException;
11 
12                 @ControllerAdvice
13                 public class PageNotExistExceptionHandler {
14                     @ExceptionHandler(PageNotExistException.class)
15                     public String handleException(Exception e, HttpServletRequest request) {
16 
17                         Map<String, Object> map = new HashMap<>();
18                         request.setAttribute("javax.servlet.error.status_code", 404);
19                         map.put("code", "PageNotExist");
20                         map.put("message", e.getMessage());
21                         request.setAttribute("ext", map);
22                         return "forward:/error";
23                     }
24                 }

            (3) 创建 src/main/java/com/example/componet/CustomErrorAttributes.java 文件

1                 package com.example.componet;
 2 
 3                 import java.util.Map;
 4                 import org.springframework.stereotype.Component;
 5                 import org.springframework.web.context.request.WebRequest;              
 6                 import org.springframework.boot.web.error.ErrorAttributeOptions;
 7                 import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
 8 
 9                 @Component
10                 public class CustomErrorAttributes extends DefaultErrorAttributes {
11                     @Override
12                     public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
13                         Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
14 
15                         // 添加自定义的错误数据
16                         errorAttributes.put("custom", "Custom Error Attributes");
17                         // 获取 PageNotExistExceptionHandler 传入 request 域中的错误数据
18                         Map ext = (Map) webRequest.getAttribute("ext", 0);
19                         errorAttributes.put("ext", ext);
20                         return errorAttributes;
21                     }
22                 }

            (4) 创建 src/main/resources/templates/error/404.html 文件

1                 <!DOCTYPE html>
 2                 <html lang="en" xmlns:th="http://www.thymeleaf.org">
 3                 <head>
 4                     <meta charset="UTF-8">
 5                     <title>自定义动态错误页面 404.html</title>
 6                 </head>
 7                 <body>
 8                     <h1>自定义静态错误页面 404.html</h1>
 9                     <p>status:<span th:text="${status}"></span></p>
10                     <p>error:<span th:text="${error}"></span></p>
11                     <p>timestamp:<span th:text="${timestamp}"></span></p>
12                     <p>message:<span th:text="${message}"></span></p>
13                     <p>path:<span th:text="${path}"></span></p>
14 
15                     <h3>定制错误信息:</h3>
16                     <p>custom:<span th:text="${custom}"></span></p>
17                     <p>ext.code:<span th:text="${ext.code}"></span></p>
18                     <p>ext.message:<span th:text="${ext.message}"></span></p>
19                 </body>
20                 </html>

            (5) 创建 src/main/com/example/controller/IndexController.java 文件

1                 package com.example.controller;
 2 
 3                 import org.springframework.stereotype.Controller;
 4                 import org.springframework.web.bind.annotation.RequestMapping;
 5                 import org.springframework.web.bind.annotation.ResponseBody;
 6 
 7                 import com.example.exception.PageNotExistException;
 8 
 9                 @Controller
10                 public class IndexController {
11                     @ResponseBody
12                     @RequestMapping("/test")
13                     public String testErr(String action) {
14 
15                         if ("error".equals(action)) {
16                             throw new PageNotExistException();
17                         }
18                         return "Test";
19                     }
20                 }

            访问:http://localhost:9090/test?action=error

                自定义动态错误页面 404.html
                status:404
                error:Not Found
                timestamp:Wed Apr 20 20:49:52 CST 2022
                message:
                path:/test

                定制错误信息:
                custom:Custom Error Attributes
                ext.code:PageNotExist
                ext.message:页面不存在