背景
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;
private String errMessage;
private int code;
private String message; }
|
另外,@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() { ResponseBuilder<?> builder = ApplicationContextHelper.getBean(ResponseBuilder.class); if (builder != null) { return 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。