Pre
Spring Boot2.x-11 使用@ControllerAdvice和@ExceptionHandler实现自定义全局异常
演进过程
我们搞个boot工程 ,来看下为什么以及如何来实现统一格式封装及高阶全局异常处理
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency> </dependencies>
版本V1
@RestController // 返回JSON @RequestMapping("/v1") public class ArtisanV1Controller { /** * 返回字符串 * * @return */ @GetMapping("/getString") public String getStr() { return "OOOOOOOK"; } /** * 返回自定义对象 * * @return */ @GetMapping("/getArtisan") public Artisan getArt() { Artisan artisan = new Artisan(); artisan.setJob("ArtisanJob"); artisan.setAge(18); return artisan; } /** * 接口异常 * * @return */ @GetMapping("/getMockError") public int getMockError() { int i = 1 / 0; return i; } }
分别测试下
这混乱的格式, 前端同学怎么想
版本2
Step1 约定统一返回格式
一个合格的标准的返回格式至少包含3部分:
- status 状态值
由后端统一定义各种返回结果的状态码
- message 描述
本次接口调用的结果描述
- data 数据
本次接口返回的数据
如果需要可以加入其他节点,比如在返回对象中添加了接口调用时间 (timestamp: 接口调用时间)
Step2 开发统一返回对象
package com.artisan.resp; import lombok.Data; /** * @author 小工匠 * @version 1.0 * @description: 公共结果 * @mark: show me the code , change the world */ @Data public class ResponseData<T> { /** * 结果状态 ,具体状态码参见ResponseCode */ private int status; /** * 响应消息 **/ private String message; /** * 响应数据 **/ private T data; /** * 接口请求时间 **/ private long timestamp; /** * 初始化,增加接口请求事件 */ public ResponseData() { this.timestamp = System.currentTimeMillis(); } /** * 成功 * * @param <T> * @return */ public static <T> ResponseData<T> success() { ResponseData<T> resultData = new ResponseData<>(); resultData.setStatus(ResponseCode.RC100.getCode()); resultData.setMessage(ResponseCode.RC100.getMessage()); return resultData; } /** * 成功 * * @param message * @param <T> * @return */ public static <T> ResponseData<T> success(String message) { ResponseData<T> resultData = new ResponseData<>(); resultData.setStatus(ResponseCode.RC100.getCode()); resultData.setMessage(message); return resultData; } /** * 成功 * * @param data * @param <T> * @return */ public static <T> ResponseData<T> success(T data) { ResponseData<T> resultData = new ResponseData<>(); resultData.setStatus(ResponseCode.RC100.getCode()); resultData.setMessage(ResponseCode.RC100.getMessage()); resultData.setData(data); return resultData; } /** * 失败 * * @param message * @param <T> * @return */ public static <T> ResponseData<T> fail(String message) { ResponseData<T> resultData = new ResponseData<>(); resultData.setStatus(ResponseCode.RC999.getCode()); resultData.setMessage(message); return resultData; } /** * 失败 * * @param code * @param message * @param <T> * @return */ public static <T> ResponseData<T> fail(int code, String message) { ResponseData<T> resultData = new ResponseData<>(); resultData.setStatus(code); resultData.setMessage(message); return resultData; } /** * 失败 * * @param <T> * @return */ public static <T> ResponseData<T> fail() { ResponseData<T> resultData = new ResponseData<>(); resultData.setStatus(ResponseCode.RC999.getCode()); resultData.setMessage(ResponseCode.RC999.getMessage()); return resultData;
Step3 约定接口状态码
package com.artisan.resp; import lombok.Getter; /** * @author 小工匠 * @version 1.0 * @description: 状态码集合 * @mark: show me the code , change the world */ public enum ResponseCode { /** * 操作成功 **/ RC100(100, "操作成功"), /** * 操作失败 **/ RC999(999, "操作失败"), /** * access_denied **/ RC403(403, "无访问权限,请联系管理员授予权限"), /** * access_denied **/ RC401(401, "匿名用户访问无权限资源时的异常"), /** * 服务异常 **/ RC500(500, "系统异常,请稍后重试"), ILLEGAL_ARGUMENT(3001, "非法参数"), INVALID_TOKEN(2001, "访问令牌不合法"), ACCESS_DENIED(2003, "没有权限访问该资源"), CLIENT_AUTHENTICATION_FAILED(1001, "客户端认证失败"), USERNAME_OR_PASSWORD_NOTMATCH(1002, "用户名或密码错误"); /** * 自定义状态码 **/ @Getter private final int code; /** * 自定义描述 **/ @Getter private final String message; ResponseCode(int code, String message) { this.code = code; this.message = message; } }
Step4 验证
package com.artisan.controller; import com.artisan.entity.Artisan; import com.artisan.resp.ResponseData; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 小工匠 * @version 1.0 * @description: 版本2 * @mark: show me the code , change the world */ @RestController @RequestMapping("/v2") public class ArtisanV2Controller { @GetMapping("/getString") public ResponseData<String> getStr() { return ResponseData.success("OOOOOOK"); } @GetMapping("/getArtisan") public ResponseData<Artisan> getArt() { Artisan artisan = new Artisan(); artisan.setJob("CodeMonkey"); artisan.setAge(18); return ResponseData.success(artisan); } @GetMapping("/getMockError") public ResponseData<Integer> getMockError() { int i = 1 / 0; return ResponseData.success(i); } }
好像部分实现了统一格式返回,确实也是有很多项目在Controller层通过ResponseData.success()对返回结果进行包装后返回给前端。
但是这个抛异常的这么玩还是不行呀? ------------------------> 全局异常处理
Step5 完善全局异常处理 @RestControllerAdvice + @ExceptionHandler
package com.artisan.resp; import com.artisan.exception.BaseException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; import java.util.stream.Collectors; /** * @author 小工匠 * @version 1.0 * @description: 全局异常处理 * @mark: show me the code , change the world */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * 默认全局异常处理。 * * @param e e * @return ResponseData */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseData<String> exception(Exception e) { log.error("兜底异常信息 ex={}", e.getMessage()); return ResponseData.fail(ResponseCode.RC500.getCode(), e.getMessage()); } /** * Assert异常 */ @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class}) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseData<String> exception(IllegalArgumentException e) { return ResponseData.fail(ResponseCode.ILLEGAL_ARGUMENT.getCode(), e.getMessage()); } /** * 抓取自定义异常 BaseException */ @ExceptionHandler(BaseException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ResponseData<String> exception(BaseException e) { return ResponseData.fail(e.getErrorCode(), e.getMessage()); } }
- @ExceptionHandler,统一处理某一类异常, 减少代码重复率和复杂度,比如要捕获自定义异常可以@ExceptionHandler(BusinessException.class)
- @ResponseStatus指定客户端收到的http状态码
重新验证下
全局异常处理器的必要行
- 避免try…catch,由全局异常处理器统一捕获
- 自定义异常,只能通过全局异常处理器来处理
- Validator参数校验器的时候,参数校验不通过会抛出异常,无法用try…catch捕获,只能使用全局异常处理器。
- …
。
版本3 (ResponseBodyAdvice)
V2版本有缺陷么?
我们不难发现每写一个接口都需要调用ResponseData.success()对结果进行包装 ,程序猿懒啊, 能不写吗
Step1 自定义ResponseBodyAdvice接口实现类
ResponseBodyAdvice的作用一般是用于拦截Controller方法的返回值,统一处理返回值/响应体, 加解密,签名等
package com.artisan.resp.v3; import com.artisan.resp.ResponseData; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; /** * @author 小工匠 * @version 1.0 * @description: 拦截Controller方法的返回值,统一处理返回值/响应体 * @mark: show me the code , change the world */ @RestControllerAdvice public class CustomResponseAdvice implements ResponseBodyAdvice<Object> { @Autowired private ObjectMapper objectMapper; @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @SneakyThrows @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { // 处理String类型 if (o instanceof String) { return objectMapper.writeValueAsString(ResponseData.success(o)); } // 若是统一返回类型,则不用再此封装 // if (o instanceof ResponseData) { // return o; // } return ResponseData.success(o); } }
- @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器
- 有一个地方对String做了特殊处理,因为如果Controller直接返回String ,SpringBoot是直接返回,所以我们需要手动转换成json
接入@RestControllerAdvice后, Controller就正常写就可以了,不用统一格式去包装了,如下
@RestController @RequestMapping("/v3") public class ArtisanV3Controller { @GetMapping("/getString") public String getStr() { return "OOOOOOK"; } @GetMapping("/getArtisan") public Artisan getArt() { Artisan artisan = new Artisan(); artisan.setJob("CodeMonkey"); artisan.setAge(18); return artisan; } @GetMapping("/getMockError") public int getMockError() { int i = 1 / 0; return i; } }
测试下
看到问题了吧,当我们同时启用统一标准格式封装功能ResponseAdvice和RestExceptionHandler全局异常处理器时,统一格式增强功能会给返回的异常结果再次封装,所以跟前端的接口响应又迷糊了
Step2 全局异常整合到返回的标准格式
因为全局异常处理器已经帮我们封装好了标准格式,我们只需要直接返回给客户端即可。
// 若是统一返回类型,则不用再此封装 if (o instanceof ResponseData) { return o; }
如果返回的结果是ResponseData对象,直接返回即可。
重新测试 ,
源码
https://github.com/yangshangwei/boot2