【盗心社区】统一接口返回和全局异常处理

回顾

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

返回对象 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;
    }
}
复制代码

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

image-20211120163259165.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 还没深入地看看。

若有不足之处请多指教。





这里参考了掘金众多文章

猜你喜欢

转载自juejin.im/post/7032609987083894814