SpringBoot2 | 第二十三篇:错误处理机制

​ 我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况。接下来来看看SpringBoot的错误处理机制。(本文章以整合模板引擎 thymeleaf 为例)

[TOC]

SpringBoot默认的错误处理机制

1、默认效果(自适应)

自适应:指访问浏览器响应错误页面,访问客户端响应 JSON 数据(适用于传统项目);不支持自适应指不管访问的是浏览器还是客户端,响应的都是 JSON 数据(适用于前后端分离)。

  1. 浏览器访问,如果出现错误,SpringBoot会返回一个默认的 错误页面

1540819390719

  1. 其他客户端访问,如果出现错误,SpringBoot默认会响应一串 json数据

    1540865532344

2、底层 – ErrorMvcAutoConfiguration

SpringBoot的错误处理机制是由 ErrorMvcAutoConfiguration 自动配置组件实现的。

下面看看这个配置组件中有什么重要的组件

组件

ErrorPageCustomizer

用于错误页面定制

错误页面定制器

其中 @Value("${error.path:/error}") 用的语法是

1
2
3
4
5
/**
* 若在配置文件中找不到 hello 属性,则默认赋值为 defaultValue
*/
@Value("${hello:defaultValue}")
private String hello;
BasicErrorController

根据请求头的 Accept 控制,调用指定方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
...
/**
* 响应html页面的处理方法
*/
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
/**
* 响应json数据的处理方法
*/
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}


...
}
DefaultErrorAttributes

接受处理方法中的request域,定制返回的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes
implements ErrorAttributes, HandlerExceptionResolver, Ordered {
...

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}

...
}
DefaultErrorViewResolver

默认错误视图解析器:通过解析,根据模板引擎类别是否存在,返回指定的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

...

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), 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) {
// 默认SpringBoot去找一个页面 error/404
String errorViewName = "error/" + viewName;
// 获得模板引擎提供者
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
.getProvider(errorViewName, this.applicationContext);
if (provider != null) {
// 模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
// 模板引擎不可用,就在静态资源文件夹下找errorViewName对应的页面 error/404.html
return resolveResource(errorViewName, model);
}

...
}

流程

​ 一旦系统出现了4xx或5xx之类的错误;ErrorPageCustomizer(定制错误页面规则)就会生效;然后定制器会发送 /error 请求,该请求会被 BasicErrorController 处理。 BasicErrorController 会根据请求头的 Accept 调用对应的方法 errorHtmlerror(自适应),在这两个方法中,都调用了父类AbstractErrorController的方法 getErrorAttributes() (父类通过构造方法注入组件ErrorAttributes),getErrorAttributes()中调用了 ErrorAttributes组件的 getErrorAttributes(),在该方法中对返回的数据进行封装,详细请看源码~~

分析

该控制器是如何做到对浏览器响应页面,对其他客户端响应 json 数据的呢?

访问浏览器

1540889338496

访问客户端

1540889629457

1540889658631

BasicErrorController 根据这两种方式定制了两个方法 errorHtml 和 error 来匹配请求

1540889796980

3、如何定制错误响应

​ 虽然,Spring Boot 中实现了默认的 error 映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示

错误页面

templates 文件夹

SpringBoot 默认先去 templates 文件夹 下找资源 ==error/状态码.html==,将匹配的页面返回 (所以我们在使用的时候,直接将错误页面命名为 状态码.html,放在 template/error/ 下即可。SpringBoot 就会去找与错误状态码对应的页面)

那么问题就来了?错误码这么多。难道我们需要定制n个错误页面吗?

当然不用。

Spring Boot 已经帮我们想好了这个问题,DefaultErrorViewResolver 组件中可以看到,对于同一系列的错误码,我们可以使用 前缀xx 的格式定制错误页面。如 ==4xx、5xx==。如果我们需要对指定的状态码做特殊的处理,这时候我们可以单独给这个状态码做一个页面,如 404.html 。SpringBoot 匹配的时候会优先查找 精确的状态.html,后模糊匹配。(先精确后模糊

1540869426354

数据模板(定制的时候,一些非自定义的异常。通过下面格式拿数据,实现统一格式。自定义的可以建个父异常,然后在自定义的 ErrorAttributes 中根据是否继承分类讨论)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"timestamp": "2019-07-26T01:21:48.384+0000", //时间戳
"status": 405, // 状态码
"error": "Method Not Allowed", // 错误提示
"message": "Request method 'POST' not supported", // 异常消息
"path": "/categories/nesting" // 请求方法
}

// 这种没有自定义的,从默认的取信息,改成符合的自己`统一返回VO`咯。例如:
{
"code": 405,
"message": "Request method 'POST' not supported"
}
static 文件夹

​ 如果在 templates 下找不到错误页面,SpringBoot 接下来会去 static 下找;如果都这个两个文件夹都没有,那么 SpringBoot 会返回默认的错误页面

json数据

当客户端访问报错后,显示的 json 数据是固定的。那么我们如何定制自己的 json 数据呢。

自适应(推荐)

定制自己想要的 json 格式的数据,保持自适应效果。即使浏览器的不需要也无妨,我们只用它客户端的姿势就行。

需要 ExceptionHandler + ErrorAttributes 两个组件共同实现

  1. 在异常处理方法中,我们将想要的数据放到 map 中

    要求:

    1. 返回类型为 String
    2. 返回值为 "forward:/error"
    3. request域 中放我们自己的状态码,不放默认为 200
    4. 数据放在 request域
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 测试定制json数据(保持自适应效果)的异常处理方法
    */
    @ExceptionHandler(AdaptiveException.class)
    public String adaptive(Exception e, HttpServletRequest request) {
    // 传入自己的错误状态码,如 4xx,5xx 不传默认200
    request.setAttribute(CUSTOM_STATUS_CODE, 500);
    Map<String, Object> map = new HashMap<>();
    map.put("code", "adaptive.code");
    map.put("message", e.getMessage());
    request.setAttribute("ext", map);
    return "forward:/error";
    }
  2. 在getErrorAttributes() 中我们 定制返回的 json 数据的格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 这个方法有点像代理
    * @param webRequest :接受异常处理方法中的request(可参考源码)
    * @param includeStackTrace
    * @return
    */
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest,
    boolean includeStackTrace) {
    /**
    * 这个map就是最后返回的数据,如果你想改格式,改这个map即可
    */
    Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
    // 后面的scope知道你对哪个域存取数据
    Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
    map.put("ext", ext);
    map.put("where", "进入自定义ErrorAttributes的getErrorAttributes中了~~");
    return map;
    }
不自适应

缺点:没有自适应效果

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试传统定制json数据的异常处理方法
*/
@ResponseBody
@ExceptionHandler(TraditionalException.class)
public Map<String, Object> fatal(Exception e) {
Map<String, Object> map = new HashMap<>();
map.put("code", "traditional.code");
map.put("message", e.getMessage());
return map;
}

环境/版本一览:

  • 开发工具:Intellij IDEA 2018.2.2
  • springboot: 2.0.6.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9
  • spring-boot-starter-thymeleaf:2.0.6.RELEASE
  • lombok:1.16.20

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!-- 引入 Thymeleaf 模板引擎依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
</dependencies>

application.yml

1
2
3
spring:
thymeleaf:
cache: false

html

指定三个错误页面

4xx.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${status}">4xx</title>
</head>
<body>
<h1>4xx</h1>
<h2>时间戳:[[${timestamp}]]</h2>
<h2>状态码:[[${status}]]</h2>
<h2>错误提示:[[${error}]]</h2>
</body>
</html>

5xx.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="${status}">5xx</title>
</head>
<body>
<h1>5xx</h1>
<h2>状态码:[[${status}]]</h2>
<h2>错误提示:[[${error}]]</h2>
<h2>时间戳:[[${timestamp}]]</h2>
</body>
</html>

404.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404错误页面(这是特制的~~)</title>
</head>
<body>
<h1>404定制页面</h1>
<h2>时间戳:[[${timestamp}]]</h2>
<h2>状态码:[[${status}]]</h2>
<h2>错误提示:[[${error}]]</h2>
</body>
</html>

目录如下:(一定要放在templates下的error中)

1564379969025

FatalErrorAttributes

自定义一个 ErrorAttributes 组件,用于定制返回的(统一的) JSON 格式

为什么说统一呢?因为如果你直接在 ExceptionHandler 上面,加了@RequestBody,那么没被你拦截到的异常显示的格式是什么样子,还是 timestamp 那一串默认的 json。也许你会说,我加个 ExceptionHandler专门处理剩下来的所有异常,code都返回 500。这可不行哦,这种用法很危险,你还有很多 4xx 类型的错误呢。比方说,前端搞错请求方式,把 GET 搞成了 POST,一顿操作,真的报 500 ,他截图发了句,你这个接口有 bug,你看了下日志,发现是对方请求方式不对,但是你怎么跟他说,百口难辩呀。一定程度下还降低了双方对接的效率…

但是,如果你自定义这个组件,就不同了,根据是否自定义来封装返回的属性;就能达到自定义的ExceptionHandler可以返回定制的错误提示未自定义的ExceptionHandler能返回默认的错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.fatal.attribute;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

/**
* 自定义 ErrorAttributes
* @author: Fatal
* @date: 2018/10/30 0030 15:12
*/
@Component
public class FatalErrorAttributes extends DefaultErrorAttributes {

/**
* 这个方法有点像代理
* @param webRequest :接收异常处理方法中的request
* @param includeStackTrace
* @return
*/
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
boolean includeStackTrace) {
/**
* 这个map就是最后返回的数据,如果你想改格式,改这个map即可
*/
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
// 后面的scope知道你对哪个域存取数据,可以参考 RequestAttributes类
Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext", ext);
map.put("where", "进入自定义ErrorAttributes的getErrorAttributes中了~~");
return map;
}
}

Custom Exception

TraditionalException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.fatal.exception;

/**
* 测试定制json数据异常(不自适应)
* @author: Fatal
* @date: 2018/10/30 0030 14:08
*/
public class TraditionalException extends RuntimeException {

public TraditionalException() {
super("这是一个测试定制json数据的异常(不自适应)");
}

}

AdaptiveException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.fatal.exception;

/**
* 测试定制json数据异常(自适应)
* @author: Fatal
* @date: 2018/10/30 0030 14:08
*/
public class AdaptiveException extends RuntimeException {

public AdaptiveException() {
super("这是一个测试定制json数据的异常(自适应)");
}

}

GlobalExceptionHandler

写一个全局异常处理器,里边可以自定义处理各种异常的处理器(ExceptionHandler)。类上标记注解@ControllerAdvice声明该组件是全局异常处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.fatal.handler;

import com.fatal.exception.AdaptiveException;
import com.fatal.exception.TraditionalException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
* 自定义全局异常处理器
* 子类的处理方法优先于父类获得处理权,若子类不存在,就近原则,离子类最近的父类的处理方法获得处理权
* @author: Fatal
* @date: 2018/10/30 0030 14:23
*/
@ControllerAdvice
public class GlobalExceptionHandler {

/** 状态码键名 */
private final String CUSTOM_STATUS_CODE = "javax.servlet.error.status_code";

/**
* 不自适应
*/
@ResponseBody
@ExceptionHandler(TraditionalException.class)
public Map<String, Object> fatal(Exception e) {
Map<String, Object> map = new HashMap<>();
map.put("code", "traditional.code");
map.put("message", e.getMessage());
return map;
}

/**
* 自适应
*/
@ExceptionHandler(AdaptiveException.class)
public String adaptive(Exception e, HttpServletRequest request) {
// 传入自己的错误状态码,如 4xx,5xx 不传默认200
request.setAttribute(CUSTOM_STATUS_CODE, 500);
Map<String, Object> map = new HashMap<>();
map.put("code", "adaptive.code");
map.put("message", e.getMessage());
request.setAttribute("ext", map);
return "forward:/error";
}

}

FatalController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.fatal.controller;

import com.fatal.exception.AdaptiveException;
import com.fatal.exception.TraditionalException;
import com.fatal.handler.GlobalExceptionHandler;
import lombok.Data;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
* @author: Fatal
* @date: 2018/10/30 0030 14:50
*/
@Controller
@Data
public class FatalController {

private GlobalExceptionHandler handler;

FatalController(GlobalExceptionHandler handler) {
this.handler = handler;
}

/**
* 模拟 400 错误
*/
@GetMapping("/400")
public void error400 (@RequestParam(value = "data", required = true) String data) {

}

/**
* 测试传统定制的json数据
*/
@GetMapping("/traditional")
public void errorTraditional500() {
throw new TraditionalException();
}

/**
* 测试定制的json数据(保持自适应效果)
*/
@GetMapping("/adaptive")
public void errorAdaptive500() {
throw new AdaptiveException();
}


}

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.fatal;

import com.fatal.controller.FatalController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter23ApplicationTests {

@Autowired
private FatalController controller;

/**
* 测试构造方法注入组件不需要在构造方法上加注解`@Autowired`
*/
@Test
public void contextLoads() {
System.out.println(controller.getHandler());
}

}

测试

启动项目

400

http://localhost:8080/400

1564404928215

404

http://localhost:8080/

1564404950090

不自适应

浏览器

1564405143417

客户端

1564405357640

自适应

自适应

1564405268422

客户端

1564405292375

构造函数注入

启动 Chapter23ApplicationTests.contextLoads(),显示如下

1564405772877

笔记

全局异常处理器

在全局异常处理器中,各个处理方法都遵循两个原则

  • 子类的处理方法优先于父类获得处理权
  • 若子类不存在,就近原则,离子类最近的父类的处理方法获得处理权

构造方法注入

可以不用在构造方法上加注解,只需要将组件作为构造方法参数即可

总结

定制错误页面

​ 在 templates/error 下定制 状态码.html(可以 指定 某个状态码,也可以使用 前缀xx 的格式进行模糊匹配)。SpringBoot 会优先匹配精确的,再匹配模糊的。

定制json数据(保持自适应)

步骤:

  1. 创建全局异常处理器(对处理方法的要求:返回类型必须是 String;返回值为 "forward:/error" ;request域 中放我们自己的状态码,不放默认为 200;数据放在 request域 中)
  2. 创建自定义 ErrorAttrubute

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter23