参数校验

参数校验

捡破烂的诗人 2,168 2023-03-01
  • 联系方式:1761430646@qq.com
  • 编写时间:2023年3月1日21:36:14
  • 博客地址:www.zeroeden.cn
  • 菜狗摸索,有误勿喷,烦请联系

前言

  • 由于网络传输的不可靠性,以及前端数据控制的可篡改性,

  • 即使在前端对数据进行校验的情况下,后端的参数校验是必须的,避免用户绕过浏览器直接通过一些Http 工具直接向后端请求一些违法数据,防止脏数据落到数据库中

  • 应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的

  • 在一个正常的Web项目中,我们想到的做参数校验的方式

  • 最简单无脑的无非就是在各个接口方法中写一堆屎山的if-else代码出来做校验,诸如下图所示

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
        private static final Pattern PHONE_PATTERN = Pattern.compile("^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$");
        private static final Pattern EMAIL_PATTERN = Pattern.compile("^\\\\s*\\\\w+(?:\\\\.{0,1}[\\\\w-]+)*@[a-zA-Z0-9]+(?:[-.][a-zA-Z0-9]+)*\\\\.[a-zA-Z]+\\\\s*$");
    
    
        @PostMapping("/add")
        public Result addUser( @RequestBody User user) throws IllegalArgumentException {
            if(StringUtils.isEmpty(user.getId()) ){
                throw new IllegalArgumentException("id不能为空");
            }
            if(StringUtils.isEmpty(user.getName())){
                throw new IllegalArgumentException("name不能为空");
            }
            if(Objects.isNull(user.getAge())){
                throw new IllegalArgumentException("age不能为空");
            }
            if(StringUtils.isEmpty(user.getEmail())){
                throw new IllegalArgumentException("email不能为空");
            }
            if(StringUtils.isEmpty(user.getSex())){
                throw new IllegalArgumentException("sex不能为空");
            }
            if(StringUtils.isEmpty(user.getPhone())){
                throw new IllegalArgumentException("phone不能为空");
            }
            if(user.getId().length() < 6 || user.getId().length() > 7){
                throw new IllegalArgumentException("id长度非法");
            }
            if( user.getAge() < 18 || user.getAge() > 65){
                throw new IllegalArgumentException("age取值非法");
            }
            if(!EMAIL_PATTERN.matcher(user.getEmail()).find()){
                throw new IllegalArgumentException("email格式错误");
            }
            if(!user.getSex().equals("M") || !user.getSex().equals("F")){
                throw new IllegalArgumentException("sex取值非法");
            }
            if(!PHONE_PATTERN.matcher(user.getPhone()).find()){
                throw new IllegalArgumentException("phone格式错误");
            }
            log.info("增加用户");
            return Result.SUCCESS();
        }
    }
    
  • ​ 在实际开发中,如果通过这种屎山代码的方式去做参数校验,无疑是自己给自己找麻烦

  • 先来理清几个概念

    • JSR-303JavaEE 6中的一个子规范,又称作为Bean Validation,主要是提供了一些针对Java Bean字段的校验注解,如@NotNull@Min@Max但没有提供实现

      • 百度百科:JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
      • [JSR-303文档](JSR 303: Bean Validation)
    • JSR-349JSR-303其升级版本,添加了一些新特性

    • Hibernate Validation是对这个规范的实现(这个跟ORM框架没半毛钱关系),并在它的基础上新增了一些校验注解,如@Email@Length

    • 为了给开发者提供便捷,Spring也全面支持JSR-303JSR-349规范,对Hibernate Validation进行二次封装,在SpringMVC模块中添加了自动校验机制

  • 下面通过Demo来演示如何在SpringBoot项目中使用【参数校验】


使用

依赖问题

  • SpringBoot版本小于2.3.x时,spring-boot-starter-web会自动传入hibernate-validator及相关依赖

  • 如果版本大于2.3.x,spring-boot-starter-webhibernate-validator删减掉了

  • spring-boot-starter-web之所以把hibernate-validator删减掉,是因为把校验包也封装成了独立的一个starter组件(内部包含了必要的数据绑定组件),所以也可以直接引入这个,比较方便

校验注解

  • 下面列出的校验注解都是通过源码上的注释直译过来的

JSR-303 包含的注解

  • JSR-303文档

  • 表格展示

    注解 说明
    @Null 被注解元素必须为 null(支持任何类型)
    @NotNull 被注解元素必须不为 null(支持任何类型)
    @AssertTrue 被注解元素必须为 true 或 null (只支持布尔类型)
    @AssertFalse 被注解元素必须为 fasle 或 null (只支持布尔类型)
    @Min(value) 被注解元素必须 >= value 或为 null
    (支持 BigDecimal,BigInteger,byte, short, int, long和它们各自的封装类型)
    注意:由于舍入错误,float 和 double 不受支持
    @Max(value) 被注解元素必须 <= value 或为 null
    (支持 BigDecimal,BigInteger,byte, short, int, long和它们各自的封装类型)
    注意:由于舍入错误,float 和 double 不受支持
    @DecimalMin(value) 被注解元素必须 >= value 或为 null
    (支持 BigDecimal,BigInteger,CharSequence,byte, short, int, long和它们各自的封装类型)
    注意:由于舍入错误,float 和 double 不受支持
    @DecimalMax(value) 被注解元素必须 <= value 或为 null
    (支持 BigDecimal,BigInteger,CharSequence,byte, short, int, long和它们各自的封装类型)
    注意:由于舍入错误,float 和 double 不受支持
    @Size(max= ,min= ) 被注解元素的大小/长度必须在此指定范围中(左右封闭区间) 或为 null
    (支持CharSequence, Collection, Map, Array)
    @Digits(interger,frqction) 被注解元素的数值类型格式检查,null 被认为是合理的
    interger 指定整数部分的最大长度,fraction 指定小数部分的最大长度
    (支持byte, short, int, long和它们各自的封装类型)
    @Past 被注解元素必须是过去的某个瞬间,日期,时间 或者为 null
    (Now 是由附加到Validator或ValidatorFactory的ClockProvider定义的 ,
    默认的ClockProvider根据虚拟机定义当前时间)
    @Future 被注解元素必须是过去的某个瞬间,日期,时间 或为 null
    @Pattern(regex= ,flag = ) 被注解元素必须符合指定的正则表达式 或为 null

hibernate-validator 扩展的注解

  • Hibernate Validator 6.2.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide (jboss.org)

  • 表格形式

    注解 说明
    @Email 被注解元素必须符合电子邮件格式 或为 null(只支持CharSequence)
    @NotBlank 被注解元素必须不为 null且 trim() 后长度 > 0(只支持CharSequence)
    @NotEmpty 被注解元素不能为null 且长度/大小 > 0(支持CharSequence, Collection, Map, Array)
    @Length(min=,max=) 被注解元素长度在min 和 max 之间(只支持字符串,左右封闭区间)
    @Range(min=,max=) 被注解元素大小必须在min 和 max之间(支持数值类型或装载数值的字符串,左右封闭区间)
  • 注意:这里只列举了常用的几个校验注解

  • 特别注意下@NotNull,@NoeBlank,@NotEmpty的区别

  • 特别注意下@Size,@Length,@Range的区别

项目搭建

  1. 实体类以及参数简要要求

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
        /**
         * id 长度在 6 - 12 之间
         */
        @NotNull(message = "id不能为空")
        @Length(max = 12, min = 6,message = "id长度必须位于[6-12]")
        private String id;
    
        /**
         * 不能为空
         */
        @NotBlank(message = "姓名不能为空")
        private String name;
    
        /**
         * 年龄必须处于 18 ~ 65 这个区间
         */
        @NotNull(message = "年龄不能为空")
        @Range(max = 65, min = 18, message = "年龄取值范围必须为[18,65]")
        private Integer age;
    
        /**
         * 要符合邮箱格式
         */
    
        @NotNull(message = "邮箱地址不能为空")
        @Email(message = "邮箱格式错误")
        private String email;
    
        /**
         * 取值只能为 M,F
         */
        @NotNull(message = "性别不能为空")
        @Pattern(regexp = "^[MF]$",message = "性别只能取值M,F")
        private String sex;
    
        /**
         * 要符合电话号码格式
         */
        @NotNull(message = "手机号不能为空")
        @Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",
        message = "手机号码格式错误")
        private String phone;
    }
    
  2. 统一结果封装类

    @Data
    @NoArgsConstructor
    public class Result {
        /**
         * 是否成功
         */
    
        private boolean success;
        /**
         * 返回码
         */
        private Integer code;
        /**
         * 返回信息
         */
        private String message;
        /**
         * 返回信息
         */
        private Object data;
    
        public Result(ResultCode code) {
            this.success = code.success();
            this.code = code.code();
            this.message = code.message();
        }
    
        public Result(ResultCode code, Object data) {
            this.success = code.success();
            this.code = code.code();
            this.message = code.message();
            this.data = data;
        }
    
        public Result(Integer code, String message, boolean success) {
            this.code = code;
            this.message = message;
            this.success = success;
        }
    
        public static Result SUCCESS() {
            return new Result(ResultCode.SUCCESS);
        }
        public static Result SUCCESS(Object data) {
            return new Result(ResultCode.SUCCESS, data);
        }
    
        public static Result ERROR() {
            return new Result(ResultCode.SERVER_ERROR);
        }
        public static Result ERROR(Object data) {
            return new Result(ResultCode.SERVER_ERROR, data);
        }
    
        public static Result FAIL() {
            return new Result(ResultCode.FAIL);
        }
    }
    
  3. 状态码常量自定义(瞎编即可)

    public enum ResultCode {
        SUCCESS(true, 10000, "操作成功!"),
        //---系统错误返回码-----
        FAIL(false, 10001, "操作失败"),
        UNAUTHENTICATED(false, 10002, "您还未登录"),
        TOKEN_LOSE_EFFICACY(false, 10003, "登录凭证已失效!"),
        UNAUTHORISE(false, 10004, "权限不足"),
    
        /**
         * 登录失败异常
         */
        USERNAME_PASSWORD_ERROR(false, 20001, "用户名或者密码错误"),
    
        REQUEST_PARARMETER_MISS(false, 30000, "请求参数缺失"),
        REQUEST_PARARMETER_ILLEGAL(false, 30001,"请求参数非法"),
        /**
         * 请求类型不支持
         */
        REQUEST_METHOD_NOT_SUPPORT(false, 40000, "不支持的请求类型"),
        SERVER_ERROR(false, 99999, "抱歉,系统繁忙,请稍后重试!");
        //---其他操作返回码----
        //操作是否成功
        boolean success;
        //操作代码
        int code;
        //提示信息
        String message;
        ResultCode(boolean success, int code, String message) {
            this.success = success;
            this.code = code;
            this.message = message;
        }
    
        public boolean success() {
            return success;
        }
    
        public int code() {
            return code;
        }
    
        public String message() {
            return message;
        }
    
  4. 统一异常处理

    @RestControllerAdvice
    public class BaseExceptionHandler {
        /**
         * 通用自定义异常捕获
         *
         * @return
         */
        @ExceptionHandler(value = CommonException.class)
        public Result commonException(CommonException exception) {
            if (exception.getMessage() != null && exception.getMessage().equals(ResultCode.REQUEST_METHOD_NOT_SUPPORT.message())) {
                // 不支持的请求方法类型
                return new Result(ResultCode.REQUEST_METHOD_NOT_SUPPORT);
            }
            if (exception.getMessage() != null) {
                // 给定异常信息
                return new Result(10001, exception.getMessage(), false);
            }
            // 请求失败
            return new Result(ResultCode.FAIL);
        }
        /**
         * 请求参数不符合异常
         *
         * @return
         */
        @ExceptionHandler(value = IllegalArgumentException.class)
        public Result illegalArgumentException (IllegalArgumentException e) {
            return new Result(REQUEST_PARARMETER_ILLEGAL.code(),e.getMessage(), false);
        }
        @ExceptionHandler(value = BindException.class)
        public Result parameterBindFail(BindException e){
            return new Result(REQUEST_PARARMETER_ILLEGAL.code(),
                    e.getAllErrors().stream()
                            .map(ObjectError::getDefaultMessage)
                            .collect(Collectors.joining(";")),
                    false);
        }
        @ExceptionHandler(value = ConstraintViolationException.class)
        public Result parameterValidationFail(ConstraintViolationException e){
            return new Result(REQUEST_PARARMETER_ILLEGAL.code(), e.getConstraintViolations().stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(";")),
                    false);
        }
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public Result methodArgumentNotValid(MethodArgumentNotValidException e){
            return new Result(REQUEST_PARARMETER_ILLEGAL.code(),
                    e.getBindingResult().getAllErrors().stream()
                            .map(ObjectError::getDefaultMessage)
                            .collect(Collectors.joining(";")),
                    false);
    
        }
    
        /**
         * 服务器异常统一返回
         *
         * @return
         */
        @ExceptionHandler(value = Exception.class)
        public Result error(Exception e) {
            return new Result(ResultCode.SERVER_ERROR);
        }
    
    • 注意:这里的BindException,ConstraintViolationException,MethodArgumentNotValidException是使用时出现校验失败后抛出的几个异常,后面会讲到
  5. 一个简单的Controller

    @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {
        private static final Pattern PHONE_PATTERN = Pattern.compile("^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$");
        private static final Pattern EMAIL_PATTERN = Pattern.compile("^\\\\s*\\\\w+(?:\\\\.{0,1}[\\\\w-]+)*@[a-zA-Z0-9]+(?:[-.][a-zA-Z0-9]+)*\\\\.[a-zA-Z]+\\\\s*$");
        @PostMapping("/add")
        public Result addUser( @RequestBody @Validated User user) throws IllegalArgumentException {
    //        if(StringUtils.isEmpty(user.getId()) ){
    //            throw new IllegalArgumentException("id不能为空");
    //        }
    //        if(StringUtils.isEmpty(user.getName())){
    //            throw new IllegalArgumentException("name不能为空");
    //        }
    //        if(Objects.isNull(user.getAge())){
    //            throw new IllegalArgumentException("age不能为空");
    //        }
    //        if(StringUtils.isEmpty(user.getEmail())){
    //            throw new IllegalArgumentException("email不能为空");
    //        }
    //        if(StringUtils.isEmpty(user.getSex())){
    //            throw new IllegalArgumentException("sex不能为空");
    //        }
    //        if(StringUtils.isEmpty(user.getPhone())){
    //            throw new IllegalArgumentException("phone不能为空");
    //        }
    //        if(user.getId().length() < 6 || user.getId().length() > 7){
    //            throw new IllegalArgumentException("id长度非法");
    //        }
    //        if( user.getAge() < 18 || user.getAge() > 65){
    //            throw new IllegalArgumentException("age取值非法");
    //        }
    //        if(!EMAIL_PATTERN.matcher(user.getEmail()).find()){
    //            throw new IllegalArgumentException("email格式错误");
    //        }
    //        if(!user.getSex().equals("M") || !user.getSex().equals("F")){
    //            throw new IllegalArgumentException("sex取值非法");
    //        }
    //        if(!PHONE_PATTERN.matcher(user.getPhone()).find()){
    //            throw new IllegalArgumentException("phone格式错误");
    //        }
            System.out.println(user);
            log.info("增加用户");
            return Result.SUCCESS();
        }
    
        @PostMapping("/update")
        public Result updateById(User user){
            log.info("根据Id修改用户");
            return Result.SUCCESS();
        }
        @GetMapping("/getById")
        public Result getById1(int id){
            log.info("根据Id获取用户:{}",id);
            return Result.SUCCESS();
        }
    
        @DeleteMapping("/delete/{id}")
        public Result deleteById(@PathVariable("id") int id){
            log.info("根据id删除用户");
            return Result.SUCCESS();
        }
    }
    

简单使用

  • 在大部分情况下,请求参数携带主要分成两种形式
    • POSTPUT请求,使用requestBody传递参数
    • GETDELETE请求,使用ReqeustParam/PathVariable传递参数

RequstBody

  • 通常在接口方法中,我们会使用一个对象用来装载参数

  • 此时只要在对象加@Validated注解或者@Valid注解即可实现自动参数校验

  • 模拟请求

  • 此时如果出现检验失败,会抛出MethodArgumentNotValidExceptionSpring默认会将其转为400Bad Request)请求

  • 因为之前做了统一异常处理,所以现在返回的是标准的数据格式

  • 小结:

    • 在方法参数上加@Validated注解(位于org.springframework.validation.annotation包下)或者加@Valid注解(位于javax.validation包下)后面会提到这两个注解的区别,可以先注意下它们所在的包
    • 校验失败时抛出的是MethodArgumentNotValidException异常(位于org.springframework.web.bind包下)

RequestParam/PathVariable

  • 正常来讲,通过RequestParam/PathVariable来接受的参数数量都是比较少的

  • 此时将参数一个一个平铺在接口方法中,再加以自己想要的参数检验注解,最后在Controller类上表上注解@Validated即可

  • 模拟请求

  • 此时如果出现检验失败,会抛出ConstraintViolationExceptionSpring默认会将其转为500Internal Server Error)请求

  • 使用PathVariable接受参数时同理

  • 小结:

    • 在方法对应参数上加自己需要的校验注解
    • Controller类上标上注解@Validated(注意不能加Valid,否则不起作用的)
    • 检验失败时抛出的是ConstraintViolationException异常(位于javax.validation包下)
  • 当然也有接受参数数量较多时的情况,通常使用对象来接受

  • 而此时直接在参数对象加@Validated@Valid注解即可,如同【RequestBody/PathVariable】章节的用法一样

  • 而此时如果校验失败,抛出的异常为BindException(位于org.springframework.validation包下),Spring默认会将其转为400Bad Request)请求


统一异常处理

  • 从上面可知,当出现校验失败时抛出的异常类型有3种

  • 所以这也就是之前搭建项目时统一异常处理中捕获的那几个校验失败异常的由来


在其他组件上检验

  • 上述例子我们都是在Controller上进行的参数校验

  • 实际上我们还可以其他Spring组件的层面上进行参数校验

  • 比如说在Service组件中

    • 一定要在类上加注解@Validated(不能是@Valid
    • 在方法参数中进行对应校验注解(特别注意,如果是对对象进行校验的话,只能用@Valid,而不能像在Controller中进行参数校验时使用@Valid/@Validated都可)
  • 如下图所示

  • 参数不符要求访问时如下所示

    • 访问/user/demo1

    • 访问/user/demo2


手动检验

  • 上面的例子都是通过Spring来帮我们在接口方法处做参数自动校验的

  • 实际上也可以通过手动编码的方式来进行参数校验的


方式一

  • 用法也很简单,就是拿位于javax.validation包下的Validator来调用方法即可注意此时在接口方法的参数中,对象不用加@Valid/@Validated校验注解

    • 这样的话先当于是进入到接口方法内部后才开始进行【参数检验】
  • 参数不符要求时访问该接口


方式二

  • 直接在接口方法的参数列表中加多个BindingResult对象,然后在方法内部中判断是否检验成功与否即可

    • 这样的话相当于Spring帮我们做参数自动检验时,就算检验失败,也不会马上抛异常,而是继续执行方法,按照方法内部的逻辑手动控制如何处理
  • 参数不符要求时访问该接口

  • 特别注意:BindingResult一定要放在接受参数对象的后面,要不然会出现下面这种错误


分组检验

  • 在实际开发中,按照我们上述的例子,会通常出现一种情况

  • User类的id字段在新增时通常由后端生成或数据库表主键自增,而做修改时此字段必须存在,非null且符合我们的要求

  • 这也就是说在使用某个VO作为Controller接口方法接受参数时,其中的某个属性在新增时无检验要求,而在修改操作时需满足对应的检验规则

  • 面对这种场景,实际上最容易想到的是做多个VO,其属性完全一样,只是属性上的对应检验规则匹配各自的接口方法中参数要求即可

  • 这样也确实能实现效果,但不得不说这种做法确实很呆,容易造成类膨胀

  • 而实际上在JSR-303规范中,本身就考虑到了这一点

  • 在各个校验注解中,都提供了Class<?>[] groups() default { }用以作为区分不同组

  • 下面做个分组检验Demo

    • 定义不同接口用以作为分组值

    • 根据需要对类上字段进行针对性分组

      public class User {
      
          /**
           * id 长度在 6 - 12 之间
           */
          @NotNull(message = "id不能为空", groups = {UserUpdate.class})
          @Length(max = 12, min = 6,message = "id长度必须位于[6-12]",groups = {UserUpdate.class})
          private String id;
      
          /**
           * 不能为空
           */
          @NotBlank(message = "姓名不能为空",groups = {UserAdd.class})
          private String name;
      
          /**
           * 年龄必须处于 18 ~ 65 这个区间
           */
          @NotNull(message = "年龄不能为空",groups = {UserAdd.class})
          @Range(max = 65, min = 18, message = "年龄取值范围必须为[18,65]",groups = {UserAdd.class})
          private Integer age;
      
          /**
           * 要符合邮箱格式
           */
      
          @NotNull(message = "邮箱地址不能为空",groups = {UserAdd.class})
          @Email(message = "邮箱格式错误",groups = {UserAdd.class})
          private String email;
      
          /**
           * 取值只能为 M,F
           */
          @NotNull(message = "性别不能为空",groups = {UserAdd.class})
          @Pattern(regexp = "^[MF]$",message = "性别只能取值M,F",groups = {UserAdd.class})
          private String sex;
      
          /**
           * 要符合电话号码格式
           */
          @NotNull(message = "手机号不能为空",groups = {UserAdd.class})
          @Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$",
          message = "手机号码格式错误",groups = {UserAdd.class})
          private String phone;
      }
      
    • Controller中的接口方法处使用某一类进行参数接受时,根据需求填充分组值即可

    • 这样,在@Validated中指定了分组,那么其他不属于这一组的检验注解将会被忽略

  • 特别注意:使用分组检验时在Controller的接口方法参数列表处,只能使用@Validated注解(位于org.springframework.validation.annotation包下),@Valid注解(位于javax.validation包下)是无法填充分组值的,此时这个是起不了分组作用的

  • 在这里再特别提一点,就是在javax.validation.groups包下,有一个Default的接口

  • 可以间接的理解为所有的检验注解其实默认的分组值就是这个Default,当然,如果检验注解上你指定了别的Class,那么就不具有这个Defalut.Class了。(实际上检验注解上的groups()默认都是{},我这里就是为了表达一个类似的意思方便后面说)

  • 所以可以看到定义分组值时会有这样的一个写法

    • 简单来说就是作为分组值的接口都间接性的继承了Default接口
  • 假设现在的User类其字段检验规则如下(为演示简单化,这里就只关注两个字段,其他字段全部现注释掉)

  • 特别注意,name字段是没有指定任何groups值的

  • 现有接口方法如下,指明要使用ValidGroup.UserCurd.Update这个分组的检验注解

  • 按照当前的上下文环境理解,也就是此时像此接口发请求是不会对name做检验的

  • 但是实际访问时,其结果如下

  • 也就是这样的写法,实际上还是对name字段做了检验

  • 是不是感觉很奇怪?

  • 这就是我上文把Defalut间接理解成是所有参数检验注解的默认分组值的原由

  • 按照这样的理解,ValidGroup.UserCurd.Update间接性继承了Defalut接口

  • name字段上的@NotBlank注解没有指明groups值,也即是可以“理解为”指定的是默认的Default接口

  • 所以在接口方法处,指明的分组值是ValidGroup.UserCurd.Update.class,但由于间接性继承Default接口,所以也会把name字段做了校验源码还没看,通过结果反推的话,感觉它的逻辑处理应该是把某个分组值的所有直接的,间接的父类都当成同一组

    • JSR-303文档中有相关描述

  • 当然,实际上校验注解上的groups上的默认值为{},实际做参数检验处理时可能会达到这种说法的逻辑,所以这种说法便于理解就好了

  • 如果说我们的ValidGroup.UserCurd.Update.class没有间接性继承Defalut接口

  • 那么如果我们还想要把name字段也做参数检验的话

  • 就需要在原来的接口方法处,分组值再加多一个Default.class即可

  • 访问该接口,可以看到name字段的检验规则也生效了


嵌套检验

  • 在实际项目中,不管是DTOVO,还是Entity,所在类的字段类型除了String和基本数据类型及其封装类型外

  • 还可能由于1-11-n的关系,某个字段是一个对象,而此对象内部还要做【参数校验】

  • 这就是这个小章要讲的嵌套查询

  • 做法其实也很简单,直接在字段上加注解@Valid即可(这里可以配合分组检验一起使用)注意这里不能换成@Validated@Validated是不能标注在字段上的

  • 现有接口

  • 访问该接口方法结果如下所示(可以看到嵌套查询起到了作用)

  • 如果字段对象是个集合的话,同样会对里面的任何一个元素都会做检验


集合检验

  • Controller上的接口方法中,实际上是很有可能前端传过来一个Json数组过来

  • 我们通常都是通过List来进行接受,并期待对集合中的每一项元素同样都能做到【参数检验】

  • 但是实际上此时是不会做参数检验的

  • 现有接口方法

  • 携带不符要求的参数访问该接口(可以看到此时【参数检验】不生效)

  • 要想解决这个问题,其实很挺简单的

  • 这要借助上一个小章【嵌套检验】的做法

  • 我们可以自定义一个List集合,然后再把实际装在元素的List集合当成我们自定义集合的一个字段,并在其标注上@Valid注解即可

    • 这个Delegate相当于把字段list原有的方法(比如说size()isEmpty()这些)搬到我们自定义的ValidationList上来(具体可以参考lombok 实验性注解之 @Delegate)

  • 这样在接口方法处我们直接使用ValidationList接受即可

  • 携带不符要求参数访问该接口(可以看到此时校验规则生效了)

  • 当然,使用统一异常处理后,实际上获得结果为


组合检验注解

  • 可以看到我们之前对Userid的要求如下

  • 也就是对于某个字段的检验规则,可能需要同时捆绑多个检验注解才可满足

  • 而实际上我们可以去把这些已有的检验注解组合起来,形成一个捆绑体可同时满足多个检验规则

  • 比如下图所示

  • 这样的话在id字段上直接加这个注解即可

  • 假设现在有如下接口

  • 携带不符要求的id访问该接口时,结果如下图所示

  • 可以看到效果出来了


自定义检验注解

  • 在上述小章【组合检验注解】中,我们是拿已经存在的检验注解去缝纫起来形成新的可以满足多个检验规则的检验注解

  • 实际上,我们也可以完全去从0-1去构建一个新的检验注解

  • 先假设个环境,如果现在有一个城市枚举类如下

  • User类也新增了一个字段用来表示用户所在国家地区码

  • 现在呢,就有个要求,就是在新增用户时,其city值只能是在枚举类中City中出现的枚举值的地区码

  • 这时候就完全可以自定义一个全新的检验注解出来

  • 其检验注解的定义都大同小异的,如下图所示

  • 特别注意@Constraint(validatedBy = {CityValidator.class}),这里是指明我们现在自定义的这个City注解用的是哪个校验器来进行校验

    • 事实上,我现在还不太懂payload()和内置的接口List是干嘛用。。,只是照着写出来的
  • 下面我们来实现CityValidator校验器

    • 几个注意点:
      1. 必须实现ConstraintValidator<A extends Annotation, T>接口,其中第一个参数是要检验的注解类型,第二个是需要被检验的类型
      2. 这个接口下有两个方法,见名思意
        • initialize()是做初始化逻辑的,可以从City参数中获取一些值
        • isValid()是做具体检验逻辑的,第一个参数是被检验注解标注在该变量上的实际值,第二个参数可以拿到一些上下文环境的东西
  • 当定义完检验注解和实现检验器后,我们就可以使用了

  • 现有接口方法

  • 携带不存在的国家地区码访问该接口,结果如下图所示

  • 可以看到我们自定义的检验注解生效了


failFast

  • 从上述的各种携带不符要求参数的请求结果得知,诸如

  • 当出现多个不符检验规则的参数时,总是会把所有字段的参数检验都过一遍,然后才抛出异常

  • 而实际上可以通过一些配置,开启failFast模式,一旦遇到不符检验规时就直接可以抛异常,不必等到得所有字段都得检验完后

  • 如下图配置

  • 当携带多个非法参数时,结果如下图所示


@Valid 和 @Validated 区别

  • 特别注意,在RequestParam/PathVariable情况下进行参数检验时,一定要在类上加@Validated注解

TODO

  • 原理实现分析

总结

  • cao
  • 之前一直在拖拉,居然写出来了

参考

  1. Validation with Spring Boot - the Complete Guide
  2. Validating Form Input
  3. JSR-303
  4. Hibernate Validator
  5. JSR-000349 Bean Validation 1.1 Final Release for Evaluation (oracle.com)
  6. 徐靖峰-使用 spring validation 完成数据后端校验
  7. 杨业壮的专栏-SpringBoot参数检验
  8. 鼓楼丶-SpringBoot 实现各种参数校验
  9. 如何优雅的进行参数校验?

# Spring # 参数校验 # Hibernate validation # JSR-303 # Bean Validation