zoukankan      html  css  js  c++  java
  • JSR 303 进行后台数据校验

    一、JSR 303

    1、什么是 JSR 303?

      JSR 是 Java Specification Requests 的缩写,即 Java 规范提案。
      存在各种各样的 JSR,简单的理解为 JSR 是一种 Java 标准。
      JSR 303 就是数据检验的一个标准(Bean Validation (JSR 303))。
    参考:
      https://www.jianshu.com/p/554533f88370

    2、为什么使用 JSR 303?

      处理一段业务逻辑,首先要确保数据输入的正确性,所以需要先对数据进行检查,保证数据在语义上的正确性,再根据数据进行下一步的处理。
      前端可以通过 js 程序校验数据是否合法,后端同样也需要进行校验。而后端最简单的实现就是直接在业务方法中对数据进行处理,但是不同的业务方法可能会出现同样的校验操作,这样就出现了数据的冗余。为了解决这个情况,JSR 303 出现了。
      JSR 303 使用 Bean Validation,即在 Bean 上添加相应的注解,去实现数据校验。这样在执行业务方法前,都会根据注解对数据进行校验,从而减少自定义的校验逻辑,减少代码冗余。

    3、JSR 303 常见操作?

    (1)可以通过简单的注解校验 Bean 属性,比如 @NotNull、@Null 等。
    (2)可以通过 Group 分组自定义需要执行校验的属性。
    (3)可以自定义注解并指定校验规则。
    (4)支持基于 JSR 303 的实现,比如 Hibernate Validator(额外添加一些注解)。

    二、演示 JSR303 的简单使用

    1、构建一个 SpringBoot 项目用来演示

    (1)构建一个 SpringBoot 项目,以及使用 EasyCode 逆向生成相关的代码。
    参考地址:
      https://www.cnblogs.com/l-y-h/p/12781586.html
    模板代码地址:
      https://gitee.com/lyh-man/fast-template.git

    (2)工具使用详情:
      SpringBoot 2.2.6 + JDK 1.8 + mysql 1.8 搭建基本开发环境
      IDEA + EasyCode + Lombok 插件 逆向生成基本代码
      Postman 发送请求,测试接口

    2、未使用 JSR303 相关注解时

      没用 JSR 303 相关注解时,需要手动在业务方法里写处理数据的逻辑。
      修改 Controller ,简单测试一下未使用 JSR 303 相关注解时的做法。

    @RestController
    @RequestMapping("api")
    public class EmpController {
        @Resource
        private EmpService empService;
    
        @PostMapping("/emp")
        public Result createEmp(@RequestBody Emp emp) {
            if (emp.getId() == null || emp.getName() == null) {
                return Result.error().message("数据不存在");
            }
            return Result.ok().data("items", emp).message("数据插入成功");
        }
    }

    使用 postman 测试该接口,当 id 不存在时,会被检测到。

    id,name 都存在时,不会被捕获。

      这里只是简单的测试一下逻辑,真实的数据检测肯定比这复杂的多,然后每个方法都需要写不同的数据处理逻辑,这样就会造成数据的冗余。而使用 JSR303 的相关注解,就很简单,继续往下看。

    3、使用 JSR 303 相关注解处理逻辑

    (1)使用步骤:
    Step1:
      在相关的 Bean 上标注需要处理的注解,并指定需要提示的信息(若不指定,会从默认配置文件中读取默认的信息)。

    Step2:
      在相关的方法上,使用 @Valid 注解(或者 @Validated 指定组名)标记需要被校验的数据,否则会不生效。
    注意:
      检测到数据异常后,系统会向外抛出异常,如果做了统一异常处理,可以根据 postman 测试的结果,找到控制台打印出的 相应的异常,并处理。

    Step3:
      处理异常。使用 BindingResult 可以获取到检测结果,然后进行处理。
      也可以使用 全局统一异常 处理(@RestControllerAdvice 与 @ExceptionHandler),处理检测结果。
    注:
      统一异常处理参考:https://www.cnblogs.com/l-y-h/p/12781586.html#_label2

    (2)使用:
    Step1:
      在相关的 Bean 上标注注解,并写上指定信息。

    import lombok.Data;
    
    import javax.validation.constraints.NotNull;
    import java.io.Serializable;
    
    @Data
    public class Emp implements Serializable {
        private static final long serialVersionUID = 281903912367009575L;
    
        @NotNull(message = "id 不能为 null")
        private Integer id;
    
        @NotNull(message = "name 不能为 null")
        private String name;
        
        private Double salary;
        
        private Integer age;
        
        private String email;
    }

    Step2:
      修改 Controller 方法,使用 @Valid 注解标记需要检测的数据。

    @RestController
    @RequestMapping("api")
    public class EmpController {
        @Resource
        private EmpService empService;
    
        @PostMapping("/emp")
        public Result createEmp(@Valid @RequestBody Emp emp) {
            return Result.ok().data("items", emp).message("数据插入成功");
        }
    }

    Step3:
      使用 postman 测试一下。会抛出 MethodArgumentNotValidException 异常。

    控制台打印的信息:

    Step4:
      可以使用 BindingResult 去处理捕获到的数据并进行相关处理。

    @RestController
    @RequestMapping("api")
    public class EmpController {
        @Resource
        private EmpService empService;
    
        @PostMapping("/emp")
        public Result createEmp(@Valid @RequestBody Emp emp, BindingResult result) {
            if (result.hasErrors()) {
                Map<String, String> map = new HashMap<>();
                // 获取校验结果,遍历获取捕获到的每个校验结果
                result.getFieldErrors().forEach(item ->{
                    // 获取校验的信息
                    String message = item.getDefaultMessage();
                    String field = item.getField();
                    // 存储得到的校验结果
                    map.put(field, message);
                });
                return Result.error().message("数据不合法").data("items", map);
            }
            return Result.ok().data("items", emp).message("数据插入成功");
        }
    }

    使用 Postman 测试。

    Step5:
      通过上面的步骤,已经可以捕获异常、处理异常,但是每次都是在业务方法中手动处理逻辑,这样的实现,代码肯定会冗余。可以将其抽出,使用 统一异常处理,每次异常发生时,将其捕获。
      根据 Step3 可以看到会抛出 MethodArgumentNotValidException 异常,所以需要将其捕获。
      需要使用 @RestControllerAdvice 与 @ExceptionHandler。

    @RestControllerAdvice
    public class GlobalExceptionHandler {
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public Result handlerValidException(MethodArgumentNotValidException e) {
            logger.error(e.getMessage(), e);
            BindingResult result = e.getBindingResult();
            Map<String, String> map = new HashMap<>();
            // 获取校验结果,遍历获取捕获到的每个校验结果
            result.getFieldErrors().forEach(item ->{
                // 存储得到的校验结果
                map.put(item.getField(), item.getDefaultMessage());
            });
            return Result.error().message("数据校验不合法").data("items", map);
        }
    }

    相应的业务方法里,不需要再用 BindingResult 去处理数据了(即 Step2 的状态)。

    @RestController
    @RequestMapping("api")
    public class EmpController {
        @Resource
        private EmpService empService;
    
        @PostMapping("/emp")
        public Result createEmp(@Valid @RequestBody Emp emp) {
            return Result.ok().data("items", emp).message("数据插入成功");
        }
    }

    使用 Postman 测试。

    4、JSR 303 分组校验

    (1)为什么使用 分组校验?
      通过上面的过程,可以了解到单个方法的校验规则。
      如果出现多个方法,都需要校验 Bean,且校验规则不同的时候,怎么办呢?
      分组校验就可以去解决该问题,每个分组指定不同的校验规则,不同的方法执行不同的分组,就可以得到不同的校验结果。

    (2)基本认识
      JSR 303 的每个注解都默认具备三个属性:
        message 用来定义数据校验失败后的提示消息,默认读取配置文件的内容。
          全局搜索 ValidationMessages.properties,可以看到默认的信息。

        groups 用来定义分组,其是一个 class 数组,可以指定多个分组。

    String message() default "{javax.validation.constraints.NotNull.message}";
    
    Class<?>[] groups() default { };
    
    Class<? extends Payload>[] payload() default { };

    (3)使用分组步骤:
    Step1:
      定义一个空接口,用于指定分组,内部不需要任何实现。

    Step2:
      指定 注解时,通过 groups 指定分组。用于指定在某个分组条件下,才去执行校验规则。

    Step3:
      在相关的业务方法上,通过 @Validated 注解指定分组,去指定校验。
    注:
      使用分组校验后,Bean 注解上若不指定分组,则不会执行校验规则。

    (4)使用:
    Step1:
      创建分组接口。
      创建两个分组接口 AddGroup、UpdateGroup。
    其中:
      AddGroup 用于指定 添加数据 时的校验规则(比如:id、name 均不为 null)。
      UpdateGroup 用于指定 修改数据 时的校验规则(比如:name 不允许为 null)。

    Step2:
      给 Bean 添加注解,并指定分组信息。

    @Data
    public class Emp implements Serializable {
        private static final long serialVersionUID = 281903912367009575L;
    
        @NotNull(message = "id 不能为 null", groups = {AddGroup.class})
        private Integer id;
    
        @NotNull(message = "name 不能为 null", groups = {AddGroup.class, UpdateGroup.class})
        private String name;
    
        private Double salary;
    
        private Integer age;
    
        private String email;
    }

    Step3:
      在业务方法上,通过 @Validated 注解指定分组,去指定校验。
    如下例,定义两个方法,Post 请求会触发 createEmp 方法,Put 请求会触发 UpdateEmp 方法。

    @RestController
    @RequestMapping("api")
    public class EmpController {
        @Resource
        private EmpService empService;
    
        @PostMapping("/emp")
        public Result createEmp(@Validated({AddGroup.class}) @RequestBody Emp emp) {
            return Result.ok().data("items", emp).message("数据插入成功");
        }
    
        @PutMapping("/emp")
        public Result UpdateEmp(@Validated({UpdateGroup.class}) @RequestBody Emp emp) {
            return Result.ok().data("items", emp).message("数据插入成功");
        }
    }

    Step4:
      使用 Postman 测试,发送 Post 请求,触发 createEmp 方法,执行 AddGroup 校验规则。
      检测 id、name 是否合法。

    发送 Put 请求,触发 UpdateEmp 方法,执行 UpdateGroup 校验规则。
    只检测 name 是否合法。

    5、JSR 303 自定义校验注解

    (1)为什么使用自定义校验注解?
      上面的注解满足不了业务需求时,可以自定义校验注解,自定义校验规则。

    (2)步骤:
    Step1:
      需要自定义一个校验注解。
      可以创建一个 ValidationMessages.properties 用于保存默认的 message 信息。

    Step2:
      需要自定义一个校验器,即自定义校验规则。
      实现 ConstraintValidator 接口,并重写相关方法。
    注:
      initialize 方法用于初始化,可以获取 自定义的属性的值。
      isValid 方法用于校验,可以获取到实际的值,然后与自定义的属性值进行比较。

    Step3:
      将校验注解 与 校验器 关联起来。
      @Constraint(validatedBy = {TestValidConstraintValidator.class})

    (3)使用:
      如下例,自定义一个校验规则,判断数据长度是否合法。
      默认为 String 属性,当 String 为 Null 或者 长度大于 5 时,校验不通过。
      可以自定义 长度。
    Step1:
      自定义一个校验注解,@TestValid,用于判断一个值的长度是否合法。

    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.Documented;
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import static java.lang.annotation.ElementType.FIELD;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    /**
     * 用于判断一个值的长度是否合法
     */
    @Target({FIELD})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {TestValidConstraintValidator.class})
    public @interface TestValid {
        String message() default "{com.lyh.test.TestValid.message}";
    
        Class<?>[] groups() default { };
    
        Class<? extends Payload>[] payload() default { };
    
        /**
         * 返回一个长度
         * @return 默认为 5
         */
        int length() default 5;
    }

    配置文件内容:

    Step2:
      自定义一个校验器TestValidConstraintValidator, 用于检测值是否合法。

    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    /**
     * 实现 ConstraintValidator 接口,
     * 其中 ConstraintValidator 的泛型,一个需要指定自定义的注解,一个需要指定需要获取的值的类型。
     * 比如:
     *  ConstraintValidator<TestValid, String> 中
     *      TestValid   表示自定义注解
     *      String      表示获取的值的类型
     * 即定义规则,判断一个 String 的值的长度是否满足条件
     */
    public class TestValidConstraintValidator implements ConstraintValidator<TestValid, String> {
    
        /**
         * 用于保存自定义的(默认)长度
         */
        private int length;
    
        /**
         * 初始化方法,获取默认数据
         * @param constraintAnnotation 注解对象
         */
        @Override
        public void initialize(TestValid constraintAnnotation) {
            length = constraintAnnotation.length();
        }
    
        /**
         * 自定义校验规则,如果 String 为 Null 或者 长度大于 5,则校验失败(返回 false)
         * @param value 需要校验的值
         * @param context
         * @return true 表示校验成功,false 表示校验失败
         */
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            return value == null ? false : length > value.length();
        }
    
    }

    Step3:
      使用注解。

    @Data
    public class Emp implements Serializable {
        private static final long serialVersionUID = 281903912367009575L;
    
        @NotNull(message = "id 不能为 null", groups = {AddGroup.class})
        private Integer id;
    
        @TestValid(groups = {AddGroup.class})
        @NotNull(message = "name 不能为 null", groups = {AddGroup.class, UpdateGroup.class})
        private String name;
    
        private Double salary;
    
        private Integer age;
    
        @TestValid(length = 10, message = "值不能为 Null 且长度不超过 10", groups = {AddGroup.class})
        private String email;
    }

    使用 Postman 测试。
      name、email 都不存在时,会被捕获数据异常。

    name 数据不合法、email 数据合法时,name 会被捕获。

    三、JSR 303 相关注解

    1、空检查相关注解

    注解                      注解详情
    @Null                  被指定的注解元素必须为 Null
    @NotNull               任意类型,不能为 Null,但可以为空,比如空数组、空字符串。
    @NotBlank              针对字符串,不能为 Null,且去除前后空格后的字符串长度要大于 0。
    @NotEmpty              针对字符串、集合、数组,不能为 Null,且长度要大于 0。

     

     

     

    2、长度检查

    注解                      注解详情
    @Size                   针对字符串、集合、数组,判断长度是否在给定范围内。
    @Length                 针对字符串,判断长度是否在给定范围内。

     

    3、布尔值检查

    注解                      注解详情
    @AssertTrue             针对布尔值,用来判断布尔值是否为 true
    @AssertFalse            针对布尔值,用来判断布尔值是否为 false

     

    4、日期检查

    注解                      注解详情
    @Past                  针对日期,用来判断当前日期是否为 过去的日期
    @Future                针对日期,用来判断当前日期是否为 未来的日期

    5、数值检查

    注解                      注解详情
    @Max(value)         针对字符串、数值,用来判断是否小于等于某个指定值
    @Min(value)         针对字符串、数值,用来判断是否大于等于某个指定值

     

    6、其他

    注解                      注解详情
    @Pattern             验证字符串是否满足正则表达式
    @Email               验证字符串是否满足邮件格式
    @Url                 验证是否满足 url 格式

  • 相关阅读:
    数独高阶技巧入门之六——ALS
    数独高阶技巧入门之七——AIC & Nice Loop
    数独-链的理解顺序
    数独高阶技巧入门之三——Fish
    数独·唯一性技巧(Uniqueness)-2
    游戏剧本从入门到放弃
    Electron和NW.js入门笔记
    Spring boot Access-Control-Allow-Origin 问题解决
    Materialize -- 基于Material Design的主流前端响应式框架
    Ubuntu 安装 nvm
  • 原文地址:https://www.cnblogs.com/l-y-h/p/12797809.html
Copyright © 2011-2022 走看看