spring-boot-validator使用汇总

在写业务代码时,对参数的校验必不可少,基于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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容