image.png

为什么需要全局异常处理?

在日常开发中,为了不抛出异常堆栈信息给前端页面,每次编写 Controller 层代码都要尽可能的 catch 住所有 service 层、dao 层等异常,代码耦合性较高,且不美观,不利于后期维护。
这时就需要进行统一的异常处理。
为解决该问题,计划将 Controller 层异常信息统一封装处理,且能区分对待 Controller 层方法返回给前端的 String、Map、JSONObject、ModelAndView 等结果类型。

Springboot 默认是如何做的?

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

  1. 对于浏览器客户端而言,Spring Boot 会响应一个“ whitelabel”错误视图,以 HTML 格式呈现错误信息,如图:

image.png

  1. 对于机器客户端而言,Spring Boot 将生成 JSON 响应,来展示异常消息:
1
2
3
4
5
6
7
{
"timestamp": "2021-07-12T07:05:29.885+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/m1ain.html"
}

上述/error 错误页面路径可以理解为 SpringBoot 默认为我们写了一个模版错误页面,然后默认还写了一个 Controller,该 Controller 中包含一个/error 请求地址映射指向该错误页面。当 SpringBoot 的错误处理机制捕获到请求异常之后,则会将用户的原请求携带上错误信息,然后转发到这个/error 页面,页面再显示错误的相关信息。
虽然 SpringBoot 提供了默认的错误显示页面,但是仅使用该默认错误页面会存在大量的局限性:

  • 该页面比较简陋,对于用户而言并不友好;
  • 500 错误暴露了服务器的详细出错原因,存在严重安全隐患;
  • 在前后端分离的项目中,客户端需要的不是页面,而是 JSON 数据。

如何自定义?

基于上述 SpringBoot 默认错误处理机制存在的局限性和问题,在 SpringBoot 中,@ControllerAdvice即可开启全局异常处理,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

1
2
3
4
5
6
7
@ControllerAdvice
public class ControllerHandlers(){
@ExceptionHandler
public String errorHandler(Exception e){
return "error";
}
}

控制器通知还有一个兄弟,@RestControllerAdvice,如果用了它,错误处理方法的返回值不会表示用的哪个视图,而是会作为 HTTP body 处理,即相当于错误处理方法加了@ResponseBody注解。

1
2
3
4
5
6
7
@RestControllerAdvice
public class ControllerHandlers(){
@ExceptionHandler
public String errorHandler(Exception e){
return "error";
}
}

@ExceptionHandler注解的方法只能返回一种类型,在前后端分离开发中我们通常返回,统一返回类型、优化错误的提示和数据等,因此我们可以封装我们自己的数据传输对象ApiResponse,在传输过程中,会对异常进行枚举,所以先定义枚举类ExceptionEnum

自定义异常枚举类

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
@Getter
@AllArgsConstructor
public enum ExceptionEnum {
/**
* 成功
*/
SUCCESS(20000, "操作成功"),
/**
* 没有操作权限
*/
AUTHORIZED(40300, "没有操作权限"),
/**
* 系统异常
*/
SYSTEM_ERROR(50000, "系统异常"),
/**
* 失败
*/
FAIL(51000, "操作失败"),
/**
* 参数校验失败
*/
VALID_ERROR(52000, "参数格式不正确"),
/**
* 用户名已存在
*/
USERNAME_EXIST(52001, "用户名已存在"),
/**
* 用户名不存在
*/
USERNAME_NOT_EXIST(52002, "用户名不存在"),
/**
* qq登录错误
*/
QQ_LOGIN_ERROR(53001, "qq登录错误"),
/**
* 微博登录错误
*/
WEIBO_LOGIN_ERROR(53002, "微博登录错误");

/**
* 状态码
*/
private final Integer code;

/**
* 描述
*/
private final String message;

}

自定义数据传输

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
73
74
75
76
77
@Data
public class ApiResponse<T> {
/**
* 返回状态
*/
private Boolean flag;
/**
* 返回码
*/
private Integer code;
/**
* 返回信息
*/
private String message;
/**
* 返回数据
*/
private T data;

/**
* 请求成功几种重载定义
*
* @param <T>
* @return
*/
public static <T> ApiResponse<T> ok() {
return RestResponse(true, SUCCESS.getCode(), SUCCESS.getMessage(), null);

}

public static <T> ApiResponse<T> ok(T data) {
return RestResponse(true, SUCCESS.getCode(), SUCCESS.getMessage(), data);
}

public static <T> ApiResponse<T> ok(String message) {
return RestResponse(true, SUCCESS.getCode(), message, null);
}

public static <T> ApiResponse<T> ok(String message, T data) {
return RestResponse(true, SUCCESS.getCode(), message, data);
}

/**
* 请求失败几种重载定义
*
* @param <T>
* @return
*/
public static <T> ApiResponse<T> fail() {
return RestResponse(false, FAIL.getCode(), FAIL.getMessage(), null);
}

public static <T> ApiResponse<T> fail(T data) {
return RestResponse(false, FAIL.getCode(), FAIL.getMessage(), data);
}

public static <T> ApiResponse<T> fail(String message) {
return RestResponse(false, FAIL.getCode(), message, null);
}

public static <T> ApiResponse<T> fail(String message, T data) {
return RestResponse(false, FAIL.getCode(), message, data);
}

public static <T> ApiResponse<T> fail(ExceptionEnum exceptionEnum) {
return RestResponse(false, exceptionEnum.getCode(), exceptionEnum.getMessage(), null);
}

public static <T> ApiResponse<T> RestResponse(boolean flag, Integer code, String message, T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setFlag(flag);
response.setCode(code);
response.setMessage(message);
response.setData(data);
return response;
}
}

###

自定义异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Getter
@AllArgsConstructor
public class BizException extends RuntimeException {
/**
* 错误码
*/
private Integer code = FAIL.getCode();

/**
* 错误信息
*/
private final String message;

public BizException(String message) {
this.message = message;
}

public BizException(ExceptionEnum exceptionEnum) {
this.code = exceptionEnum.getCode();
this.message = exceptionEnum.getMessage();
}
}

自定义全局异常处理

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
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerConfig {

/**
* 处理业务异常
*
* @param e 异常
* @return 自定义数据传输
*/
@ExceptionHandler(value = BizException.class)
public ApiResponse<?> errorHandler(BizException e) {
log.error("业务异常:" + e);
return ApiResponse.fail(e.getCode(), e.getMessage());
}

/**
* 处理系统异常
*
* @param e 异常
* @return 自定义数据传输
*/
@ExceptionHandler(value = Exception.class)
public ApiResponse<?> errorHandler(Exception e) {
log.error("系统异常:" + e);
return ApiResponse.fail(ExceptionEnum.SYSTEM_ERROR);
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class TestController {
@GetMapping("/hi")
public String test01() {
int a = 10 / 0;
return "正常访问hi";
}
@GetMapping("/user")
public String test01(@RequestParam Integer id) {
if(id<0){
throw new BizException(-1,"id不能小于0");
}
return "正常访问user";
}
}

image.png
image.png

自定义全局异常处理除了可以处理上述的数据格式之外,也可以处理页面的跳转,只需在新增的异常方法的返回处理上填写该跳转的路径并不使用 ResponseBody 注解即可。

总结

异常处理,能够减少代码的重复度和复杂度,有利于代码的维护,并且能够快速定位到 BUG,大大提高我们的开发效率。