Hibernate Validator 校验最佳实践

本文最后更新于:2021年6月15日 晚上

几年前刚学习 SpringBoot 的时候,就接触并开始使用 HibernateValidator 校验框架,注解校验结合统一异常处理,对代码的整洁性提高特别有帮助。但是目前发现公司里比较新的项目中对参数进行校验还是使用以前传统的方式,需要逐一对请求对象中的属性使用 if 来判断合法性,当需要校验的属性很多时,一大坨的 if 判断校验代码就不可避免。本文介绍 HibernateValidator 校验框架的日常使用,不涉及自定义约束注解。

没有对比就没有伤害

首先来看一下使用校验框架前后的代码对比

使用校验框架后,代码简洁很多有木有~

环境配置

首先需要引入依赖,需要注意是否依赖有冲突,如果有一定要解决掉冲突

1
2
3
4
5
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.0.Final</version>
</dependency>

依赖引入后,Spring 环境需要配置一下校验器。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ValidatorConfig {

@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}

这里设置 failFast 为 true,代表着如果有多个参数的情况下,只要校验出一个参数有误就返回错误而不再继续校验。

为对象添加校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class StudentVo {

@NotNull(message = "学号不能为空")
private Integer id;

@NotNull(message = "姓名不能为空")
private String name;

@NotNull(message = "邮箱地址不能为空")
@Email(message = "邮箱地址不正确")
private String email;

private Integer age;
}

Hibernate Validator 是对 JSR 349 验证规范的具体实现,相关的常用注解 我贴在文末以供参考。

请求接口处理

1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/create")
public Result<StudentVo> create(@Validated StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}

@PostMapping("/update")
public Result<StudentVo> update(@Validated StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}

注意 @Validatedorg.springframework.validation.annotation.Validated,不要引入错了。加了这个注解之后,就会自动会参数进行校验。如果校验不通过,会抛出 BindException 或者 MethodArgumentNotValidException这两个异常中的一个异常,一般项目中为了规范接口返回值,都会进行统一异常处理。

校验异常统一异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BindException.class)
@ResponseBody
public Result validateErrorHandler(BindException e) {
ObjectError error = e.getAllErrors().get(0);
return Result.fail(error.getDefaultMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result<?> validateErrorHandler(MethodArgumentNotValidException e) {
ObjectError error = e.getBindingResult().getAllErrors().get(0);
return Result.fail(error.getDefaultMessage());
}

@ExceptionHandler(Throwable.class)
@ResponseBody
public Result<?> handleException(HttpServletRequest request, Throwable ex) {
return Result.fail(ex.getMessage());
}
}

因为配置校验为 failFast,因此错误信息中只会有一条记录。

第一次测试

1
curl -i -X POST -d "name=张三" 'http://localhost:8082/learning-validator/create'

只填写了 name 参数,而 id 与 email 未填写的情况下进行请求,返回结果

1
{"success":false,"msg":"学号不能为空"}

按照一般开发逻辑而言,create 接口是不需要传递 id 参数的,但是 update 接口一般必须要传 id 参数,难不成用这个这个校验框架后需要写两个对象么?其实不是的。这就引出了 校验分组 的概念,即可以选择校验某些组的属性进行校验。

校验分组

首先需要创建两个分组,CreateGroupUpdateGroup,分别代表着新增时需要校验与更新时需要校验。两个组都是空的接口,只是作为一个标记类使用。

1
2
3
4
public interface CreateGroup {
}
public interface UpdateGroup {
}

接着修改请求对象,分别指定哪些对象是哪些组需要校验的,如果不指定组,默认都会进行校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class StudentVo {
@NotNull(groups = UpdateGroup.class, message = "学号不能为空")
private Integer id;

@NotNull(message = "姓名不能为空")
private String name;

@NotNull(groups = CreateGroup.class, message = "邮箱地址不能为空")
@Email(groups = CreateGroup.class, message = "邮箱地址不正确")
private String email;

private Integer age;
}

本文示例中请求对象,id 属性只在更新时校验;name 属性无论是新增还是更新都要校验;email 属性只在新增时进行校验,而 age 属性因为未指定校验注解,因此不进行校验,这里的 groups 可以是多个分组。

指定属性的分组之后,控制器接口也需要指定使用哪一个组来进行校验。

1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/create")
public Result<StudentVo> create(@Validated(CreateGroup.class) StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}

@PostMapping("/update")
public Result<StudentVo> update(@Validated(UpdateGroup.class) StudentVo student) {
System.out.println(student.toString());
return Result.success(student);
}

第二次测试

1
curl -i -X POST -d "name=张三" 'http://localhost:8082/learning-validator/create'

只填写了 name 参数,而 id 与 email 未填写的情况下进行请求,返回结果:

1
{"success":false,"msg":"邮箱地址不能为空"}

填写 name 参数以及 email 参数进行请求

1
2
3
4
curl -i -X POST \
-d "name=张三" \
-d "email=vcmq@foxmail.com" \
'http://localhost:8082/learning-validator/create'

返回结果:

1
{"data":{"name":"张三","email":"vcmq@foxmail.com"},"success":true,"msg":"success"}

可以看到 id 这个字段在 create 的时候,并没有进行校验。修改为 update 接口进行测试。

1
2
3
4
curl -i -X POST \                                             
-d "name=张三" \
-d "email=vcmq@foxmail.com" \
'http://localhost:8082/learning-validator/update'

返回结果:

1
{"success":false,"msg":"学号不能为空"}

手动校验

除了使用@Validated 注解方式校验,也可以进行手动校验,手动校验同样也支持分组校验。

1
2
3
4
5
6
@PostMapping("/create2")
public Result<StudentVo> create2(StudentVo student) {
ValidatorUtils.validateEntity(student, CreateGroup.class);
System.out.println(student.toString());
return Result.success(student);
}

校验工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ValidatorUtils {
private static Validator validator;

static {
validator = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败
.failFast(true)
.buildValidatorFactory().getValidator();
}

/**
* 校验对象
*
* @param object 待校验对象
* @param groups 待校验的组
* @throws ApiException 校验不通过,则报 ApiException 异常
*/
public static void validateEntity(Object object, Class<?>... groups)
throws ApiException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
constraintViolations.stream().findFirst()
.map(ConstraintViolation::getMessage)
.ifPresent(v1 -> {
throw new ApiException(v1);
});
}
}

常用注解

空与非空检查

注解 支持 Java 类型 说明
@Null Object 为 null
@NotNull Object 不为 null
@NotBlank CharSequence 不为 null,且必须有一个非空格字符
@NotEmpty CharSequence、Collection、Map、Array 不为 null,且不为空(length/size>0)

Boolean 值检查

注解 支持 Java 类型 说明 备注
@AssertTrue boolean、Boolean 为 true 为 null 有效
@AssertFalse boolean、Boolean 为 false 为 null 有效

日期检查

注解 支持 Java 类型 说明 备注
@Future Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间之后 为 null 有效
@FutureOrPresent Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间或之后 为 null 有效
@Past Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间之前 为 null 有效
@PastOrPresent Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证日期为当前时间或之前 为 null 有效

数值检查

注解 支持 Java 类型 说明 备注
@Max BigDecimal、BigInteger,byte、short、int、long 以及包装类 小于或等于 为 null 有效
@Min BigDecimal、BigInteger,byte、short、int、long 以及包装类 大于或等于 为 null 有效
@DecimalMax BigDecimal、BigInteger、CharSequence,byte、short、int、long 以及包装类 小于或等于 为 null 有效
@DecimalMin BigDecimal、BigInteger、CharSequence,byte、short、int、long 以及包装类 大于或等于 为 null 有效
@Negative BigDecimal、BigInteger,byte、short、int、long、float、double 以及包装类 负数 为 null 有效,0 无效
@NegativeOrZero BigDecimal、BigInteger,byte、short、int、long、float、double 以及包装类 负数或零 为 null 有效
@Positive BigDecimal、BigInteger,byte、short、int、long、float、double 以及包装类 正数 为 null 有效,0 无效
@PositiveOrZero BigDecimal、BigInteger,byte、short、int、long、float、double 以及包装类 正数或零 为 null 有效
@Digits(integer = 3, fraction = 2) BigDecimal、BigInteger、CharSequence,byte、short、int、long 以及包装类 整数位数和小数位数上限 为 null 有效

其他

注解 支持 Java 类型 说明 备注
@Pattern CharSequence 匹配指定的正则表达式 为 null 有效
@Email CharSequence 邮箱地址 为 null 有效,默认正则 '.*'
@Size CharSequence、Collection、Map、Array 大小范围(length/size>0) 为 null 有效

hibernate-validator 扩展约束(部分)

注解 支持 Java 类型 说明
@Length String 字符串长度范围
@Range 数值类型和 String 指定范围
@URL URL 地址验证

示例代码

参考