• 【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("访问失败");
        }
    
  • 相关阅读:
    常见Oracle HINT的用法
    2011年的每一天是周几?
    TOM上关于JOIN跟+号的讨论
    关于index_ffs使用索引的一点问题.
    数据库中分组字符串相加
    国服3.3.5:死亡骑士全系DPS饰品收益评分
    WLK狂暴战怎么玩
    3.3萨满手册
    关于clob类型在函数中的处理。
    pivot_clause [Oracle SQL]
  • 原文地址:https://www.cnblogs.com/pandaNHF/p/15720140.html
Copyright © 2020-2023  润新知