美文网首页
spring-boot-validator使用汇总

spring-boot-validator使用汇总

作者: 艺超51iwowo | 来源:发表于2021-04-19 00:31 被阅读0次

    在写业务代码时,对参数的校验必不可少,基于Hibernate的Validator,可以非常便捷的实现参数校验。本文以SpringBoot为例,介绍一下如何使用Validator

    基本操作

    1、maven依赖

    首先需要引入validator的starter依赖

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    

    在该starter中,实际上最终会依赖Hibernate,

    image-20210418073511067
    2、实体添加校验参数

    对需要校验的实体对象,添加校验规则

    public class Student {
    
      @NotNull(message = "student id should not be null", groups = UpdateStudentBasicInfo.class)
      private Long id;
    
      @NotEmpty(message = "student name should not be empty")
      private String name;
    
      @NotNull(message = "student address should not be null")
      private String address;
    
      @NotEmpty(message = "student sex should not be empty")
      @Pattern(regexp = "male|female", message = "student sex should match male or female")
      private String sex;
    
      @NotEmpty(message = "student telephone should not be empty", groups = StudentAdvanceInfo.class)
      private String telephone;
    }
    

    上面使用了一些常用的规则。

    3、请求参数使用注解,开启校验

    实现一个controller,对实体进行操作,同时开启校验。备注: 这里仅仅是为了验证校验,不做实际业务处理。

    @PostMapping("/add")
    public ApiCommonResponse<String> addStudent(@Valid Student student) {
      return ApiCommonResponse.success("OK");
    }
    
    4、对校验结果进行处理

    方案1:直接在请求方法中,添加BindResult进行处理

    对请求方法进行修改,通过BindResult获取到校验结果,基于结果进行响应处理。

    @PostMapping("/add")
    public ApiCommonResponse<String> addStudent(@Valid Student student, BindingResult bindResult) {
      if (bindResult.hasErrors()) {
        log.error(bindResult.toString());
        return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), bindResult.toString());
      }
      return ApiCommonResponse.success("OK");
    }
    

    方案2:统一使用ExceptionHandler拦截器,对校验结果封装(推荐)

    此处主要是基于@RestControllerAdvice和@ExceptionHandler注解完成的,不过这里有一个需要注意的地方。

    那就是Validator对于json的请求和表单的请求,当违反校验时,抛出的异常不一致。

    @RestControllerAdvice
    public class ValidationHandlers {
    
      /**
         * 表单请求校验结果处理
         * @param bindException
         * @return
         */
      @ExceptionHandler(value = BindException.class)
      public ApiCommonResponse<String> errorHandler(BindException bindException) {
        BindingResult bindingResult = bindException.getBindingResult();
        return extractException(bindingResult.getAllErrors());
      }
    
      /**
         * JSON请求校验结果,也就是请求中对实体标记了@RequestBody
         * @param methodArgumentNotValidException
         * @return
         */
      @ExceptionHandler(value = MethodArgumentNotValidException.class)
      public  ApiCommonResponse<String> errorHandler(MethodArgumentNotValidException methodArgumentNotValidException) {
        BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
        return extractException(bindingResult.getAllErrors());
      }
    
      private  ApiCommonResponse<String> extractException(List<ObjectError> errorList) {
        StringBuilder errorMsg = new StringBuilder();
        for (ObjectError objectError : errorList) {
          errorMsg.append(objectError.getDefaultMessage()).append(";");
        }
        // 移出最后的分隔符
        errorMsg.delete(errorMsg.length() - 1, errorMsg.length());
        return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), errorMsg.toString());
      }
    }
    
    5、测试结果
    场景1:基于表单+ExceptionHandler的add
    image-20210418230114467
    场景2:请求参数中,直接使用BindResult
    @PostMapping("/addWithBindResult")
    @ResponseBody
    public ApiCommonResponse<String> addStudent(@Valid Student student, BindingResult bindResult) {
      if (bindResult.hasErrors()) {
        log.error(bindResult.toString());
        return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), bindResult.toString());
      }
      return ApiCommonResponse.success("OK");
    }
    
    image-20210418231215897

    高级用法

    1、对请求中的pathVariable和requestParam进行校验

    第一步: 对方法参数添加校验

    @GetMapping("/getStudentById/{id}")
    public ApiCommonResponse<String> getStudentById(@PathVariable("id") @Min(value = 10, message = "input id must great than 10") Long id) {
      return ApiCommonResponse.success("OK");
    }
    
    @GetMapping("/getStudentById")
    public ApiCommonResponse<String> getStudentByIdRequestParam(@RequestParam @Min(value = 10, message = "input id must great than 10") Long id) {
      return ApiCommonResponse.success("OK");
    }
    

    第二步:在类级别开启校验

    @Controller
    @Slf4j
    @Validated
    public class ValidatorDemoController 
    

    测试情况:

    image-20210418231510556

    可以看到此时,捕获的异常是ConstraintViolationException,所以可以通过新增ExceptionHandler,返回统一的错误响应。

    /**
         * pathVariable 和RequestParam的校验
         * @param constraintViolationException
         * @return
         */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public ApiCommonResponse<String> errorHandler(ConstraintViolationException constraintViolationException) {
      Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();
      String errorMsg = constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
      return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
    }
    
    image-20210418231958468
    2、group分组

    主要应用场景,是针对同一个实体,在不同的场景下,会有不同的校验规则。比如新增的时候,唯一标识id可以为空。但是在修改的时候,该值必须不为空。

    第一步:定义不同场景下的标记接口。

    // 新增场景
    public interface AddStudentBasicInfo {
    }
    
    // 修改场景
    public interface UpdateStudentBasicInfo {
    }
    

    第二步:对实体的校验规则,指名触发的group条件

    
    @NotNull(message = "student id should not be null", groups = UpdateStudentBasicInfo.class)
    private Long id;
    

    第三步:在需要校验的地方,指定触发的条件

    @PostMapping("/update")
    @ResponseBody
    public ApiCommonResponse<String> updateStudent(@Validated(UpdateStudentBasicInfo.class) Student student) {
      return ApiCommonResponse.success("OK");
    }
    
    image-20210418233722142

    这里需要注意一下,如果指定了groups,那么校验就会只针对该groups中的规则进行。所以,如果对于没有指定groups的规则,默认属于Default.class,此时如果需要包含,可以使用下面的方式。

    @PostMapping("/update")
    @ResponseBody
    public ApiCommonResponse<String> updateStudent(@Validated({UpdateStudentBasicInfo.class, Default.class}) Student student) {
      return ApiCommonResponse.success("OK");
    }
    
    package javax.validation.groups;
    
    public interface Default {
    }
    
    image-20210418234129357
    3、group sequence

    当有多个group时,校验规则的顺序是不固定的,可以通过以下两种方式指定校验的顺序。这里,有点类似组合校验。

    比如这里,会有学生的基本信息,也会有学生的高级信息。校验的时候,希望先对基本信息校验,通过后,再对高级信息校验。

    第一步:还是需要定义高级信息和基本信息标记接口:

    // 高级信息
    public interface StudentAdvanceInfo {
    }
    
    // 基本信息
    public interface StudentBasicInfo {
    }
    

    第二步:按照需要加到实体的group上。

    @NotEmpty(message = "student name should not be empty", groups = StudentBasicInfo.class)
    private String name;
    
    @NotEmpty(message = "student telephone should not be empty", groups = StudentAdvanceInfo.class)
    private String telephone;
    

    下面会有2种方式,指定校验顺序。

    方案1:在被校验实体上指定

    @GroupSequence({StudentBasicInfo.class, StudentAdvanceInfo.class, Student.class})
    public class Student 
    

    注意,一定要将自身包含到GroupSequence中。否则会报错误:xxx must be part of the redefined default group sequence

    之后对Student的校验,会默认按照StudentBasicInfo-> StudentAdvanceInfo->Default的顺序执行。

    方案2:定义一个新的标记接口,指名sequence顺序。相比较而言,如果不希望全局影响Student的校验行为,推荐用该方式。

    @GroupSequence({StudentBasicInfo.class, StudentAdvanceInfo.class})
    public interface ValidateStudentGroupSequence {
    }
    
    @GetMapping("/testGroupSequence")
    @ResponseBody
    public ApiCommonResponse<String> testGroupSequence(@Validated(ValidateStudentGroupSequence.class) Student student) {
      return ApiCommonResponse.success("OK");
    }
    

    可以看到当name属性有了之后,会自动走到StudentAdvanceInfo对应的telephone的校验。

    image
    4、 自定义校验

    Validator自身提供了非常多常用的校验,如果不满足需要,可以自行实现自定义的校验。这里举得例子,实际上用默认的也可以。

    第一步:定义校验注解。

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = CustomerValidator.class)
    public @interface CustomerValidatorAnnotation {
    
      /**
         * 违反校验时的错误信息
         */
      String message() default "{CustomerValidatorAnnotation.message}";
    
      /**
         * 用于指定校验的条件
         */
      Class<?>[] groups() default {};
    
      Class<? extends Payload>[] payload() default {};
    }
    

    第二步:实现校验器,也就是第一步中@Constraint中的类

    
    @Slf4j
    public class CustomerValidator implements ConstraintValidator<CustomerValidatorAnnotation, String> {
    
      private static final String CUSTOMER_TEST = "china";
    
      @Override
      public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s != null && s.startsWith(CUSTOMER_TEST);
      }
    }
    

    第三步:按照需要将其放置到实体上即可。

    @CustomerValidatorAnnotation(message = "student address must start with china")
    private String address;
    

    下图中的start with china,即是自定义规则。

    [图片上传失败...(image-afa4d6-1618763454151)]

    5、Validator的一些定制行为
    自定义注入Validator
    @Bean
    public Validator validator() {
      ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
        .configure()
        .failFast(false)
        .buildValidatorFactory();
      return factory.getValidator();
    }
    

    这个里面,配置了failFast属性,failFast为false时,会对所有的信息进行校验。如果设置failFast为true,则当碰到不满足的条件后,会立即终止,返回当前违反的错误。

    备注:这里需要注意一下,如果实体类是继承的,即使failFast设置为true,子类中有违反的约束,父类也会触发校验。也就是failFast为true,可以理解是每个类都校验,也就是每个类最多会存在一个违反约束的校验结果。

    在非Controller中使用Validator

    除了在controller中使用validator验证,实际上在service中同样可以使用

    @Service
    @Validated
    public class ValidatorService {
    
      public void testValidateService(@Valid Student student) {
    
      }
    }
    

    注意此时如果违反约束,会抛出ConstraintViolationException。

    另外一种方式是基于注入的Validator实现,这种方式需要自己处理校验结果,不会主动抛出异常。

    @Service
    public class ValidatorBeanService {
    
      @Resource
      private Validator validator;
    
      public ApiCommonResponse<String> validate(Student student) {
        Set<ConstraintViolation<Student>> constraintViolations = validator.validate(student);
        if (CollectionUtils.isEmpty(constraintViolations)) {
          return ApiCommonResponse.success("OK");
        }
        String errorMsg = constraintViolations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
        return ApiCommonResponse.fail(HttpStatus.BAD_REQUEST.value(), errorMsg);
      }
    }
    
    @GetMapping("/testValidatorBean")
    @ResponseBody
    public ApiCommonResponse<String> testValidatorBean(Student student) {
      validatorBeanService.validate(student);
      return ApiCommonResponse.success("OK");
    }
    
    image-20210419000544528

    参考文章 https://reflectoring.io/bean-validation-with-spring-boot/

    相关文章

      网友评论

          本文标题:spring-boot-validator使用汇总

          本文链接:https://www.haomeiwen.com/subject/djmblltx.html