• Validator (+BindResult) 进行校验 --针对业务层对传来的参数进行判断


    业务层经常做的一些判断:

    public String addUser(User user) {
         if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
             return "对象或者对象字段不能为空";
         }
         if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
             return "不能输入空字符串";
         }
         if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
             return "账号长度必须是6-11个字符";
         }
         if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
             return "密码长度必须是6-16个字符";
         }
         if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
             return "邮箱格式不正确";
         }
         // 参数校验完毕后这里就写上业务逻辑
         return "success";
    }

    Validator 可以非常方便的制定校验规则,并自动帮你完成校验。首先在入参里需要校验的字段加上注解,每个注解对应不同的校验规则,并可制定校验失败后的信息:

    @Data
    public class User {
        @NotNull(message = "用户id不能为空")
        private Long id;
    
        @NotNull(message = "用户账号不能为空")
        @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
        private String account;
    
        @NotNull(message = "用户密码不能为空")
        @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
        private String password;
    
        @NotNull(message = "用户邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        private String email;
    }

    校验规则和错误提示信息配置完毕后,接下来只需要在接口需要校验的参数上加上 @Valid 注解,并添加 BindResult 参数即可方便完成验证:

    @RestController
    @RequestMapping("user")
    public class UserController {
        @Autowired
        private UserService userService;
    
        @PostMapping("/addUser")
        public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
            // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
            for (ObjectError error : bindingResult.getAllErrors()) {
                return error.getDefaultMessage();
            }
            return userService.addUser(user);
        }
    }

    这样当请求数据传递到接口的时候 Validator 就自动完成校验了,校验的结果就会封装到 BindingResult 中去,如果有错误信息我们就直接返回给前端,业务逻辑代码也根本没有执行下去。

    此时,业务层里的校验代码就已经不需要了:

    eg:

    public String addUser(User user) {
         // 直接编写业务逻辑
         return "success";
    }

    现在可以看一下参数校验效果。我们故意给这个接口传递一个不符合校验规则的参数,先传递一个错误数据给接口,故意将 password这 个字段不满足校验条件:

    {
      "account": "12345678",
      "email": "123@qq.com",
      "id": 0,
      "password": "123" // 密码规则为"密码长度必须是6-16个字符"
    }

    再来看一下接口的响应数据:

    这样是不是方便很多?不难看出使用 Validator 校验有如下几个好处:

    1. 简化代码,之前业务层那么一大段校验代码都被省略掉了;

    2. 使用方便,那么多校验规则可以轻而易举的实现,比如邮箱格式验证,之前自己手写正则表达式要写那么一长串,还容易出错,用 Validator 直接一个注解搞定(还有更多校验规则注解,可以自行去了解哦);

    3. 减少耦合度,使用 Validator 能够让业务层只关注业务逻辑,从基本的参数校验逻辑中脱离出来。

    使用 Validator + BindingResult 已经是非常方便实用的参数校验方式了,在实际开发中也有很多项目就是这么做的,不过这样还是不太方便,因为你每写一个接口都要添加一个 BindingResult 参数,然后再提取错误信息返回给前端。

    这样有点麻烦,并且重复代码很多(尽管可以将这个重复代码封装成方法)。我们能否去掉 BindingResult 这一步呢?当然是可以的!

    Validator + 自动抛出异常

    我们完全可以将 BindingResult 这一步给去掉:

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid User user) {
        return userService.addUser(user);
    }

    去掉之后会发生什么事情呢?直接来试验一下,还是按照之前一样故意传递一个不符合校验规则的参数给接口。此时我们观察控制台可以发现接口已经引发 MethodArgumentNotValidException 异常了,

    控制台打印:

     其实这样就已经达到我们想要的效果了,参数校验不通过自然就不执行接下来的业务逻辑,去掉 BindingResult 后会自动引发异常,异常发生了自然而然就不会执行业务逻辑。也就是说,我们完全没必要添加相关 BindingResult 相关操作嘛。不过事情还没有完,异常是引发了,可我们并没有编写返回错误信息的代码呀,那参数校验失败了会响应什么数据给前端呢?我们来看一下刚才异常发生后接口响应的数据:

     返回的信息直接将整个错误对象相关信息都响应给前端了!这样就很难受,不过解决这个问题也很简单,就是我们接下来要讲的全局异常处理!

    全局异常处理

    参数校验失败会自动引发异常,我们当然不可能再去手动捕捉异常进行处理,不然还不如用之前 BindingResult 方式呢。又不想手动捕捉这个异常,又要对这个异常进行处理,那正好使用 SpringBoot 全局异常处理来达到一劳永逸的效果!

    1、首先,我们需要新建一个类,在这个类上加上 @ControllerAdvice 或 @RestControllerAdvice 注解,这个类就配置成全局处理类了(这个根据你的 Controller 层用的是 @Controller 还是 @RestController 来决定)。

    然后在类中新建方法,在方法上加上 @ExceptionHandler 注解并指定你想处理的异常类型。接着在方法内编写对该异常的操作逻辑,就完成了对该异常的全局处理!我们现在就来演示一下对参数校验失败抛出的 MethodArgumentNotValidException 全局处理:

    @RestControllerAdvice
    public class ExceptionControllerAdvice {
        @ExceptionHandler(MethodArgumentNotValidException.class) //==>指定异常
        public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
          // 从异常对象中拿到ObjectError对象
            ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
            // 然后提取错误提示信息进行返回
            return objectError.getDefaultMessage();
        }
    }

    校验后:

    {
        "timestamp": 1596009721019,
        "status": 400,
        "error": "Bad Request",
        "errors": [
            {
                "codes": [
                    "Size.user.password",
                    "Size.password",
                    "Size.java.lang.String",
                    "Size"
                ],
                "arguments": [
                    {
                        "codes": [
                            "user.password",
                            "password"
                        ],
                        "arguments": null,
                        "defaultMessage": "password",
                        "code": "password"
                    },
                    11,
                    6
                ],
                "defaultMessage": "密码长度必须是6-16个字符",
                "objectName": "user",
                "field": "password",
                "rejectedValue": "123",
                "bindingFailure": false,
                "code": "Size"
            }
        ],
        "message": "Validation failed for object='user'. Error count: 1",
        "path": "/nfdw-szpj-5G/userMapper/addUser"
    }

    只有密码不合格,返回的就是我们制定的错误提示信息!我们通过全局异常处理优雅的实现了我们想要的功能!以后我们再想写接口参数校验,就只需要在入参的成员变量上加上 Validator 校验规则注解,然后在参数上加上 @Valid 注解即可完成校验,校验失败会自动返回错误提示信息,无需任何其他代码!

    自定义异常

    全局处理当然不会只能处理一种异常,用途也不仅仅是对一个参数校验方式进行优化。在实际开发中,如何对异常处理其实是一个很麻烦的事情。传统处理异常一般有以下烦恼:

    1. 是捕获异常 try...catch 还是抛出异常 throws?

    2. 是在 Controller 层做处理还是在 Service 层处理又或是在 DAO 层做处理?

    3. 处理异常的方式是啥也不做,还是返回特定数据?如果返回又返回什么数据?

    4. 不是所有异常我们都能预先进行捕捉,如果发生了没有捕捉到的异常该怎么办?

    以上这些问题都可以用全局异常处理来解决,全局异常处理也叫统一异常处理,全局和统一处理代表什么?代表规范!规范有了,很多问题就会迎刃而解!全局异常处理的基本使用方式大家都已经知道了,我们接下来更进一步的规范项目中的异常处理方式:自定义异常。在很多情况下,我们需要手动抛出异常,比如在业务层当有些条件并不符合业务逻辑,我这时候就可以手动抛出异常从而触发事务回滚。那手动抛出异常最简单的方式就是 throw new RuntimeException("异常信息") 了,不过使用自定义会更好一些:

    1. 自定义异常可以携带更多的信息,不像这样只能携带一个字符串;

    2. 项目开发中经常是很多人负责不同的模块,使用自定义异常可以统一了对外异常展示的方式;

    3. 自定义异常语义更加清晰明了,一看就知道是项目中手动抛出的异常。

    我们现在就来开始写一个自定义异常:

    /**
     * Created by Alex on 2020-07-29
     */
    @Getter //只要getter方法,无需setter
    public class APIException extends RuntimeException {
        private int code;
        private String msg;
    
        public APIException() {
            this(1001, "接口错误");
        }
    
        public APIException(String msg) {
            this(1001, msg);
        }
    
        public APIException(int code, String msg) {
            super(msg);
            this.code = code;
            this.msg = msg;
        }
    }

    在刚才的全局异常处理类中记得添加对我们自定义异常的处理:

    // 自定义
        @ExceptionHandler(APIException.class)
        public String APIExceptionHandler(APIException e) {
            return e.getMsg();
        }

    这样就对异常的处理就比较规范了。当然还可以添加对 Exception 的处理,这样无论发生什么异常我们都能屏蔽掉然后响应数据给前端。不过建议最后项目上线时这样做,能够屏蔽掉错误信息暴露给前端,在开发中为了方便调试还是不要这样做。

    现在全局异常处理和自定义异常已经弄好了,不知道大家有没有发现一个问题,就是当我们抛出自定义异常的时候,全局异常处理只响应了异常中的错误信息 msg 给前端,并没有将错误代码 code 返回。这就要引申出我们接下来要讲的东西了:数据统一响应。

    数据统一响应

    现在我们规范好了参数校验方式和异常处理方式,然而还没有规范响应数据!比如我要获取一个分页信息数据,获取成功了呢自然就返回的数据列表;获取失败了后台就会响应异常信息,即一个字符串。就是说前端开发者压根就不知道后端响应过来的数据会是啥样的!所以,统一响应数据是前后端规范中必须要做的!

    自定义统一响应体

    统一数据响应,第一步肯定要做的就是我们自己自定义一个响应体类。无论后台是运行正常还是发生异常,响应给前端的数据格式是不变的!那么如何定义响应体呢?可以参考我们自定义异常类,也来一个响应信息代码 code 和响应信息说明 msg:

    @Getter
    public class ResultVO<T> {
        /**
         * 状态码,比如1000代表响应成功
         */
        private int code;
        /**
         * 响应信息,用来说明响应情况
         */
        private String msg;
        /**
         * 响应的具体数据
         */
        private T data;
    
        public ResultVO(T data) {
            this(1000, "success", data);
        }
    
        public ResultVO(int code, String msg, T data) {
            this.code = code;
            this.msg = msg;
            this.data = data;
        }
    }

    修改一下全局异常处理那的返回值:

    @RestControllerAdvice
    public class ExceptionControllerAdvice {
    //    全局异常
    //    @ExceptionHandler(MethodArgumentNotValidException.class)
    //    public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    //        // 从异常对象中拿到ObjectError对象
    //        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
    //        // 然后提取错误提示信息进行返回
    //        return objectError.getDefaultMessage();
    //    }
    
    //  自定义异常
        @ExceptionHandler(APIException.class)
        public ResultVO<String> APIExceptionHandler(APIException e) {
            // 注意哦,这里返回类型是自定义响应体
            return new ResultVO<>(e.getCode(), "响应失败", e.getMsg());
        }
    //  自定义全局异常
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
            // 注意哦,这里返回类型是自定义响应体
            return new ResultVO<>(1001, "参数校验失败", objectError.getDefaultMessage());
        }
    }

    此时如果发生异常了会响应什么数据给前端:

    响应码枚举--规范code码

    @Getter
    public enum ResultCode {
        SUCCESS(1000, "操作成功"),
        FAILED(1001, "响应失败"),
        VALIDATE_FAILED(1002, "参数校验失败"),
        ERROR(5000, "未知错误");
    
        private int code;
        private String msg;
    
        ResultCode(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    }

    然后修改响应体的构造方法,让其只准接受响应码枚举来设置响应码和响应信息:

    @Getter
    public class ResultVO<T> {
      public ResultVO(T data) {
            this(ResultCode.SUCCESS, data);
        }
    
        public ResultVO(ResultCode resultCode, T data) {
            this.code = resultCode.getCode();
            this.msg = resultCode.getMsg();
            this.data = data;
        }
    }

    然后同时修改全局异常处理的响应码设置方式:

    //  自定义异常
        @ExceptionHandler(APIException.class)
        public ResultVO<String> APIExceptionHandler(APIException e) {
            // 注意哦,这里传递的响应码枚举
            return new ResultVO<>(ResultCode.FAILED, e.getMsg());
        }
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
            // 注意哦,这里传递的响应码枚举
            return new ResultVO<>(ResultCode.VALIDATE_FAILED, objectError.getDefaultMessage());
        }

    这样响应码和响应信息只能是枚举规定的那几个,就真正做到了响应数据格式、响应码和响应信息规范化、统一化!

  • 相关阅读:
    Linux-进程描述(1)—进程控制块
    C++中的继承(2)类的默认成员
    Linux系统date命令的参数及获取时间戳的方法
    new/new[]和delete/delete[]是如何分配空间以及释放空间的
    golang垃圾回收
    golang内存分配
    go中的关键字-reflect 反射
    go中的关键字-go(下)
    go中的关键字-go(上)
    go中的关键字-defer
  • 原文地址:https://www.cnblogs.com/alomsc/p/13397864.html
Copyright © 2020-2023  润新知