Java Bean Validation入门介绍
概述
“数据校验”是比较常见的工作,在日常的开发中贯穿于代码的各个层次,从上层的View层到底层的数据层,为了保证程序的正确运行以及数据的正确性,开发者通常会在不同层次间做数据校验,而且这些校验工作通常都是重复的,为了实现代码的复用性,通常会把校验的逻辑写在被校验的对象上。
Bean Validation
就是为了解决这样的问题,它定义了一套元数据模型和API,对JavaBean实现校验,默认是以注解作为元数据,可以通过XML重写或者拓展元数据,通常来说注解的方式可以实现比较简单逻辑的校验,而复杂校验就需要通过XML来描述。
Bean Validation
包含两部分:规范
和实现
。
规范只提供了相关的API和注解说明,而实现则要依靠具体的软件厂商提供实现代码。就像JDBC
是数据库操作的规范,再由各个数据库厂商提供实现了规范的数据库驱动一样。
使用Bean Validation
前的校验方式:
使用Bean Validation
后的校验方式:
介绍
规范
Bean Validation
通常的使用方法是:在你需要被校验的实体类上用一些注解来修饰需要校验的属性,这些注解就是校验规则,比如必须是邮件地址、必须是整数、必须是未来的日期等。
那么这些作为规则的注解是哪里来的呢,它们一定是来自某个Jar包,那Jar包是谁定义的呢?答案就是它们来自JSR
规范,通常是某个组织比如JCP
或Eclipse
基金会。JSR
规范就像是接口(Interface),它只作为标识,用来描述能力,并不提供具体的逻辑。
Java Specification Requests(Java规范提案)关于Bean Validation
的定义目前有三个版本,正在广泛使用的版本是JSR-380。
JSR名称 | 对应版本 | 发起时间 |
---|---|---|
JSR 380 | Bean Validation 2.0 | 2017-08-21 |
JSR 349 | Bean Validation 1.1 | 2013-05-24 |
JSR 303 | Bean Validation 1.0 | 2009-11-16 |
Bean Validation
2.0版本针对1.0版本做了向下很好的兼容,因此从Bean Validation 1.0
切换到Bean Validation 2.0
基本上是无缝和透明的。本文的侧重也是介绍Bean Validation 2.0
(JSR-380
)。
实现
在Bean Validation 2.0
之前,有两个官方认可的实现,分别是:Hibernate Validator
和Apache BVal
,但如果你想用2.0版本的话,就只有Hibernate Validator
这个实现了(hibernate-validator
与持久层框架 hibernate
没有什么关系)。
Hibernate Validator的兼容性矩性:
Hibernate Validator | 7.0 | 6.1 | 6.0 | 5.4 |
---|---|---|---|---|
Java | 8 or 11 | 8 or 11 | 8 or 11 | 6, 7 or 8 |
Bean Validation | N/A | N/A | 2.0 | 1.1 |
Jakarta Bean Validation | 3.0 | 2.0 | N/A | N/A |
导入了hibernate-validator
这个实现之后,会自动引入规范的Jar包,也就不必要再自己手工导入Java Bean Validation
API了,因此不建议再手动导入API,交给内部来管理依赖。
使用
添加依赖
1 | <dependency> |
声明约束
Java Bean约束
字段/属性级别的约束
1
2
private String manufacturer;方法返回值约别的约束
1
2
3
4
public String getManufacturer(){
return manufacturer;
}容器级别的约束
1
private Map< FuelConsumption, Integer> fuelConsumption = new HashMap<>();
级联验证,使用
@Valid
修饰对象属性的引用,则对象属性中声明的所有约束也会起作用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 当验证 Car 实例时,Person 对象中的 name 字段也会验证
public class Car
{
private Person driver;
//...
}
public class Person
{
private String name;
//...
}约束继承
当一个类继承/实现另一个类时,父类声明的所有约束也会应用在子类继承的对应属性上。 如果方法
重写
,约束注解将会聚合,也就是此方法父类和子类声明的约束都会起作用。
声明方法约束
参数约束
1
2
3public RentalStation(@NotNull String name){}
public void rentCar(@NotNull Customer customer, @NotNull @Future Date startDate, @Min(1) int durationInDays){}返回值约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class RentalStation
{
// 自定义约束,加在构造方法上,表明任何新创建的 RentalStation 对象都必须满足 @validRentalStation 约束
public RentalStation()
{
//...
}
// 列表不能为空,并且必须至少包含 1 个元素
1) (min =
public List< Customer> getCustomers() // 返回的客户列表不能包含空对象
{
//...
return null;
}
}级联验证,使用
@Valid
标记可执行参数和级联验证的返回值。当验证用@valid 注释的参数或返回值时,也会验证在参数或返回值对象上声明的约束。 而且,也可用在容器元素中。1
2
3
4
5
6
7
8
9public class Garage
{
// 列表不能为空,返回的小汽车对象的属性也需要验证
public boolean checkCars(@NotNull List<@Valid Car> cars)
{
//...
return false;
}
}约束继承,当在继承体系中声明方法约束时,必须了解两个规则:
- 方法调用方要满足前置条件不能在子类型中得到加强
- 方法调用方要保证后置条件不能再子类型中被削弱
这些规则是由子类行为概念所决定的:在使用类型 T 的任何地方,也能在不改变程序行为的情况下使用 T 的子类。
当两个类分别有一个同名且形参列表相同的方法,而另一个类用一个方法重写/实现上述两个类的同名方法时,这两个父类的同名方法上不能有任何参数约束,因为不管怎样都会与上述规则冲突。 示例:
1 | public interface Vehicle |
Web场景
集成到SpringMVC
配置验证器
1
2
3
4
5
6
7
8
9
10
public class WebConfig implements WebMvcConfigurer
{
public Validator getValidator();
{
// ...
}
}使用注解
@Valid
和@Validated
实现对请求参数的校验1
2
3
4
5
6
7
public ResponseEntity<User> create(@RequestBody @Validated UserForm form)
{
User user = userService.create(form);
return ResponseEntity.ok().body(user);
}配置统一的控制器通知来处理校验结果
1
2
3
4
5
6
7
8
9
10
11
public class ValidationResponseAdvice extends ResponseEntityExceptionHandler
{
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request)
{
String message = ex.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(","));
return ResponseEntity.badRequest().body(message);
}
}如果方法中有BindingResult类型的参数,spring校验完成之后会将校验结果传给这个参数。通过BindingResult控制程序抛出自定义类型的异常或者返回不同结果
1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean addUser(@Validated ValidatorVO user, BindingResult result)
{
if (result.hasErrors())
{
for (ObjectError error : result.getAllErrors())
{
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
直接使用场景
1 |
|
示例
1 | "用户名不能为空") (message = |
其它
关于更名
2018年03月, Oracle决定把Java EE移交给开源组织Eclipse基金会,但是不希望Java EE继续使用Java这个名字,尽管 Eclipse 做了争取,但是最终没有达成一致,因此Eclipse最终将Java EE正式改名为Jarkarta EE。
在改名之后对应的Maven GAV坐标也进行了更名,迁移前:
1 | <dependency> |
迁移后:
1 | <dependency> |
Bean Validation 2.0新特性
使用Bean Validation的最低Java版本为Java 8
支持容器的校验,通过
TYPE_USE
类型的注解实现对容器内容的约束:List<@Email String>
支持日期/时间的校验,
@Past
和@Future
新增注解:
@Email
、@NotEmpty
、@NotBlank
、@Positive
、@PositiveOrZero
、@Negative
、@NegativeOrZero
、@PastOrPresent
和@FutureOrPresent
(@Email、@NotEmpty、@NotBlank
之前是Hibernate额外提供的注解,Bean Validation 2.0标准后将这些注解引入到自己的规范中了,因此Hibernate自动将这些注解标注为过期,引包的时候需要注意一下)官方认证的实现就只有
Hibernate Validator
,不再包含Apache BVal
了
未来版本
目前Jakarta Bean Validation 3.0已经在开发中,对应Java规范是Java 8或Java11,Bean Validation校验规范的Maven坐标是:jakarta.validation:jakarta.validation-api:3.0.0,对应的Hibernate实现是Hibernate Validator 7.0.0.Alpha5。
1 | <dependency> |
1 | <dependency> |
集合校验
1 | private List< String> emails; // emails这个List中包含的每个字符串都必须是合法的Email地址 |
自定义约束规则
Bean Validation API规范要求约束注解定义以下要求:
一个
message
属性:在违反约束的情况下返回一个默认 key,以用于创建错误消息。一个
groups
属性:允许指定此约束所属的验证分组。必须默认是一个空 Class 数组。一个
payload
属性:能被 Bean Validation API 客户端使用,以自定义一个注解的 payload 对象。API 本身不使用此属性。自定义 payload 可以是用来定义严重程度。
下面的例子通过实现一个身份证的验证器,演示如何自定义约束:
创建自定义注解
1 |
|
自定义验证器
1 | public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> |
使用自定义的注解
1 | "身份证号不能为空") (message = |
内置约束规则
以下每个约束都有参数 message,groups 和 payload,其中,message
是提示消息,groups
可以根据情况来分组。(以下每一个注解都可以在相同元素上定义多个)
注解 | 版本 | 支持数据类型 | 说明 |
---|---|---|---|
@Null | 1.0 | 对象 | 验证注解的元素值是null |
@NotNull | 1.0 | 对象 | 验证注解的元素值不是null,但可以为empty(“”,” “,” “) |
@AssertTrue | 1.0 | boolean/Boolean | 验证注解的元素值是true |
@AssertFalse | 1.0 | boolean/Boolean | 验证注解的元素值是false |
@Min(value) | 1.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value) | 1.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value, inclusive) | 1.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@DecimalMin指定的value值,inclusive:是否包括value的值,布尔值,默认为true |
@DecimalMax(value, inclusive) | 1.0 | BigDecimal、BigInteger、 byte、short、int,、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值小于等于@ DecimalMax指定的value值,inclusive:是否包括value的值,布尔值,默认为true |
@Negative | 2.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值是负数 |
@NegativeOrZero | 2.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值是负数或零 |
@Positive | 2.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值是正数 |
@PositiveOrZero | 2.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值是正数或零 |
@Size(min, max) | 1.0 | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Digits(integer, fraction) | 1.0 | BigDecimal、BigInteger、 byte、short、int、long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值的整数位数和小数位数上限 integer:整数精度,fraction:小数精度 |
@Past | 1.0 | java.util.Date、java.util.Calendar、Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@PastOrPresent | 2.0 | java.util.Date、java.util.Calendar、Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早或者是现在 |
@Future | 1.0 | java.util.Date、java.util.Calendar、Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间晚 |
@FutureOrPresent | 2.0 | java.util.Date、java.util.Calendar、Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间晚或者是现在 |
@Pattern(regexp, flags) | 1.0 | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 String regexp:正则表达式 Flag[] flags:标志的模式,默认为{} |
@NotEmpty | 2.0 | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null,且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 2.0 | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首尾空格 |
@Email(regexp, flags) | 2.0 | CharSequence子类型(如String) | 检查指定的字符序列是否为有效的电子邮件地址 String regexp:正则表达式,默认为., Flag[] *flags**:标志的模式,默认为{} |
分组校验
最常见的一个场景是,我们在增加和修改实体的时候,一般都是使用同一个实体类,但是增加和修改操作对实体的参数校验是不同的。比如,更新时候要校验userId,在保存的时候不需要校验userId,在两种情况下都要校验username。Java Bean Validation提供分组校验的功能,可以实现针对不同的场景应用不同的校验规则。
定义分组类
每个分组类只需要一个接口就可以了,即使用一个空接口做标识
1 | public interface AddGroup |
在校验规则上添加分组
1 |
|
在Controller接口配置使用分组校验
注意要使用
@Validated
注解,而不是@Valid
注解
1 | "create") ( |
分组继承
如果想要默认分组起作用,而其他分组也要校验,怎么操作呢? 可以在使用的时候,指定校验多个分组,如下:
1 | public boolean addUser(@Validated({Default.class,NoIdGroup.class}) ValidatorVO user, BindingResult result) |
这里是想 Default
分组一直都要校验,每次都带上有些赘余,像这样:@Validated({Default.class, ….),因此建议分组在定义的时候继承默认分组(javax.validation.groups.Default):public interface AddGroup extends Default { }
- 配置分组的时候,记得不要漏掉默认分组
Default.class
,否则就只会校验groups = {AddGroup.class}
的规则了 - 上面的22个内置约束规则都有一个groups属性,如果不指定groups,默认为Default分组
分组排序
分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。比如用户输入的请求中包含用户名和密码,要求先校验用户名,再校验密码。
定义一个标识接口,使用@GroupSequence注解,对校验的注解进行组合排序:
1 | public interface Name {} |
然后使用Validated注解时,指定这个组合了顺序之后的新的标识接口:
1 | .class }) (groups = {UserSequence |
关于@Valid注解和@Validated注解
简单的讲:@Valid
是JSR的注解(javax.validation.Valid),@Validated
是Spring的注解(org.springframework.validation.annotation.Validated)。
JSR
规范支持手动校验,不直接支持使用注解校验,不过 spring
提供了分组校验注解扩展支持,即:@Validated
,参数为 group 类集合。@Validated
注解group属性不传时会调用默认的Default.class分组。
在全局校验中增加校验异常
ControllerAdvice
1 | import org.slf4j.Logger; |
AOP
1 |
|
参考
- The Java Community Process(SM) Program - JSRs: Java Specification Requests - summary https://jcp.org/en/jsr/summary?id=bean+validation
- Jakarta Bean Validation - Jakarta Bean Validation 2.0 https://beanvalidation.org/2.0/
- Jakarta Bean Validation specification https://beanvalidation.org/2.0/spec/
- Releases - Hibernate Validator http://hibernate.org/validator/releases/
- Jakarta Bean Validation 3.0 | The Eclipse Foundation https://jakarta.ee/specifications/bean-validation/3.0/
- Apache BVal https://bval.apache.org/downloads.html
- Jakarta Bean Validation specification https://beanvalidation.org/2.0/spec/#changelog
- validator 自动化校验 - 掘金 https://juejin.im/post/6844903951171584013#heading-39
- 深入了解数据校验:Java Bean Validation 2.0(JSR303、JSR349、JSR380)Hibernate-Validation 6.x使用案例【享学Java】_YourBatman-CSDN博客 https://blog.csdn.net/f641385712/article/details/96638596