0%

接口设计-统一异常及响应处理

1. 全局异常定义

首先自定义异常 code 和异常描述接口,并自定义业务异常编码枚举 BizErrorCode:

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
public interface ErrorCode {
String getCode();
String getDescription();
}

/**
* 业务自定义异常
*/
public enum BizErrorCode implements ErrorCode {

SUCCESS("SUCCESS", "成功"),
FAIL("FAIL", "失败"),
SYSTEM_ERROR("SYSTEM_ERROR", "系统异常"),
// ...
;

private final String code;
private final String description;

BizErrorCode(final String code, final String description) {
this.code = code;
this.description = description;
}

@Override
public String getCode() {
return this.code;
}

@Override
public String getDescription() {
return this.description;
}
}

自定义如下业务异常类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BizException extends RuntimeException {
protected final ErrorCode errorCode;

public BizException(ErrorCode errorCode) {
super(errorCode.getDescription());
this.errorCode = errorCode;
}

public BizException(ErrorCode errorCode, String detailedMessage) {
super(detailedMessage);
this.errorCode = errorCode;
}

public ErrorCode getErrorCode() {
return this.errorCode;
}
}

最后,需要将对 BizException 进行解析和二次处理,比如将异常封装为统一的响应 HttpResult 并响应给调用方。其中 handlerBizException 方法统一对抛出来的 BizException 进行处理并解析为 HttpResult 类响应给调用方。而 handlerException 则对所有异常进行兜底处理解析处理成 HttpResult 。

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
/**
* 全局异常处理器
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

/**
* BizException 类型异常处理器
* @param e BizException 异常
* @return HttpResult
*/
@ExceptionHandler(BizException.class)
@ResponseBody
public HttpResult handlerBizException(BizException e) {
HttpResult httpResult;
String message = e.getMessage();
if (StringUtils.isNotBlank(message)) {
httpResult = HttpResult.build(new JSONObject(), e.getErrorCode().getCode(), message);
} else {
httpResult = HttpResult.fail(new JSONObject(), e.getErrorCode());
}
return httpResult;
}

/**
* 兜底的异常处理器
* @param e Exception 异常
* @return HttpResult
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public HttpResult handlerException(Exception e) {
String message = e.getMessage();
return HttpResult.fail(new JSONObject(), message);
}
}

2. 全局响应封装

GlobalExceptionHandler 会对异常解析成 HttpResult,最后以 Json 串响应给调用方,其中 HttpResult 定义如下:

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
/**
* 通用响应
* @param <T>
*/
@Data
@ToString
public class HttpResult<T> implements Serializable {
// 响应数据
T data;
// 状态码
String code;
// 错误消息
String msg;
// 系统异常信息
String errorMsg;
// 调用链追踪id
String requestId;

private HttpResult(T data) {
this(data, BizErrorCode.SUCCESS.getCode(), "");
}

private HttpResult(T data, String code, String msg) {
this.errorMsg = "";
this.requestId = "";
this.data = data;
this.code = code;
this.msg = msg;
}

/**
* 构建响应
* @param data 响应体
* @param code 响应码
* @param msg 响应描述
* @param <T> 响应体类型
* @return HttpResult
*/
public static <T> HttpResult<T> build(T data, String code, String msg) {
return new HttpResult(data, code, msg);
}

/**
* 成功的响应
* @param data 响应体
* @param <T> 响应体类型
* @return HttpResult
*/
public static <T> HttpResult<T> succ(T data) {
return new HttpResult(data);
}

/**
* 错误的响应
* @param data 响应体
* @param err 异常 ErrorCode
* @param <T> 响应体类型
* @return HttpResult
*/
public static <T> HttpResult<T> fail(T data, ErrorCode err) {
return new HttpResult(data, err.getCode(), err.getDescription());
}

/**
* 错误的响应
* @param data 响应体
* @param msg 响应描述
* @param <T> 响应体类型
* @return HttpResult
*/
public static <T> HttpResult<T> fail(T data, String msg) {
return new HttpResult(data, BizErrorCode.FAIL.getCode(), msg);
}
}

如果需要对响应 HttpResult 进行额外的处理,比如加密等,可以在实现一个 ResponseAdvice 类并实现 ResponseBodyAdvice,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType mediaType, Class converterType,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
// 可以对响应体 body(实际就是 HttpResult)进行加密
return body;
}
}

ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行的对 response 的一些处理,可以实现对 response 数据的一些统一封装或者加密等操作。该接口一共有两个方法:

  • supports:判断是否要执行beforeBodyWrite方法,true为执行,false不执行 —— 通过supports方法,我们可以选择哪些类或哪些方法要对response进行处理,其余的则不处理。
  • beforeBodyWrite:对 response 处理的具体执行方法。

如下,我们验证一下响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("global/exception")
public class GlobalExceptionController {

@RequestMapping("error")
public void error() {
int i = 1 / 0;
}

@RequestMapping("bizError")
public void bizError() {
throw new BizException(BizErrorCode.SYSTEM_ERROR, "禁止访问");
}

@RequestMapping("right")
public HttpResult<QueryOrderResponse> right() {
QueryOrderResponse response = new QueryOrderResponse();
response.setOrderNo("456656565656565");
response.setUid("652326596");
return HttpResult.succ(response);
}
}

分别响应如下:

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
{
"data": {},
"code": "FAIL",
"msg": "/ by zero",
"errorMsg": "",
"requestId": ""
}

{
"data": {},
"code": "SYSTEM_ERROR",
"msg": "禁止访问",
"errorMsg": "",
"requestId": ""
}

{
"data": {
"orderNo": "456656565656565",
"uid": "652326596"
},
"code": "SUCCESS",
"msg": "",
"errorMsg": "",
"requestId": ""
}

3. 响应配置国际化

假若需要给异常响应配置国际化,重写 BizErrorCode 如下所示:

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
/**
* 业务自定义异常
*/
public enum BizErrorCode implements ErrorCode {

SUCCESS("SUCCESS", "成功"),
FAIL("FAIL", "失败"),
SYSTEM_ERROR("SYSTEM_ERROR", "系统异常"),
INVALID_PARAMS("INVALID_PARAMS", "无效参数", "10001"), // i18nCode 值为 10001
// ...
;

private final String code;
private final String description;
private final String i18nCode; // 增加国际化响应 code

BizErrorCode(final String code, final String description) {
this(code, description, "");
}

BizErrorCode(final String code, final String description, String i18nCode) {
this.code = code;
this.description = description;
this.i18nCode = i18nCode;
}

@Override
public String getCode() {
return this.code;
}

@Override
public String getDescription() {
return this.description;
}

public String getI18nCode() {
return i18nCode;
}

public static BizErrorCode getByCode(String code) {
for (BizErrorCode v : BizErrorCode.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
return null;
}
}

新增国际化资源文件,如下:

image-20220908213308171

其中 messages_en_US.properties 和 messages_zh_CN.properties 的内容为:

1
2
3
4
# messages_en_US.properties
10001=Invalid params
# messages_zh_CN.properties
10001=无效参数

为了让配置文件生效,同时还需要进行如下配置:

1
2
3
4
## application.yaml
spring:
messages:
basename: i18n/messages #配置国际化资源文件路径

假若需要对系统抛出来的 BizException 异常做国际化处理,修改 GlobalExceptionHandler 类的 handlerBizException 方法如下所示:

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
@ExceptionHandler(BizException.class)
@ResponseBody
public HttpResult handlerBizException(BizException e) {
HttpResult httpResult;
String message = e.getMessage();
// 判断当前系统语言环境是否为英文
if (MessageUtils.isEnLanguage()) {
message = "System Error";
BizErrorCode bizErrorCode = BizErrorCode.getByCode(e.getErrorCode().getCode());
if (bizErrorCode != null && StringUtils.isNotBlank(bizErrorCode.getI18nCode())) {
// 根据 i18nCode 获取英文配置
String i18nMessage = MessageUtils.getMessage(bizErrorCode.getI18nCode());
if (StringUtils.isNotBlank(i18nMessage)) {
message = i18nMessage;
}
}
}
if (StringUtils.isNotBlank(message)) {
httpResult = HttpResult.build(new JSONObject(), e.getErrorCode().getCode(), message);
} else {
httpResult = HttpResult.fail(new JSONObject(), e.getErrorCode());
}
return httpResult;
}

public class MessageUtils {

public static String getMessage(String code, Object... args) {
// Spring 提供了 MessageSource 来实现国际化操作,这里从 Spring 上下文获取 MessageSource 实例
MessageSource messageSource = SpringContextUtils.getBean(MessageSource.class);
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}

public static boolean isEnLanguage() {
Locale locale = LocaleContextHolder.getLocale();
return Locale.US.equals(locale);
}
}

// 实现从 Spring 上下文获取指定 Class 类型的实例
@Component
public class SpringContextUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtils.applicationContext = applicationContext;
}

public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
}

国际化语言的切换主要是因为有一个区域信息解析器在其作用,Spring 默认提供使用的是 LocaleResolver 的实现。下面我们自定义一个名为的 system-language 请求头参数,并根据其值是 CN 还是 EN 来切换不同的语言环境。定义类 CustomLocaleResolver 并实现 LocaleResolver 接口,并重写 resolveLocale 方法,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomLocaleResolver implements LocaleResolver {

private static final String LANGUAGE_EN = "EN";

@Override
public Locale resolveLocale(HttpServletRequest request) {
String language = Optional.ofNullable(request.getHeader("system-language")).orElse("CN");
if (LANGUAGE_EN.equalsIgnoreCase(language)) {
return Locale.US;
}
return Locale.SIMPLIFIED_CHINESE;
}

@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { }
}

最后将我们自定义的区域信息解析器 CustomLocaleResolver 交由 Spring 托管,如下:

1
2
3
4
5
6
7
@Configuration
public class LocaleResolverConfiguration {
@Bean
public LocaleResolver localeResolver() {
return new CustomLocaleResolver();
}
}

最后,我们根据请求头配置 system-language 来切换不同语言环境一次得到不用语言的响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping("i18n/error")
public void i18n() {
throw new BizException(BizErrorCode.INVALID_PARAMS);
}

// 中文环境响应
{
"data": {},
"code": "INVALID_PARAMS",
"msg": "无效参数",
"errorMsg": "",
"requestId": ""
}

// 英文环境响应
{
"data": {},
"code": "INVALID_PARAMS",
"msg": "Invalid params",
"errorMsg": "",
"requestId": ""
}
------ 本文结束------