spring-boot-validator使用汇总

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

基本操作

1、maven依赖

首先需要引入validator的starter依赖

<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-validationartifactId>
dependency>

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

spring-boot-validator使用汇总_第1张图片

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

spring-boot-validator使用汇总_第2张图片

场景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");
}

spring-boot-validator使用汇总_第3张图片

高级用法

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 

测试情况:

spring-boot-validator使用汇总_第4张图片

可以看到此时,捕获的异常是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);
}

spring-boot-validator使用汇总_第5张图片

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");
}

spring-boot-validator使用汇总_第6张图片

这里需要注意一下,如果指定了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 {
}

spring-boot-validator使用汇总_第7张图片

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的校验。

spring-boot-validator使用汇总_第8张图片

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,即是自定义规则。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PpSYMbYD-1618763372919)(/Users/wuchunjing/Library/Application Support/typora-user-images/image-20210418234232785.png)]

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");
}

spring-boot-validator使用汇总_第9张图片

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

你可能感兴趣的:(springboot,java,java,spring,boot)