写在前面

在软件开发过程中,不可避免的是需要处理各种异常,在 Java 中,处理异常的方式一般就是采用try{...}catch{...}finally{...}代码块。在业务系统中,可能会有大量的异常处理代码块,这样不仅有大量的冗余代码,而且还影响代码的可读性。比较下面两张图:

image-20220328132128521 image-20220328132228437

可以看到,明显第二种的代码简洁,可读性高!此处的代码是在 Controller 层中的,在 Service 层中会有更多的异常处理代码块。

那么我们应该如何优雅的进行异常处理呢?

什么是统一异常处理

在 Spring 里,我们可以使用@ControllerAdvice 来处理一些全局性的东西,最常见的是结合@ExceptionHandler 注解用于全局异常的处理。

@ControllerAdvice 是在类上声明的注解,其用法主要有三点:

  • @ExceptionHandler注解标注的方法:用于捕获 Controller 中抛出的不同类型的异常,从而达到异常全局处理的目的
  • @InitBinder注解标注的方法:用于请求中注册自定义参数的解析,从而达到自定义请求参数格式的目的
  • @ModelAttribute注解标注的方法:表示此方法会在执行目标 Controller 方法之前执行

跟异常处理有关的只有@ExceptionHandler注解,从字面意思上理解,就是异常处理器的意思,其实际作用也正是如此:若在某个Controller类定义一个异常处理方法,并在方法上添加该注解,那么当出现指定的异常时,会执行该处理异常的方法,其可以使用SpringMVC提供的数据绑定,比如接受一个当前抛出的Throwable对象。

但是,这样一来,就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。这样一来,就会造成大量的冗余代码,而且若需要新增一种异常的处理逻辑,就必须修改所有Controller类了,很不优雅。

当然你可能会说,那就定义个类似BaseController的基类,这样总行了吧。

这种做法虽然没错,但仍不尽善尽美,因为这样的代码有一定的侵入性和耦合性。简简单单的Controller,我为啥非得继承这样一个类呢,万一已经继承其他基类了呢。大家都知道Java只能继承一个类。

那有没有一种方案,既不需要跟Controller耦合,也可以将定义的 异常处理器 应用到所有控制器呢?所以注解@ControllerAdvice出现了,简单的说,该注解可以把异常处理器应用到所有控制器,而不是单个控制器。借助该注解,我们可以实现:在独立的某个地方,比如单独一个类,定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对 不同阶段的不同异常 进行处理。这就是统一异常处理的原理。

注意到上面对异常按阶段进行分类,大体可以分成:进入Controller前的异常 和 Service 层异常,具体可以参考下图:

image-20220328135114123

统一异常处理实战

通过全局统一的异常处理将自定义的错误码以 json 的格式返回给前端。

统一返回结果类

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package org.jeecg.common.api.vo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.enums.ErrorCodeEnum;

import java.io.Serializable;

/**
* @description: 接口返回对象 -更新
* @author: luo_Jj
* @date: 2022/3/24 17:58
*/
@Data
@ApiModel(value="接口返回对象", description="接口返回对象")
public class Result<T> implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 成功标志
*/
@ApiModelProperty(value = "成功标志")
private boolean success = true;

/**
* 返回处理消息
*/
@ApiModelProperty(value = "返回处理消息")
private String message = "";

/**
* 返回代码
*/
@ApiModelProperty(value = "返回代码")
private String code = "000000";

/**
* 返回数据对象 data
*/
@ApiModelProperty(value = "返回数据对象")
private T result;

/**
* 时间戳
*/
@ApiModelProperty(value = "时间戳")
private long timestamp = System.currentTimeMillis();

public Result() {
}

public Result(String code,String message) {
this.code = code;
this.message = message;
}

public Result<T> success(String message) {
this.message = message;
this.code = CommonConstant.SC_OK_200;
this.success = true;
return this;
}

public static<T> Result<T> OK() {
Result<T> r = new Result<T>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
return r;
}

public static<T> Result<T> OK(T data) {
Result<T> r = new Result<T>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
r.setResult(data);
return r;
}

public static<T> Result<T> OK(String msg, T data) {
Result<T> r = new Result<T>();
r.setSuccess(true);
r.setCode(CommonConstant.SC_OK_200);
r.setMessage(msg);
r.setResult(data);
return r;
}

public static<T> Result<T> error(String msg, T data) {
Result<T> r = new Result<T>();
r.setSuccess(false);
r.setCode(CommonConstant.SC_INTERNAL_SERVER_ERROR_500);
r.setMessage(msg);
r.setResult(data);
return r;
}

public static<T> Result<T> error(String msg) {
return error(CommonConstant.SC_INTERNAL_SERVER_ERROR_500, msg);
}

/**
* @description: 传递一个错误码枚举
* @author: luo_jj
* @date: 2022/3/28 14:55
* @param errorCodeEnum:
* @return: org.jeecg.common.api.vo.Result<T>
*/
public static<T> Result<T> error(ErrorCodeEnum errorCodeEnum) {
return error(errorCodeEnum.getCode(), errorCodeEnum.getMessage());
}

public static<T> Result<T> error(String code, String msg) {
Result<T> r = new Result<T>();
r.setCode(code);
r.setMessage(msg);
r.setSuccess(false);
return r;
}

public Result<T> error500(String message) {
this.message = message;
this.code = CommonConstant.SC_INTERNAL_SERVER_ERROR_500;
this.success = false;
return this;
}

}

错误码枚举类

需要定义一个枚举类,包含所有的自定义的结果码:

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
package org.jeecg.common.constant.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
* @description: 错误码枚举
* @author: luo_jj
* @date: 2022年03月24日 17:15
*/
@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum ResultCodeEnum {
/*
* 错误产生来源分为 A/B/C
* A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;
* B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;
* C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;
* 四位数字编号从 0001 到 9999,大类之间的步长间距预留 100
*
* 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。
* 调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。
* 说明:在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,分别是:A0001(用户端错误)、B0001(系统执行出错)、C0001(调用第三方服务出错)。
* 错误码表:http://192.168.88.211:8090/pages/viewpage.action?pageId=5473234
*/

/*一切ok*/
SUCCESS_ERROR("000000","成功"),

/*用户端错误码*/
CLIENT_ERROR("A0001","用户端错误"),

/*服务端错误码*/
SYSTEM_ERROR("B0001","系统执行出错"),

/*第三方服务错误码*/
TPA_ERROR("C0001","调用第三方服务出错");

/** 错误码 */
private String code;

/** 错误描述 */
private 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
package org.jeecg.common.exception;

import lombok.Getter;
import lombok.Setter;
import org.jeecg.common.constant.enums.ResultCodeEnum;

/**
* @description: 万序自定义异常
* @author: luo_Jj
* @date: 2022/3/24 18:15
*/
@Getter
@Setter
public class VanxSoftException extends RuntimeException {
private static final long serialVersionUID = 1L;

private ResultCodeEnum resultCodeEnum;

public VanxSoftException(ResultCodeEnum resultCodeEnum){
super(resultCodeEnum.getMessage());
this.resultCodeEnum = resultCodeEnum;
}
}

全局异常处理类

定义一个全局异常处理类

  1. 通过 @RestControllerAdvice 指定该类为 Controller 增强类并返回 json 到前端
  2. 通过 @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
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package org.jeecg.common.exception;

import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.jeecg.common.api.vo.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.connection.PoolException;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;

/**
* 异常处理器
*
* @Author scott
* @Date 2019
*/
@RestControllerAdvice
@Slf4j
public class JeecgBootExceptionHandler {
@Value("${spring.servlet.multipart.max-file-size}")
private String maxFileSize;

/**
* 处理自定义异常
*/
@ExceptionHandler(JeecgBootException.class)
public Result<?> handleJeecgBootException(JeecgBootException e) {
log.error(e.getMessage(), e);
return Result.error(e.getMessage());
}

/**
* 处理自定义异常
*/
@ExceptionHandler(JeecgBoot401Exception.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Result<?> handleJeecgBoot401Exception(JeecgBoot401Exception e) {
log.error(e.getMessage(), e);
return new Result("401", e.getMessage());
}

/**
* @description: 处理自定义异常-万序系统异常
* @author: luo_jj
* @date: 2022/3/25 11:33
* @param e:
* @return: org.jeecg.common.api.vo.Result<?>
*/
@ExceptionHandler(VanxSoftException.class)
public Result<?> handleVanxSoftException(VanxSoftException e) {
log.error(e.getMessage(), e);
return Result.error(e.getResultCodeEnum());
}

@ExceptionHandler(NoHandlerFoundException.class)
public Result<?> handlerNoFoundException(Exception e) {
log.error(e.getMessage(), e);
return Result.error("404", "路径不存在,请检查路径是否正确");
}

@ExceptionHandler(DuplicateKeyException.class)
public Result<?> handleDuplicateKeyException(DuplicateKeyException e) {
log.error(e.getMessage(), e);
return Result.error("数据库中已存在该记录");
}

@ExceptionHandler({UnauthorizedException.class, AuthorizationException.class})
public Result<?> handleAuthorizationException(AuthorizationException e) {
log.error(e.getMessage(), e);
return Result.noauth("没有权限,请联系管理员授权");
}

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error(e.getMessage(), e);
return Result.error("操作失败," + e.getMessage());
}

/**
* @param e
* @return
* @Author 政辉
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
StringBuffer sb = new StringBuffer();
sb.append("不支持");
sb.append(e.getMethod());
sb.append("请求方法,");
sb.append("支持以下");
String[] methods = e.getSupportedMethods();
if (methods != null) {
for (String str : methods) {
sb.append(str);
sb.append("、");
}
}
log.error(sb.toString(), e);
return Result.error("405", sb.toString());
}

/**
* spring默认上传大小100MB 超出大小捕获异常MaxUploadSizeExceededException
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public Result<?> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) {
log.error(e.getMessage(), e);
return Result.error(String.format("文件大小超出%s限制, 请压缩或降低文件质量! ", maxFileSize));
}

@ExceptionHandler(DataIntegrityViolationException.class)
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
return Result.error("字段太长,超出数据库字段的长度");
}

@ExceptionHandler(PoolException.class)
public Result<?> handlePoolException(PoolException e) {
log.error(e.getMessage(), e);
return Result.error("Redis 连接异常!");
}

}

测试

编写 TestController 测试

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
package org.jeecg.modules.exception.controller;

import com.alibaba.fastjson.JSONObject;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.enums.ResultCodeEnum;
import org.jeecg.common.exception.VanxSoftException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
* @description: 全局异常处理
* @author: luo_jj
* @date: 2022年03月24日 17:45
*/
@RestController
@RequestMapping("/exception")
@Api(tags="全局异常处理")
@Slf4j
public class ExceptionController {

@ApiOperation("测试请求")
@RequestMapping(value = "/test", method = RequestMethod.POST)
public Result<JSONObject> testClientError(){
throw new VanxSoftException(ResultCodeEnum.SYSTEM_ERROR);
}
}

image-20220328151505665