统一接口返回和全局异常处理

 2023-02-03
原文作者:lkzc19 原文地址:https://juejin.cn/post/7032609987083894814

回顾

回顾下第一次做学校实训时做前后端分离开发,那时候在网上找了很多关于怎么做 统一接口返回全局异常处理 的方法,做到后面发现,这两被我写的太臃肿了。

返回对象 HttpResult

    @Data
    public class HttpResult<T> {
    
        private Boolean success;
    
        private Integer code;
    
        private T data;
    
        private String msg;
    
        private HttpResult() {
            this.code = 200;
            this.success = true;
        }
    
        private HttpResult(T obj) {
            this.code = 200;
            this.data = obj;
            this.success = true;
        }
    
        private HttpResult(String msg) {
            this.success = false;
            this.code = 400;
            this.msg = msg;
        }
    
        private HttpResult(Integer code, String msg) {
            this.success = false;
            this.code = code;
            this.msg = msg;
        }
    
        private HttpResult(ResultCodeEnum resultCode) {
            this.success = false;
            this.code = resultCode.getCode();
            this.msg = resultCode.getMsg();
        }
    
        public static<T> HttpResult<T> success(){
            return new HttpResult();
        }
    
        public static<T> HttpResult<T> success(T data){
            return new HttpResult<T>(data);
        }
    
        public static<T> HttpResult<T> failure(String msg){
            return  new HttpResult<T>(msg);
        }
    
        public static<T> HttpResult<T> failure(Integer code, String msg){
            return  new HttpResult<T>(code, msg);
        }
    
        public static<T> HttpResult<T> failure(ResultCodeEnum resultCode){
            return  new HttpResult<T>(resultCode);
        }
    }

接着就是业务状态码枚举类 ResultCodeEnum 的定义,感觉这个可真是杂乱无章。

    @Getter
    public enum ResultCodeEnum {
    
        SUCCESS(200, "SUCCESS"),
    
        SERVER_ERROR(500, "server error"),
    
        USER_ALREADY_EXIST(1209,"用户已存在"),
        LOGIN_NOT_VALUE(1209,"空值"),
        TOKEN_ERROR(1201,"token无效"),
        EMAIL_OR_PASSWORD_ERROR(1209,"邮箱或密码错误"),
        NOT_LOGIN(1201,"未登入"),
    
        SURVEY_VALUE_NULL(1204,"问卷值为空"),
        SURVEY_ALREADY_DELETE(1208,"问卷已删除");
    
        private Integer code;
    
        private String msg;
    
        ResultCodeEnum(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }

还有自定义的业务异常处理类,太多了

202301012058390381.png

问题

  1. 返回对象的静态方法太多,因为感觉每种都有需要。
  2. 业务状态码太繁杂,且都没有规律。
  3. 业务异常类太多,和第2点差不多。
  4. 且每回都要调返回对象的方法,太麻烦。

解决

返回对象

  1. code:业务状态码。
  2. data:数据。
  3. msg:返回错误信息,成功没必要返回信息。

我取消了 success 属性,因为感觉这个属性没什么用, success 代表请求有没有成功,直接通过 code 判断就好了,规定好500就是失败,200就是成功,其他就是业务异常。

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @ToString
    public class ResponseModel {
    
        private int code;
    
        private Object data;
    
        private String msg;
    
        public static ResponseModel success(Object data) {
            ResponseModel responseModel = new ResponseModel();
            responseModel.setCode(200);
            responseModel.setData(data);
            return responseModel;
        }
    
        public static ResponseModel failure(ResponseMsgEnum responseMsgEnum) {
            ResponseModel responseModel = new ResponseModel();
            responseModel.setCode(responseMsgEnum.getCode());
            responseModel.setMsg(responseMsgEnum.getMsg());
            return responseModel;
        }
    }

只写了两个静态方法, success 是成功,成功返回200,加上数据,不需要返回信息。 failure 是失败,返回业务异常状态码,和错误信息,这些都定义在枚举类 ResponseMsgEnum 中,业务失败也就不需要返回数据了。

枚举类

枚举类之前就是各种细节都定义一个,实在是太繁杂了。比如上次做的是问卷系统,那么找不到某个问卷id、找不到某一个用户id、这样就要定义两个,但其实只要定义成一个404,找不到该资源就好了。前端哪里得到404,根据不同的场景做不同的处理就好了,没必要每个都是不一样的。

    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    public enum ResponseMsgEnum {
    
        SUCCESS(200, ""),
        CLIENT_ERROR(400, "客户端参数错误"),
        AUTH_FAILED(401,"身份验证失败"),
        NO_FOUND(404, "资源不存在"),
        SERVER_ERROR(500, "服务器错误");
    
        private int code;
    
        private String msg;
    }

统一接口返回

之前是每个接口都调一次返回对象的静态方法,将数据封装返回。这些都是属于重复性地工作,可以用 @RestControllerAdvice 注解,拦截下后端返回的数据,实现 ResponseBodyAdvice 接口对数据做一层包装再返回给前端。

    @RestControllerAdvice
    public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public boolean supports(MethodParameter returnType, 
                                Class<? extends HttpMessageConverter<?>> converterType) {
            return true;
        }
    
        @SneakyThrows
        @Override
        public Object beforeBodyWrite(Object body,
                                      MethodParameter returnType,
                                      MediaType selectedContentType,
                                      Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                      ServerHttpRequest request,
                                      ServerHttpResponse response) {
    
    
            if(body instanceof ResponseModel){  // 返回类型是否已经封装
                return body;
            } else if (body instanceof String){
                return objectMapper.writeValueAsString(ResponseModel.success(body));
            } else {
                return ResponseModel.success(body);
            }
        }
    }

返回值经过这里会先走 supports 方法判断是否要执行下面地 beforeBodyWrite 方法。这里还可以做一些其他更高级的操作,但是我暂时不需要,也没去研究,所以直接放行。

然后controller这边就可以直接返回,不需要调返回对象的方法。

    @PostMapping("login")
    public Map<String, String> login(@RequestBody LoginDTO loginDTO) {
        if (loginDTO.getMethod() == LoginDTO.METHOD_PSD) { // 通过密码登录
            return loginService.loginByPsd(loginDTO);
        } else { // 通过验证码登录
            return loginService.loginByOtp(loginDTO);
        }
    }

全局异常处理

把状态码合并其实就是减少异常种类,这里的代码其实与之前没什么不同,唯一要说的就是,这里捕获异常直接返回给前端是已经包装好的,但是也会经过上面的统一接口返回处理,所以上面就加了一个类型判断是否要包装。

    @RestControllerAdvice
    public class ExceptionAdvice {
    
        @ExceptionHandler({AuthFailedException.class})
        public ResponseModel handleAuthFailedException(AuthFailedException e) {
            return ResponseModel.failure(e.getResponseMsgEnum());
        }
    
        @ExceptionHandler({NoFoundException.class})
        public ResponseModel handleNoFoundException(NoFoundException e) {
            return ResponseModel.failure(e.getResponseMsgEnum());
        }
    
        // 参数校验
        @ExceptionHandler({MethodArgumentNotValidException.class})
        public ResponseModel handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
            return ResponseModel.failure(ResponseMsgEnum.CLIENT_ERROR);
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseModel handleException(Exception e) {
            e.printStackTrace();
            return ResponseModel.failure(ResponseMsgEnum.SERVER_ERROR);
        }
    }

业务异常类

上面的代码块前两个都是我自己定义的业务异常。

先弄个异常基类,本来两个属性就够了,但是为了方便点,加上了 ResponseMsgEnum ,可以直接传一个枚举值。而且之前捕获异常是在异常处理器那里写死的,捕获什么异常就传返回特定的值(在枚举类中定义好的)回去,现在是抛出异常传一个枚举类型回去。

    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @ToString
    public class BusinessException extends RuntimeException {
    
        private int code;
    
        private String msg;
    
        private ResponseMsgEnum responseMsgEnum;
    
        public BusinessException(ResponseMsgEnum responseMsgEnum) {
            this.responseMsgEnum = responseMsgEnum;
        }
    }

然后就是业务异常类,继承上面的基类。

    public class AuthFailedException extends BusinessException {
        public AuthFailedException(ResponseMsgEnum responseMsgEnum) {
            super(responseMsgEnum);
        }
    }

之后抛出异常,可以直接传一个枚举值。

    if (userPO == null) {
        throw new NoFoundException(ResponseMsgEnum.NO_FOUND);
    }
    if (!StringUtils.equals(loginDTO.getPassword(),userPO.getPassword())) {
        throw new AuthFailedException(ResponseMsgEnum.AUTH_FAILED);
    }

后记

还有好多如 ResponseBodyAdvice 还没深入地看看。

若有不足之处请多指教。


这里参考了掘金众多文章