背景

Spring 提供了 @RestControllerAdvice 用来实现 HTTP 协议的全局异常处理。在异常信息的处理上通常只返回特定的 Response 对象,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@RestControllerAdvice
public class RestExceptionResolver {

@ExceptionHandler(Exception.class)
public ResponseEntity<?> processException(Exception ex) {
BodyBuilder builder;
Response response;
ResponseStatus responseStatus =
AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
builder = ResponseEntity.status(responseStatus.value());
response = Response.buildFailure("500", responseStatus.reason());
} else {
builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
response = Response.buildFailure("500", ex.getMessage());
}
this.process(ex, response);
return builder.body(response);
}
}

作为基础框架,笔者就遇到项目A 要求返回 Response1 对象,项目B 要求返回 Response2 对象,这个时候,适配起来就很痛苦,例如下方的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString(callSuper = true)
@Data
public class Response extends DTO {

private static final long serialVersionUID = 1L;

private boolean success;

private String errCode; // 项目A 要求错误码是字符型

private String errMessage; // 项目A 要求用这个名字

private int code; // 项目B 要求错误码是整型

private String message; // 项目B 要求用这个名字
}

另外,@RestControllerAdvice 只适用于 Web 异常捕获,我们还要考虑其他组件的情况,例如 Dubbo 捕获 RPC 异常、Sentinel 组件触发限流、Spring Security 安全框架抛出认证异常。

目标

不需要修改基础框架,允许业务方自行扩展异常返回对象。

实现

Response 提炼为 Builder 模式,改为 ResponseBuilder.builder() 构建返回对象。

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

@ExceptionHandler(Exception.class)
public ResponseEntity<?> resolveException(Exception ex) {
BodyBuilder builder;
Object response;
ResponseStatus status = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
builder = ResponseEntity.status(status.value());
response = ResponseBuilder.builder().buildFailure("500", status.reason());
} else {
builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR);
response = ResponseBuilder.builder().buildFailure("500", ex.getMessage());
}
this.postProcess(ex);
return builder.body(response);
}
}

public interface ResponseBuilder<T> {

static ResponseBuilder<?> builder() {
// 尝试从业务项目获取自定义的 Spring Bean
ResponseBuilder<?> builder = ApplicationContextHelper.getBean(ResponseBuilder.class);
if (builder != null) {
return builder;
}
// 如果业务项目没有自定义 Bean,返回默认的 Builder
return DefalutResponseBuilder.getInstance();
}

T buildSuccess();

<Body> T buildSuccess(Body data);

T buildFailure(String errCode, String errMessage, Object... params);
}

public class DefalutResponseBuilder implements ResponseBuilder<Response> {

private static final DefaultResponseBuilder INSTANCE = new DefaultResponseBuilder();

private DefaultResponseBuilder() {}

public static DefaultResponseBuilder getInstance() {
return INSTANCE;
}

@Override
public Response buildSuccess() {
Response response = new Response();
response.setSuccess(true);
return response;
}

@Override
public <Body> Response buildSuccess(Body data) {
SingleResponse<Body> response = new SingleResponse<>();
response.setSuccess(true);
response.setData(data);
return response;
}

@Override
public Response buildFailure(String errCode, String errMessage, Object... params) {
Response response = new Response();
response.setSuccess(false);
response.setErrCode(errCode);
response.setErrMessage(MessageFormatter.arrayFormat(message, placeholders).getMessage());
return response;
}
}

@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString(callSuper = true)
@Data
public class Response extends DTO {

private static final long serialVersionUID = 1L;

private boolean success;

private String errCode;

private String errMessage;
}

业务方觉得 Response 不能满足需求,重新定义了新对象,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString(callSuper = true)
@Data
public class CustomResponse {

private static final long serialVersionUID = 1L;

private boolean success;

private int code; // 要求错误码是整型

private String message; // 前端要求用这个名字
}

创建 CustomResponseBuilder 包装 CustomResponse 对象,并标记 @Component 注解,放入 Spring Bean 管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
public class CustomResponseBuilder implements ResponseBuilder<CustomResponse> {

@Override
public CustomResponse buildSuccess() {
CustomResponse response = new CustomResponse();
response.setSuccess(true);
return response;
}

@Override
public <Body> CustomResponse buildSuccess(Body data) {
// 略
}

@Override
public CustomResponse buildFailure(int code, String message, Object... params) {
CustomResponse response = new CustomResponse();
response.setSuccess(false);
response.setCode(code);
response.setMessage(MessageFormatter.arrayFormat(message, placeholders).getMessage());
return response;
}
}

上述已提到 ResponseBuilder.builder() 优先查找 Spring Bean,所以 CustomResponseBuilder 覆盖了框架内置的 DefaultResponseBuilder 类,全局异常捕获器返回结果时,就能返回业务方自定义的 CustomResponse 对象,这样,不需要改动框架,就能满足业务需求。

产出

根据这个思路,我们分别实现了 Web 异常、Dubbo 异常、Sentinel 限流、Security 认证等各种场景的异常处理机制,业务方只需要自行创建 ResponseBuilder 扩展自己的返回对象即可,不需要修改框架。

本文涉及的代码完全开源,感兴趣的伙伴可以查阅 eden-spring-framework