SpringBoot(十一) 全局异常处理

在日常web开发中发生了异常,往往是需要通过一个统一的异常处理来保证客户端能够收到友好的提示

笨方法(不推荐)

采用try-catch的方式,手动捕获异常信息,然后返回对应的结果集,相信很多人都看到过类似的代码(如:封装成Result对象);该方法虽然间接性的解决错误暴露的问题,同样的弊端也很明显,增加了大量的代码量,当异常过多的情况下对应的catch层愈发的多了起来,很难管理这些业务异常和错误码之间的匹配,所以最好的方法就是通过简单配置全局掌控….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("/test2")
public Map<String, String> test2() {
Map<String, String> result = new HashMap<>(16);
// TODO 直接捕获所有代码块,然后在 cache
try {
int i = 10 / 0;
result.put("code", "200");
result.put("data", "具体返回的结果集");
} catch (Exception e) {
result.put("code", "500");
result.put("message", "请求错误");
}
return result;
}

Spring Boot默认的异常处理机制

默认情况下,Spring Boot为两种情况提供了不同的响应方式。

情况一

浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求头中Accept: text/html,所以Spring Boot默认会响应一个html文档内容,称作Whitelabel Error Page

情况二

使用Postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,Spring Boot会返回类似如下的Json格式字符串信息。

原理剖析

原理也很简单,Spring Boot 默认提供了程序出错的结果映射路径/error。这个/error请求会在BasicErrorController中处理,其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自客户端浏览器(浏览器通常默认自动发送请求头内容Accept:text/html)还是客户端接口的调用,以此来决定返回页面视图还是 JSON 消息内容。

IDEA 使用 ctrl+N 查找类 BasicErrorController

添加依赖

在 pom.xml 中添加上 web 和 thymeleaf 的依赖即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

<!--依赖管理 -->
<dependencies>
<dependency> <!--添加Web依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <!--添加Thymeleaf依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency><!--添加Test依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency><!--添加Lombok依赖 -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</dependency>
</dependencies>

自定义错误页面

好了,了解完Spring Boot默认的错误机制后,我们来点有意思的,浏览器端访问的话,任何错误Spring Boot返回的都是一个Whitelabel Error Page的错误页面,这个很不友好,所以我们可以自定义下错误页面。

创建error.html

直接在 /resources/templates 下面创建 error.html 就可以覆盖默认的Whitelabel Error Page的错误页面,我项目用的是thymeleaf模板,对应代码如下:

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

Thymeleaf模板引擎

注:SpringBoot默认支持很多种模板引擎(如Thymeleaf、FreeMarker),并提供了相应的自动配置,做到开箱即用。默认的页面加载路径是 src/main/resources/templates ,如果放到其它目录需在配置文件指定。(举例:spring.thymeleaf.prefix=classpath:/views/ ),后续将会讲到该模板

创建404、500页面

此外,如果你想更精细一点,根据不同的状态码返回不同的视图页面,也就是对应的404,500等页面,这里分两种情况,错误页面可以是静态HTML(即,添加到任何静态资源文件夹下),也可以使用模板构建,文件的名称应该是确切的状态码。

注:这时候如果存在上面介绍的error.html页面,则状态码错误页面将覆盖error.html,具体状态码错误页面优先级比较高。

情况一

如果只是静态HTML页面,不带错误信息的,在resources/public/下面创建error目录,在error目录下面创建对应的状态码html即可 ,例如,要将404映射到静态HTML文件,您的文件夹结构如下所示:

静态404.html简单页面如下:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body>
静态404错误页面
</body>
</html>

这样访问一个错误路径的时候,就会显示静态404错误页面错误页面

情况二

注:如果同时存在静态页面500.html和动态模板的500.html,则后者覆盖前者。即templates/error/这个的优先级比resources/public/error高。

如果是动态模板页面,可以带上错误信息,在resources/templates/下面创建error目录,在error目录下面命名即可:

500.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>500</title>
</head>
<body>
动态500错误页面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>
控制层

模拟500错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author Ray
* @date 2018/7/26 0026
*/
@RestController
public class ErrorController {

@RequestMapping(value = "/ex")
public String error(){
int i = 10/0;
return "ex";
}
}

整体概括

(1)error.html会覆盖默认的 whitelabel Error Page 错误提示
(2)静态错误页面优先级别比error.html高
(3)动态模板错误页面优先级比静态错误页面高

@ControllerAdvice 处理异常

Spring Boot提供的ErrorController是一种全局性的容错机制。此外,你还可以用@ControllerAdvice注解和@ExceptionHandler注解实现对指定异常的特殊处理。

这里介绍两种情况:
(1)局部异常处理 @Controller + @ExceptionHandler
(2)全局异常处理 @ControllerAdvice + @ExceptionHandler

局部异常处理

@Controller + @ExceptionHandler

局部异常主要用到的是@ExceptionHandler注解,此注解注解到类的方法上,当此注解里定义的异常抛出时,此方法会被执行,此注解用于标注方法中的特定的异常。

示例: 捕获ArithmeticException 异常

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
/**
* @author Ray
* @date 2018/7/26 0026
*/
@RestController
public class ErrorController {

@RequestMapping(value = "/ex")
public String error(){
int i = 10 / 0;
return "ex";
}

/**
* 局部异常处理
*/
@ExceptionHandler(Exception.class)
public String exHandler(Exception e){
// 判断发生异常的类型是ArithmeticException 异常做出响应
if(e instanceof ArithmeticException){
return "发生了除0异常";
}
// 其他异常做出响应
return "发生了未知异常";
}
}

全局异常处理

@ControllerAdvice + @ExceptionHandler

在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。

简单的说,进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。

我们实际开发中,如果是要实现RESTful API,那么默认的JSON错误信息就不是我们想要的,这时候就需要统一一下JSON格式,所以需要封装一下。

AjaxObject

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.ray.springboot211.util;

import org.springframework.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

/**
* @author Ray
* @date 2018/7/26 0026
* 返回数据
*/
public class AjaxObject extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;

/**
* 正常情况返回 0
*/
public AjaxObject() {
put("code", 0);
}

public static AjaxObject error(String msg) {
return error(HttpStatus.INTERNAL_SERVER_ERROR, msg);
}

public static AjaxObject error(HttpStatus internalServerError, String msg) {
return error(HttpStatus.INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}

public static AjaxObject error(int code, String msg) {
AjaxObject r = new AjaxObject();
r.put("code", code);
r.put("msg", msg);
return r;
}

public static AjaxObject ok(String msg) {
AjaxObject r = new AjaxObject();
r.put("msg", msg);
return r;
}

public static AjaxObject ok(Map<String, Object> map) {
AjaxObject r = new AjaxObject();
r.putAll(map);
return r;
}

public static AjaxObject ok() {
return new AjaxObject();
}

@Override
public AjaxObject put(String key, Object value) {
super.put(key, value);
return this;
}

public AjaxObject data(Object value) {
super.put("data", value);
return this;
}

public static AjaxObject apiError(String msg) {
return error(1, msg);
}
}

自定义异常类

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
package com.ray.springboot211.exception;

import lombok.Data;

import java.io.Serializable;

/**
* @author Ray
* @date 2018/7/26 0026
* 自定义的异常类
*/
@Data
public class BusinessException extends RuntimeException implements Serializable {

private static final long serialVersionUID = 1L;

private String msg;
private int code = 500;

public BusinessException(String msg) {
super(msg);
this.msg = msg;
}

public BusinessException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}

public BusinessException(int code,String msg) {
super(msg);
this.msg = msg;
this.code = code;
}

public BusinessException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}

}

注:spring 对于 RuntimeException 异常才会进行事务回滚

控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author Ray
* @date 2018/7/26 0026
*/
@RestController
public class BaseErrorController {

@RequestMapping("/json")
public void json(ModelMap modelMap) {
System.out.println(modelMap.get("author"));
int i = 5 / 0;
}
}

全局异常处理类

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.ray.springboot211.exception;

import com.ray.springboot211.util.AjaxObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

/**
* @author Ray
* @date 2018/7/26 0026
*/
@RestControllerAdvice
public class BusinessExceptionHandler {

private Logger logger = LoggerFactory.getLogger(getClass());

/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
System.out.println("请求有参数才进来");
}

/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
* @param model
*/
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("author", "Ray");
}

@ExceptionHandler(Exception.class)
public Object handleException(Exception e,HttpServletRequest req){
AjaxObject r = new AjaxObject();
//业务异常
if(e instanceof BusinessException){
r.put("code", ((BusinessException) e).getCode());
r.put("msg", ((BusinessException) e).getMsg());
}else{//系统异常
r.put("code","500");
r.put("msg","未知异常,请联系管理员");
}

//使用HttpServletRequest中的header检测请求是否为ajax, 如果是ajax则返回json, 如果为非ajax则返回view(即ModelAndView)
String contentTypeHeader = req.getHeader("Content-Type");
String acceptHeader = req.getHeader("Accept");
String xRequestedWith = req.getHeader("X-Requested-With");
if ((contentTypeHeader != null && contentTypeHeader.contains("application/json"))
|| (acceptHeader != null && acceptHeader.contains("application/json"))
|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return r;
} else {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", e.getMessage());
modelAndView.addObject("url", req.getRequestURL());
modelAndView.addObject("stackTrace", e.getStackTrace());
modelAndView.setViewName("error");
return modelAndView;
}
}
}

@ExceptionHandler 拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型,上面我配置了拦截Exception,再根据不同异常类型返回不同的相应,最后添加判断,如果是Ajax请求,则返回json,如果是非ajax则返回view,这里是返回到 error.html 页面。

error.html

为了展示错误的时候更友好,封装了error.html,不仅展示了错误,还添加了跳转百度以及StackOverFlow的按钮

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
<!DOCTYPE HTML>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<title>Spring Boot管理后台</title>
<script type="text/javascript">
</script>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
<div id="navbar">
<h1>系统异常统一处理</h1>
<h3 th:text="'错误信息:'+${msg}"></h3>
<h3 th:text="'请求地址:'+${url}"></h3>

<h2>Debug</h2>
<a th:href="@{'https://www.baidu.com/s?wd='+${msg}}" class="btn btn-info btn-lg" target="_blank" id="Baidu">Baidu</a>
<a th:href="@{'http://stackoverflow.com/search?q='+${msg}}"
class="btn btn-default btn-lg" target="_blank" id="StackOverFlow">StackOverFlow</a>
<h2>异常堆栈跟踪日志StackTrace</h2>
<div th:each="line:${stackTrace}">
<div th:text="${line}"></div>
</div>
</div>
</div>
<div layout:fragment="js" th:remove="tag">
</div>
</body>
</html>

测试

访问地址: http://localhost:8080/json ,因为是浏览器发起的,返回的是error界面:

如果是ajax请求,返回的就是错误:

1
{ "msg":"未知异常,请联系管理员", "code":500 }

全局异常类我用的是@RestControllerAdvice,而不是@ControllerAdvice,因为这里返回的主要是json格式,这样可以少写一个@ResponseBody。

-------------- 本文结束  感谢您的阅读 --------------