【SpringBoot】统一返回对象和统一异常处理

为什么要统一异常

Java异常分为unchecked和checked,对于unchecked的那些异常一般都是代码写的有问题,比如你没有处理null对象,直接就用这个对象的方法或者属性了(NullPointException),或者是除0(ArithmeticException),或者是数组下标越界了(ArrayIndexOutOfBoundsException),这种的你要是能意识到try或者throw,那肯定不可能写错了。

但是对于checked,就是java要求我们处理的异常了,比如说SQLException , IOException,ClassNotFoundException,如果我们不做统一处理,那前端收到的会是一坨异常调用栈,非常恐怖,而且花样繁多,比如hibernate-validator的异常栈....

还有就是业务异常,这种的都是自定义的异常,一般都是基于RuntimeException改的。

所以,为了把这些乱七八糟的都统一起来,按照与前端约定好的格式返回,统一异常非常有必要。

为什么要统一返回值

不统一的话,返回值会五花八门,对于前端来说无法做一些统一处理,比如说统一通过状态为快速判断接口调用情况,接口调用失败原因获取每个接口都要自定义一个。

如果统一了,则前端可以写一个调用回调解析方法,就能快速获取接口调用情况了,十分便捷。如下代码:

{
    "state": false,
    "code": "-1",
    "data": "数据库中未查询到该学生",
    "timestamp": 1640142947034
}

前端可以通过code直接判断接口调用情况,通过data获取异常信息,或者是需要查询的数据。

如何实现统一异常

Spring为我们提供了一个注解:@ControllerAdvice

@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 处理自定义的业务异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public ResponseEntity businessExceptionHandler(BusinessException e){
        log.error("发生业务异常!原因是:{}",e.getMessage());
        return ResponseHelper.failed(e.getMessage());
    }
    
}

我们只需要在@ExceptionHandler这里写上想拦截处理的异常,就可以了,在该方法中,你可以把关于这个异常的信息获取出来自由拼接,然后通过统一返回格式返回给前端,方便前端处理展示。

如Hibernate-validator报的异常非常恐怖,中间叠了好几层:

Validation failed for argument [0] in public com.example.template.entity.ValidationTestVo com.example.template.controller.HibernateValidatorController.testValidation(com.example.template.entity.ValidationTestVo) with 4 errors: [Field error in object 'validationTestVo' on field 'userId': rejected value [122121]; codes [Size.validationTestVo.userId,Size.userId,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.userId,userId]; arguments []; default message [userId],5,1]; default message [用户ID长度必须在1-5之间]] [Field error in object 'validationTestVo' on field 'age': rejected value [213]; codes [Range.validationTestVo.age,Range.age,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.age,age]; arguments []; default message [age],200,0]; default message [年龄需要在0-200中间]] [Field error in object 'validationTestVo' on field 'email': rejected value [12112]; codes [Email.validationTestVo.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@6215aa90,.*]; default message [【邮箱】格式不规范]] [Field error in object 'validationTestVo' on field 'userName': rejected value [/;p[]; codes [Pattern.validationTestVo.userName,Pattern.userName,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.userName,userName]; arguments []; default message [userName],[Ljavax.validation.constraints.Pattern$Flag;@1bc38bc4,^([\\u4e00-\\u9fa5]{1,20}|[a-zA-Z\\.\\s]{1,20})$]; default message [名字只能输入中文、英文,且在20个字符以内]] 

这种的就算是返给前端,他们也得解半天,这个情况就可以通过统一拦截处理:

 /**
     * 处理参数校验失败异常
     * @param e
     * @return
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public ResponseEntity exceptionHandler(MethodArgumentNotValidException e){
        // 1.校验
        Boolean fieldErrorUnobtainable = (e == null || e.getBindingResult() == null
                || CollectionUtils.isEmpty(e.getBindingResult().getAllErrors()) || e.getBindingResult().getAllErrors().get(0) == null);
        if (fieldErrorUnobtainable) {
            return ResponseHelper.successful();
        }

        // 2.错误信息
        StringBuilder sb = new StringBuilder();
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        if(!CollectionUtils.isEmpty(allErrors)){
            for (Object fieldError_temp:allErrors) {
                FieldError fieldError = (FieldError) fieldError_temp;
                String objectName = fieldError.getObjectName();
                String field = fieldError.getField();
                String defaultMessage = fieldError.getDefaultMessage();
                sb.append(objectName).append(".").append(field).append(":").append(defaultMessage).append(";");
            }
        }
        // 3.return
        log.error("参数校验失败!原因是:{}",sb.toString());
        return ResponseHelper.failed(sb.toString());
    }

返回结果变为:

{
    "state": false,
    "code": "-1",
    "data": "validationTestVo.age:年龄需要在0-200中间;validationTestVo.userId:用户ID长度必须在1-5之间;validationTestVo.email:【邮箱】格式不规范;validationTestVo.userName:名字只能输入中文、英文,且在20个字符以内;",
    "timestamp": 1640145039385
}

如何实现自定义业务异常

原先抛出业务异常的时候,都是直接new RuntimeException,这样不是很友好,我们可以基于RuntimeException写一个BusinessException,主要优点是可以自定义异常返回信息内容格式。

public class BusinessException extends RuntimeException{
    private static final long serialVersionUID = 1L;
    protected IExceptionCode exCode;
    protected String[] params;

    public BusinessException(IExceptionCode code) {
        super(code.getError());
        this.exCode = code;
    }

    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(IExceptionCode code, String[] params) {
        super(code.getError());
        this.exCode = code;
        this.params = params;
    }

    public IExceptionCode getExCode() {
        return this.exCode;
    }

    protected String parseMessage(String message) {
        if (this.params == null) {
            return message;
        } else {
            String errorString = this.exCode.getError();

            for(int i = 0; i < this.params.length; ++i) {
                errorString = errorString.replace("{" + i + "}", this.params[i]);
            }

            return errorString;
        }
    }

    public String getMessage() {
        return this.exCode != null && !"".equals(this.exCode.getCode()) ? this.exCode.getCode() + ":" + this.parseMessage(this.exCode.getError()) : super.getMessage();
    }

}

其中的IExceptionCode,是规范自定义业务异常用的

public interface IExceptionCode {
    String getError();
    String getCode();
}

比如我们想写一个自定义异常枚举类:

public enum BusinessExCode implements IExceptionCode {
    DATABASE_NOT_FOUND("000001", "未在数据库中找到指定数据");


    private String code;
    private String error;

    BusinessExCode(String code, String error) {
        this.code = code;
        this.error = error;
    }

    @Override
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    @Override
    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

}

写例子:

    @Override
    public String testException() {
        if(在字典表中的应该有的数据==null){
            throw new BusinessException(BusinessExCode.DATABASE_NOT_FOUND);
        }
    }

测试结果:

{
    "state": false,
    "code": "-1",
    "data": "000001:未在数据库中找到指定数据",
    "timestamp": 1640161941876
}

如何实现统一返回值

这个主要要跟前端商量好,以便他们做统一处理。

写一个统一返回的模板类

@Data
public class RestResult {
    private boolean state;
    private String code;
    private Object data;
    private long timestamp;
}

再写一个便捷使用这个模板的类

public abstract class ResponseHelper {
    public static ResponseEntity<RestResult> successful() {
        return successful((Object) null);
    }

    public static ResponseEntity<RestResult> successful(Object data) {
        RestResult result = new RestResult();
        result.setState(true);
        result.setData(data);
        result.setTimestamp(System.currentTimeMillis());
        return new ResponseEntity(result, HttpStatus.OK);
    }

    public static ResponseEntity<RestResult> failed(String code, String message, HttpStatus httpStatus) {
        RestResult result = new RestResult();
        result.setState(false);
        result.setCode(code);
        result.setData(message);
        result.setTimestamp(System.currentTimeMillis());
        return new ResponseEntity(result, httpStatus);
    }

    public static ResponseEntity<RestResult> failed(String message) {
        return failed("-1", message, HttpStatus.INTERNAL_SERVER_ERROR);
    }


    public static ResponseEntity<RestResult> failed(BusinessException ex) {
        return failed(ex, HttpStatus.INTERNAL_SERVER_ERROR);
    }
    public static ResponseEntity<RestResult> failed(BusinessException ex, HttpStatus httpStatus) {
        RestResult result = new RestResult();
        result.setState(false);
        if (ex.getExCode() != null) {
            result.setCode(ex.getExCode().getCode());
        }
        result.setData(ex.getMessage());
        result.setTimestamp(System.currentTimeMillis());
        return new ResponseEntity(result, httpStatus);
    }
}

使用:

    @PostMapping(value ="/test2")
    @ResponseBody
    public ResponseEntity testValidation2(@RequestBody ValidationTestVo validationTestVo){
        return ResponseHelper.successful();
    }
    @PostMapping(value ="/test3")
    @ResponseBody
    public ResponseEntity testValidation3(@RequestBody ValidationTestVo validationTestVo){
        return ResponseHelper.successful(validationTestVo);
    }
    @PostMapping(value ="/test4")
    @ResponseBody
    public ResponseEntity testValidation4(@RequestBody ValidationTestVo validationTestVo){
        return ResponseHelper.failed("访问失败");
    }
posted @ 2021-12-22 16:36  胖达利亚  阅读(851)  评论(0编辑  收藏  举报