Java Bean Validation + Hibernate Validator
Java Bean Validation + Hibernate Validator
官方网站
参考博客
A哥学数据校验 - 随笔分类 - YourBatman - 博客园
背景知识
在实际的业务开发中,从客户端(浏览器)发起请求,到控制层,业务逻辑层,数据访问层,都需要做参数校验,有时候,我们甚至会在多层对同一个参数或者类属性进行校验,这产生了大量的重复代码,这是一个普遍的问题,而且,多人协作的时候,每个人的校验方式不一样,抛出的异常不一样,提示的信息不一样,代码量一多,会非常难以管理,所以,最好的方式是将验证逻辑与相应的域模型(也就是 bean)进行绑定,即:Bean Validation
。其实说白了,就是用面向对象的思想,将待验证的信息放到一个类中进行管理,于是,我们有了Java Bean Validation。
Java Bean Validation
是一个通过配置注解来验证参数的框架,它包含两部分 Bean Validation API
(规范)和 Hibernate Validator
(实现)。
Java Bean Validation 2.0和Jakarta Bean Validation 2.0版本是当下的主流版本,对应 JSR380,JSR 是 Java Specification Requests
的缩写,意思是 Java 规范提案。Hibernate Validator
自 6.x
版本开始对 JSR 380
规范提供完整支持,除了支持标准外,自己也做了相应的优化,比如性能改进、减少内存占用,添加了自己的注解等等。
Jakarta EE 9
发布之后,Jakarta Bean Validation 3.0
也正式发布,只是除了将包名中的 javax.*
换成 jakarta.*
之外,代码实现并没有啥改变,Hibernate Validator
也推出了 7.x 版本用于支持 Jakarta Bean Validation 3.0
。跟 Jakarta Bean Validation 3.0
一样,并没有引入新特性,这也是现在的主流版本依然是 Java Bean Validation 2.0**和**Jakarta Bean Validation 2.0
的原因。
Java Bean Validation 2.0 和 Jakarta Bean Validation 2.0
总共 22 的验证注解

除了 JSR 标准提供的这 22 个注解外,Hibernate Validator 还提供了一些非常实用的注解

JavaSE 中的使用
简单配置
引入依赖,当你导入了 hibernate-validator
后,无需再显示导入 javax.validation
,因为 hibernate-validator
中已经包含了 validation-api
。
注意,Bean Validation 需要 El 管理器,用于错误消息动态插值,因此需要自己额外导入 EL 的实现,这里我们选择 tomcat-embed-el,如果是如果是 web 项目,则跟 servlet-api 一样,tomcat 已经提供了,所以 scope 是 provided。
小贴士:EL 也属于 Java EE 标准技术,可认为是一种表达式语言工具,它并不仅仅是只能用于 Web(即使你绝大部分情况下都是用于 web 的 jsp 里),可以用于任意地方(类比 Spring 的 SpEL)
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
<!-- 引入 Expression Language 3.0,实现-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.29</version>
<!-- 如果是web项目,则跟servlet-api一样,tomcat已经提供了,所以scope 是provided-->
<!--<scope>provided</scope> 打包时最用此scope-->
</dependency>
<!-- 测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
为了在错误信息中能够打印完整的方法参数名,我们需要在编译的时候加上编译参数 -parameters
,当我们使用 Maven 的时候,添加以下配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
注解应用的位置
Bean Validation 共支持四个级别的约束:
- 字段约束(Field)
- 属性约束(Property)
- 容器元素约束(Container Element),也就是 Collection、Map 这类数据。
- 类约束(Class)
值得注意的是,并不是所有的约束注解都能够标注在上面四种级别上。现实情况是:Bean Validation 自带的 22 个标准约束全部支持 1/2/3 级别,且全部不支持第 4 级别(类级别)约束。当然喽,作为补充的 Hibernate-Validator
它提供了一些专门用于类级别的约束注解,如 org.hibernate.validator.constraints.@ScriptAssert
。
什么是字段?什么是属性?他们的区别是什么? Java beans 中字段(field)和属性(property)的区别
Glossary of Terms: https://docs.oracle.com/javase/tutorial/information/glossary.html#P
field: A data member of a class. Unless specified otherwise, a field is not static.
property: Characteristics of an object that users can set, such as the color of a window.
翻译过来就是
field 类的数据成员,默认非 static
property 用户可以设置的实例对象的一种特质这两个概念有重叠,主要是理解的主体不一样,一般谈到 field,主体都是类,而 property,主体是实例对象
默认类中声明的所有的,都是 field,添加了 setter 方法之后,就变成了一个 property这个在 IDEA 的 Structure Tool Window 中也有体现,f 就表示 field,p 就表示 property
字段的本质是 Field,属性的本质是 Method
你可以简单地把 field 理解为类内部的变量,而 property 为 set 方法,,这个 set 方法的内部设置一个 field 的值,也可以设置多个,也可以不设置。但是这个 set 方法总归是改变了对象实例的状态
字段约束
Bean Validation 将直接通过反射的方式校验字段值,不通过 get/set 方法。因此字段约束可以应用于任何访问修饰符的字段,
但是注意:
-
不支持对静态字段的约束(static 静态字段使用约束无效)。
-
若你的对象会被字节码增强,比如 AspectJ,那么请不要使用 Field 约束,而是使用下面介绍的属性级别约束更为合适。因为增强过的类并不一定能通过字段反射去获取到它的值,Lombok 也是编译期间在字节码层面进行操作,但是还好不会影响 Bean Validation,
我们优先使用字段约束,但是字段约束无法生效的时候,我们可以使用属性约束。
属性约束
效果跟字段约束类似,有几点需要注意
-
如果使用了 Lombok,还想使用属性约束,那就只能手动创建 get/set 方法,然后添加注解。
-
set 方法上无法标注注解,放了会报错,只能在 get 方法上标注,这样也好,不可变类也可以进行约束。
-
跟字段约束一样,无法在静态属性(静态字段的静态的 get 方法)上进行属性约束。
-
不要在属性和字段上都标注注解,否则会重复执行约束逻辑(有多少个注解就执行多少次)
因为在使用 Lombok 的前提下,要实现属性约束就需要手动写 get/set 方法,所以我们在能用字段约束的时候,还是尽量用属性约束,但是如果对象被字节码增强了,那没办法,就只能老老实实用属性约束了。
容器约束
-
若约束注解想标注在容器元素上,那么注解的定义中的
@Target
里必须包含TYPE_USE
(Java8 新增)这个类型。PS:Bean Validation 和 Hibernate Validator(除了 Class 级别)的所有注解均能标注在容器元素上 -
Bean Validation 规定了可以验证容器内元素,Hibernate Validator 提供实现。它默认支持如下容器类型:
-
java.util.Iterable
的实现(如 List、Set) -
java.util.Map
的实现,支持 key 和 value -
java.util.Optional/OptionalInt/OptionalDouble...
-
JavaFX 的
javafx.beans.observable.ObservableValue
-
自定义容器类型
-
类约束
约束也可以放在类级别上(也就说注解标注在类上)。在这种情况下,验证的主体不是单个属性,而是整个对象。如果验证依赖于对象的几个属性之间的相关性,那么类级别约束就能搞定这一切。比如后面介绍的@ScriptAssert。
注意
Bean 上的所有地方的所有的约束注解都会执行,不存在短路效果
多个约束之间的执行也是可以排序(有序的),这就涉及到多个约束的执行顺序(序列)问题。
关于注解的短路问题和校验排序问题,看 @GroupSequence
注解和 @GroupSequenceProvider
注解
注解必须配合特定的 Java 类型使用
-
@NotNull/@Null
可以应用于任何类型 -
@NotBlank
只能用于字符串,表示字符串不能为空字符串,必须至少包含一个非空白字符 -
@NotEmpty
只能应用于字符串类型、Collection、Map、数组,表示字符串的长度和 Collection、Map、数组的长度不能为空。和@NotBlank
的区别是," "
无法通过@NotBlank
校验,但是可以通过@NotEmpty
校验。 -
@Size
跟@NotEmpty
修饰的类型一样,表示字符串的长度和 Collection、Map、数组的长度。 -
@Min/@Max
只支持校验整形数字类型(包括各种整形原始数据类型的包装类和 BigDecimal/BigInteger),也支持小数类型,比如 float 和 double 类型(虽然类文档说不支持,但是我试了下,是支持的,应该是 hibernate-validator 实现的功劳),但是计算机中的小数毕竟是不精确的,所以为了不出现奇奇怪怪的 bug,还是不要进行高精度小数的比较,(相关知识《Java 核心技术》卷一第三章,浮点类型小节),同时@Min/@Max
还支持校验数字字符串。 -
@DecimalMin/@DecimalMax
跟@Min/@Max
差不多,区别是@DecimalMin/@DecimalMax
的注解参数是字符串,也就是可以输入小数字符串作为比较值,而@Min/@Max
的注解参数是long
型,只能用整型数字作为比较值。而且@DecimalMin/@DecimalMax
可以通过inclusive
参数设置是否包含注解中配置的值,但是@Min/@Max
无法设置,默认为可以等于,总的来说@DecimalMin/@DecimalMax
的功能多些。推荐使用。 -
当方法返回值类型为 Void 的时候,不可以对方法返回值使用任何校验注解,否则会报错
Void methods must not be constrained or marked for cascaded validation
。
这些校验注解跟其修饰的类型必须匹配,否则会报错。
简单 Bean 校验 - 字段和属性校验
字段校验
待验证的 baen
@Data
public class User {
@NonNull
@NotBlank(message = "名字不能为空")
private String name;
@NonNull
@NotBlank(message = "性别不能为空")
private String gender;
@NonNull
@NotBlank(message = "职位不能为空")
private String occupation;
@NonNull
@Min(value = 18, message = "年龄至少为18岁")
private int age;
}
进行验证
@Test
public void validation(){
User user = new User("", "", "", 1);
// 使用默认的校验器对user进行校验然后拿到结果
// 1、使用【默认配置】得到一个校验工厂 这个配置可以来自于provider、SPI提供
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
// 2、得到一个校验器
Validator validator = validatorFactory.getValidator();
// 3、校验Java Bean(解析注解) 返回校验结果
Set<ConstraintViolation<User>> result = validator.validate(user);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
System.out.println("================================");
//用hibernateValidator
HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(false);
ValidatorFactory validatorFactory4Hibernate = configuration.buildValidatorFactory();
Set<ConstraintViolation<User>> sets = validatorFactory4Hibernate.getValidator().validate(user);
sets.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ":" + v.getInvalidValueprintln;
}
输出日志:
age 年龄至少为18岁: 1
name 名字不能为空:
gender 性别不能为空:
occupation 职位不能为空:
================================
name 名字不能为空:
gender 性别不能为空:
age 年龄至少为18岁:1
occupation 职位不能为空:
注意,将校验注解放到静态字段上是无效的,例如将 name 设置为静态字段
@Data
public class UserTest4StaticField {
@NonNull
@NotBlank(message = "名字不能为空")
private static String name;
@NonNull
@NotBlank(message = "性别不能为空")
private String gender;
@NonNull
@NotBlank(message = "职位不能为空")
private String occupation;
@NonNull
@Min(value = 18, message = "年龄至少为18岁")
private int age;
/**
* Lombok 构造函数默认不添加静态字段,因为静态字段属于全体类实例共有,而构造函数是为了创建类实例
*/
public UserTest4StaticField(String name,@NonNull String gender, @NonNull String occupation, @NonNull int age) {
this.name = name;
this.gender = gender;
this.occupation = occupation;
this.age = age;
}
}
跟上面一样的测试类,只是将 User 换成 UserTest4StaticField,输出的日志就是
gender 性别不能为空:
age 年龄至少为18岁: 1
occupation 职位不能为空:
================================
gender 性别不能为空:
age 年龄至少为18岁:1
occupation 职位不能为空:
因为 Validator 等校验器是线程安全的,因此一般来说一个应用全局仅需一份即可,因此只需要初始化一次。其实这种情况非常适合用 Spring IOC 容器(默认单例模式)来管理 validator。现在为了方便,我们可以提取出一个单独的工具类。
/**
* 因为Validator等校验器是线程安全的,因此一般来说一个应用全局仅需一份即可,因此只需要初始化一次。
* 其实这种情况非常适合用Spring IOC容器(默认单例模式)来管理validator实例
*/
@UtilityClass
public class ValidationUtil {
public Validator getDefaultValidator() {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
public ExecutableValidator getDefaultExecutableValidator() {
return getDefaultValidator().forExecutables();
}
public Validator getHibernateValidator() {
HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(false);
ValidatorFactory validatorFactory4Hibernate = configuration.buildValidatorFactory();
Validator validator = validatorFactory4Hibernate.getValidator();
return validator;
}
public ExecutableValidator getHibernateExecutableValidator() {
return getHibernateValidator().forExecutables();
}
}
属性校验
注意,跟字段校验一样,将校验注解放到静态属性(静态字段的静态的 get 方法)上是无效的,比如 name 属性
@Data
public class UserPropertyValidateTest {
@NonNull
private static String name;
@NonNull
private String gender;
@NonNull
private String occupation;
@NonNull
private int age;
@NotBlank(message = "名字不能为空")
public static String getName() {
return name;
}
@NotBlank(message = "名字不能为空")
public static void setName(String name) {
UserPropertyValidateTest.name = name;
}
@NotBlank(message = "性别不能为空")
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@NotBlank(message = "职位不能为空")
public String getOccupation() {
return occupation;
}
public void setOccupation(String occupation) {
this.occupation = occupation;
}
@Min(value = 18, message = "年龄至少为18岁")
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试类
public class BeanPropertyValidateTestTest {
@Test
public void validation(){
UserPropertyValidateTest user = new UserPropertyValidateTest( "", "", 1);
Set<ConstraintViolation<UserPropertyValidateTest>> result = ValidationUtil.getDefaultValidator().validate(user);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
日志
gender 性别不能为空:
age 年龄至少为18岁: 1
occupation 职位不能为空: 空:
分组校验
有几点需要注意
-
校验注解可以属于多个分组
-
如果校验注解没有带分组,那么校验注解默认属于 Default 分组,如果指定的分组中没有显示包含 Default 分组(Default.class),则不会自动添加 Default 分组
-
validate 方法可以指定多个分组
-
validate 方法如果没有指定分组,则默认校验 Default 分组,如果指定的分组中没有显示包含 Default 分组(Default.class),则不会自动添加 Default 分组
分组接口
public interface Simple {
}
校验分组的 bean
@Data
public class UserWithGroup {
@NonNull
@NotBlank(message = "名字不能为空",groups = Simple.class)
private String name;
@NonNull
@NotBlank(message = "性别不能为空",groups = Simple.class)
private String gender;
@NotBlank(message = "职位不能为空")
private String occupation;
@NonNull
@Min(value = 18, message = "年龄至少为18岁")
private int age;
}
测试类
public class BeanValidateWithGroupTest {
@Test
public void validation(){
UserWithGroup user = new UserWithGroup( "", "", 1);
// 默认分组
Set<ConstraintViolation<UserWithGroup>> defaultResult = ValidationUtil.getDefaultValidator().validate(user);
System.out.println("--------------------------------");
defaultResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
// Simple分组
Set<ConstraintViolation<UserWithGroup>> SimpleResult = ValidationUtil.getDefaultValidator().validate(user,Simple.class);
System.out.println("--------------------------------");
SimpleResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
// 同时指定默认分组和Simple分组
Set<ConstraintViolation<UserWithGroup>> multipleResult = ValidationUtil.getDefaultValidator().validate(user,Simple.class, Default.class);
System.out.println("--------------------------------");
multipleResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
日志
--------------------------------
age 年龄至少为18岁: 1
occupation 职位不能为空: null
--------------------------------
gender 性别不能为空:
name 名字不能为空:
--------------------------------
gender 性别不能为空:
age 年龄至少为18岁: 1
name 名字不能为空:
occupation 职位不能为空: null
List 和 Map 类型的 bean 属性和方法的泛型类返回值的校验 - 容器内部元素校验
可直接在 List/Map
类型属性声明属性类型的时候,在类型参数前加校验注解,非常直观,也非常好用。同时,当容器的元素类型是自定义的类型比如 User、Student 之类的类型的时候,也可以使用级联注解@Valid,这真的非常常用。
@Data
public class CollectionMapValidation {
@NotNull
@Size(min = 1,max = 10)
private List<@NotBlank String> list;
@NotNull
@Size(min = 1,max = 10)
private Map<@NotBlank String, @Min(10) @Max(20) Integer> map;
@NotNull
@Size(min = 1,max = 10)
private Map<@NotBlank String, @Valid @NotNull User> userMap;
}
测试类
public class CollectionMapValidationTest {
@Test
public void test() {
CollectionMapValidation obj = new CollectionMapValidation();
@NotNull @Size(min = 1, max = 10) Map<@NotBlank String, @Min(10) @Max(20) Integer> map = new HashMap<@NotBlank String, @Min(20) Integer>();
map.put("aaa", 112);
map.put("", 12);
obj.setMap(map);
@NotNull @Size(min = 1, max = 10) List<@NotBlank String> list= new ArrayList<>();
//list.add("ddd");
//list.add("ccc");
obj.setList(list);
User user0 = new User("", "", "", 0);
User user1 = new User("", "", "", 1);
@NotNull @Size(min = 1, max = 10) Map<@NotBlank String, @Valid @NotNull User> userMap=new HashMap<>();
userMap.put("user0",user0);
userMap.put("user1",user1);
obj.setUserMap(userMap);
Validator validator = ValidationUtil.getDefaultValidator();
Set<ConstraintViolation<CollectionMapValidation>> result = validator.validate(obj);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
输出日志,元素的验证顺序是乱序的,而且奇怪的是,user1 的 age 的验证输出了两次,
userMap[user1].age 年龄至少为18岁: 1
map[aaa].<map value> 最大不能超过20: 112
userMap[user0].occupation 职位不能为空:
userMap[user1].occupation 职位不能为空:
userMap[user1].name 名字不能为空:
list 个数必须在1和10之间: []
map<K>[].<map key> 不能为空:
userMap[user0].age 年龄至少为18岁: 0
userMap[user0].gender 性别不能为空:
userMap[user1].gender 性别不能为空:
userMap[user0].name 名字不能为空:
userMap[user1].age 年龄至少为18岁: 1
除此之外,还可以为泛型类进行验证
@Data
public class Result<T> {
public static int STATUS_NORMAL = 1;
public static int STATUS_EXCEPTION = -1;
@Min(-1)
@Max(1)
@NonNull
private int status;
@NotNull
@NonNull
private String message;
@Valid
private T content;
}
测试代码,Result<T>
一般用于方法返回值。
public class FunctionValidation4CustomerContainer {
@SneakyThrows
public @Valid @NotNull Result<User> validateReturnValue() {
// ... 模拟逻辑执行,得到一个result
Result<User> result = new Result<User>(3,"");
result.setContent(new User("","","",0));
// 在结果返回之前校验
Method currMethod = this.getClass().getMethod("validateReturnValue", null);
Set<ConstraintViolation<FunctionValidation4CustomerContainer>> validResult = ValidationUtil.getDefaultExecutableValidator().validateReturnValue(this, currMethod, result);
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("返回结果错误");
}
return result;
}
}
测试方法
public class FunctionValidation4CustomerContainerTest {
@Test
public void validateReturnValue() {
new FunctionValidation4CustomerContainer().validateReturnValue();
}
}
生成日志
validateReturnValue.<return value>.content.name 名字不能为空:
validateReturnValue.<return value>.content.occupation 职位不能为空:
validateReturnValue.<return value>.status 最大不能超过1: 3
validateReturnValue.<return value>.content.gender 性别不能为空:
validateReturnValue.<return value>.content.age 年龄至少为18岁: 0
可以看到,成功校验
自定义 ValueExtractor
上面的例子之所以可以正常校验,是因为 Hibernate Validator 中默认添加了非常多钟类型的ValueExtractor的实现类帮助我们从容器中取出特定的值来进行校验,这个类的注释也告诉了我们该如何使用 ValueExtractor。

接下来我们自己动手试验一下。
public class ResultValueExtractor implements ValueExtractor<Result<@ExtractedValue ?>> {
@Override
public void extractValues(Result<?> originalValue, ValueReceiver receiver) {
receiver.value(null, originalValue.getContent());
}
}
测试,注意添加 ValueExtractor,获取新的 Validator,
/**
* 使用自定义的ValueExtractor
* @return
*/
@SneakyThrows
public @Valid @NotNull Result<User> validateReturnValueByMyValueExtractor() {
// ... 模拟逻辑执行,得到一个result
Result<User> result = new Result<User>(3,"");
result.setContent(new User("","","",0));
// 在结果返回之前校验
Validator validator = Validation.buildDefaultValidatorFactory().usingContext()
// 注册 ValueExtractor 然后获取新的 Validator
.addValueExtractor(new ResultValueExtractor())
.getValidator();;
ExecutableValidator executableValidator = validator.forExecutables();
Method currMethod = this.getClass().getMethod("validateReturnValueByMyValueExtractor", null);
Set<ConstraintViolation<FunctionValidation4CustomerContainer>> validResult = executableValidator.validateReturnValue(this, currMethod, result);
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("返回结果错误");
}
return result;
}
测试方法
@Test
public void validateReturnValueByMyValueExtractor() {
new FunctionValidation4CustomerContainer().validateReturnValueByMyValueExtractor();
}
输出日志
validateReturnValueByMyValueExtractor.<return value>.content.age 年龄至少为18岁: 0
validateReturnValueByMyValueExtractor.<return value>.content.occupation 职位不能为空:
validateReturnValueByMyValueExtractor.<return value>.status 最大不能超过1: 3
validateReturnValueByMyValueExtractor.<return value>.content.name 名字不能为空:
validateReturnValueByMyValueExtractor.<return value>.content.gender 性别不能为空:
Bean 级联校验 - @Valid
关于 @Valid
注解的详细源码分析:从深处去掌握数据校验@Valid的作用(级联校验) - YourBatman - 博客园,有需要可以看看。
@Valid
注解的注释:标记需要级联校验的属性、方法参数或方法返回类型。当校验属性、方法参数或方法返回类型时,将验证定义在对象及其属性上的约束。这种行为是递归的,直到在对象内部的属性上找不到约束为止。
所以@Valid 可以在 bean 属性、方法参数/返回值进行级联,这里我们先看 bean 属性的级联校验,方法参数/返回值校验在后面再看
@Data
public class Reservation {
@NonNull
@Valid
private User customer;
@Size(max = 200)
private String remarks;
@NonNull
@Min(0)
@Max(100)
private int roomNumber;
@NonNull
@Min(2)
@Max(12)
private int customerCount;
}
校验方法
@Test
public void test(){
User user = new User("", "男","", 16);
Reservation reservation = new Reservation(user, 20, 15);
Validator validator = ValidationUtil.getDefaultValidator();
Set<ConstraintViolation<Reservation>> result = validator.validate(reservation);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
输出日志
customerCount 最大不能超过12: 15
customer.occupation 职位不能为空:
customer.age 年龄至少为18岁: 16
customer.name 名字不能为空:
注意,List 或者 Map 的类型参数也可以级联校验,这里举个例子,就不详细测试了,回看上一小节即可
@Data
public class Hotel {
private List<@Valid @NotNull User> customer;
}
普通方法参数和返回值校验
很多时候,我们只是想对一些简单的独立参数(比如方法入参 int age,比如方法返回值)进行校验,并不需要大动干戈的弄个 Java Bean 装起来(这个时候用面向对象的思想也会有点笨重),所以我们要实现对方法参数/返回值直接进行校验。
注意,当方法返回值类型为 Void 的时候,不可以对方法返回值使用任何校验注解,否则会报错 Void methods must not be constrained or marked for cascaded validation
。
注意,为了在错误信息中能够打印完整的方法参数名,我们需要在编译的时候加上编译参数 -parameters
,当我们使用 Maven 的时候,添加以下配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
测试代码如下
public class SimpleFunctionValidation {
@SneakyThrows
public void validateParameters(@Size(min = 2, max = 4) @NotEmpty String name, @NotNull @Min(0) @Max(100) Integer age, @Past @NotNull Date birthDate) {
Method currMethod = this.getClass().getMethod("validateParameters", String.class, Integer.class, Date.class);
Set<ConstraintViolation<SimpleFunctionValidation>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{name, age, birthDate});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("参数错误");
}
}
@SneakyThrows
public @Valid @NotNull User validateReturnValue() {
// ... 模拟逻辑执行,得到一个result
User result = null;
// 在结果返回之前校验
Method currMethod = this.getClass().getMethod("validateReturnValue", null);
Set<ConstraintViolation<SimpleFunctionValidation>> validResult = ValidationUtil.getDefaultExecutableValidator().validateReturnValue(this, currMethod, result);
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("返回结果错误");
}
return result;
}
}
测试类
public class SimpleFunctionValidationTest {
@Test
public void testSimpleParams() {
new SimpleFunctionValidation().validateParameters("", 0, new Date());
}
@Test
public void testSimpleReturnValue() {
User user = new SimpleFunctionValidation().validateReturnValue();
}
}
输出日志
validateParameters.name 不能为空:
validateParameters.name 个数必须在2和4之间:
java.lang.IllegalArgumentException: 参数错误
.....
validateReturnValue.<return value> 不能为null: null
java.lang.IllegalArgumentException: 返回结果错误
构造函数参数和返回值校验
针对构造函数的参数和返回值的校验其实不多,尤其是构造器的返回值校验,构造器的执行结果,就是当前对象,即 this ,而类对象其实又不是基本类型,基本上没啥可校验的,要校验,也是校验返回的对象的内部的属性,是否符合配置在其上的规则,这就需要级联校验,所以,用@Valid。
public class ConstructorValidation {
@NotNull
private String name;
@NotNull
@Min(0)
private Integer age;
@SneakyThrows
public ConstructorValidation(@Size(min = 2, max = 4) @NotEmpty String name, @NotNull @Min(0) @Max(100) Integer age, @Past @NotNull Date birthDate) {
Constructor constructor = this.getClass().getConstructor( String.class, Integer.class, Date.class);
Set<ConstraintViolation<ConstructorValidation>> validResult = ValidationUtil.getDefaultExecutableValidator().validateConstructorParameters(constructor, new Object[]{name, age, birthDate});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("构造函数参数错误");
}
}
@SneakyThrows
public @Valid ConstructorValidation() {
// 构造器的执行结果,就是当前对象,即 this ,而类对象其实又不是基本类型,基本上没啥可校验的,要校验,也是校验返回的对象的内部的属性,是否符合配置在其上的规则,这就需要级联校验,所以,用@Valid
Constructor constructor = this.getClass().getConstructor();
Set<ConstraintViolation<ConstructorValidation>> validResult = ValidationUtil.getDefaultExecutableValidator().validateConstructorReturnValue(constructor, this);
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("构造函数返回结果错误");
}
}
}
测试类
public class ConstructorValidationTest {
@Test
public void testConstructorParameters() {
new ConstructorValidation("", 0, new Date());
}
@Test
public void testConstructorReturnValue() {
new ConstructorValidation();
}
}
日志
ConstructorValidation.name 不能为空:
ConstructorValidation.name 个数必须在2和4之间:
java.lang.IllegalArgumentException: 构造函数参数错误
......
ConstructorValidation.<return value>.age 不能为null: null
ConstructorValidation.<return value>.name 不能为null: null
java.lang.IllegalArgumentException: 构造函数返回结果错误
对 JavaBean 类型的参数和返回值进行级联校验 - @Valid
这里我们已普通方法为例,构造函数方法其实是差不多的
public class FunctionCascadeValidation {
@SneakyThrows
public void validateParametersCascade(@Valid @NotNull Reservation reservation) {
Method currMethod = this.getClass().getMethod("validateParametersCascade", Reservation.class);
Set<ConstraintViolation<FunctionCascadeValidation>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{reservation});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("级联参数错误");
}
}
@SneakyThrows
public @Valid @NotNull Reservation validateReturnValueCascade() {
// ... 模拟逻辑执行,得到一个result
Reservation result = new Reservation(new User("","", "",45),0,100 );
// 在结果返回之前校验
Method currMethod = this.getClass().getMethod("validateReturnValueCascade", null);
Set<ConstraintViolation<FunctionCascadeValidation>> validResult = ValidationUtil.getDefaultExecutableValidator().validateReturnValue(this, currMethod, result);
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("返回结果级联错误");
}
return result;
}
}
测试类
public class FunctionCascadeValidationTest {
@Test
public void validateParametersCascade() {
new FunctionCascadeValidation().validateParametersCascade(new Reservation(new User("","", "",45),0,100 ));
}
@Test
public void validateReturnValueCascade() {
new FunctionCascadeValidation().validateReturnValueCascade();
}
}
日志
validateReturnValueCascade.<return value>.roomNumber 最小不能小于1: 0
validateReturnValueCascade.<return value>.customerCount 最大不能超过12: 100
validateReturnValueCascade.<return value>.customer.gender 性别不能为空:
validateReturnValueCascade.<return value>.customer.occupation 职位不能为空:
validateReturnValueCascade.<return value>.customer.name 名字不能为空:
java.lang.IllegalArgumentException: 返回结果级联错误
......
validateParametersCascade.reservation.customer.name 名字不能为空:
validateParametersCascade.reservation.customerCount 最大不能超过12: 100
validateParametersCascade.reservation.roomNumber 最小不能小于1: 0
validateParametersCascade.reservation.customer.gender 性别不能为空:
validateParametersCascade.reservation.customer.occupation 职位不能为空:
java.lang.IllegalArgumentException: 级联参数错误
如果子类继承自他的父类,那么校验子类的时候会校验父类的字段的约束吗?
如果子类继承自他的父类,除了校验子类,同时还会校验父类,这就是约束继承(同样适用于接口)。同时,如果子类覆盖了父类的方法,那么子类和父类的约束都会被校验。
校验注解应该写在接口方法还是实现类方法上?
直接说结论,应该写在接口上。
看情况一:写在接口上
public interface Function0 {
@NotNull String getInfo( @Size(min = 2, max = 4) @NotBlank String name, @NotBlank String gender, @Min(0) @Max(100) Integer age);
@NotNull String getInfo(@Valid @NotNull User user);
}
/**
* 实现类一个校验注解都没有
*/
public class Impl0 implements Function0 {
@SneakyThrows
@Override
public String getInfo(String name, String gender, Integer age) {
Method currMethod = this.getClass().getMethod("getInfo", String.class,String.class, Integer.class);
Set<ConstraintViolation<Impl0>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{name,gender, age});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("参数错误");
}
return name + " " + gender + " " + age;
}
@SneakyThrows
@Override
public String getInfo(User user) {
Method currMethod = this.getClass().getMethod("getInfo", User.class);
Set<ConstraintViolation<Impl0>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{user});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("参数错误");
}
return user.toString();
}
}
测试类
public class Function0Test {
@Test
public void getInfo() {
new Impl0().getInfo("", "", 0);
}
@Test
public void getInfo2() {
User user = new User("", "", "", 0);
new Impl0().getInfo(user);
}
}
日志
getInfo.name 个数必须在2和4之间:
getInfo.gender 不能为空:
getInfo.name 不能为空:
java.lang.IllegalArgumentException: 参数错误
......
getInfo.user.age 年龄至少为18岁: 0
getInfo.user.occupation 职位不能为空:
getInfo.user.gender 性别不能为空:
getInfo.user.name 名字不能为空:
java.lang.IllegalArgumentException: 参数错误
情况二:接口方法和实现方法上都写,但是实现方法上注解少一点
public interface Function1 {
@NotNull String getInfo( @Size(min = 2, max = 4) @NotBlank String name, @NotBlank String gender, @Min(0) @Max(100) Integer age);
@NotNull String getInfo(@Valid @NotNull User user);
}
实现类
/**
* 实现类校验注解比接口少
*/
public class Impl1 implements Function1 {
@SneakyThrows
@Override
public @NotNull String getInfo(@Size(min = 2, max = 4) String name, @NotBlank String gender, @Max(100) Integer age) {
Method currMethod = this.getClass().getMethod("getInfo", String.class,String.class, Integer.class);
Set<ConstraintViolation<Impl1>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{name,gender, age});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("参数错误");
}
return name + " " + gender + " " + age;
}
@SneakyThrows
@Override
public @NotNull String getInfo( @NotNull User user) {
Method currMethod = this.getClass().getMethod("getInfo", User.class);
Set<ConstraintViolation<Impl1>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{user});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("参数错误");
}
return user.toString();
}
}
测试类
public class Function1Test {
@Test
public void getInfo() {
new Impl1().getInfo("", "", 0);
}
@Test
public void getInfo2() {
User user = new User("", "", "", 0);
new Impl1().getInfo(user);
}
}
日志
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method Impl1#getInfo(User) redefines the configuration of Function1#getInfo(User).
.....
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method Impl1#getInfo(User) redefines the configuration of Function1#getInfo(User).
A method overriding another method must not redefine the parameter constraint configuration,
方法重写另一个方法的时候,不能重新定义限定注解,也就是说,子类重写父类方法的时候,实现类实现接口方法的时候,如果父类方法或者接口方法中,已经进行了限定注解的配置,那么子类重写方法或者实现类方法都不应该再重新配置限定注解,即自动使用父类或者接口方法中的限定注解。
所以,最好写在接口方法上,这样,只用写一次校验注解,所有实现类将自动使用接口方法中的限定注解。不需要再自己写一遍了。这真的太牛了。
接口和实现类上都有注解,以谁为准?
直接说结论,会同时生效。
此外,如果接口和实现类的返回值都标了@Valid,也会报错。
javax.validation.ConstraintDeclarationException: HV000131: A method return value must not be marked for cascaded validation more than once in a class hierarchy, but the following two methods are marked as such: UserService#addUser(String, String, Integer), UserServiceImpl#addUser(String, String, Integer).
A method return value must not be marked for cascaded validation more than once in a class hierarchy
Bean Validation 常用注解
除了 注解必须配合特定的Java类型使用
小节提到的那几个注解以外,还有
@Pattern - 按照正则表达式匹配字符串 重点
真的非常方便
@Data
public class RegexValidationPattern {
@NonNull
@NotBlank(message = "值不能为空")
@Pattern(regexp = "^[a-zA-Z0-9]+",message = "不符合格式")
private String value;
}
测试类
public class RegexValidationPatternTest {
@Test
public void test() {
RegexValidationPattern pattern = new RegexValidationPattern("###aaabbb09");
//RegexValidationPattern pattern = new RegexValidationPattern("");
//RegexValidationPattern pattern = new RegexValidationPattern("");
Set<ConstraintViolation<RegexValidationPattern>> result = ValidationUtil.getDefaultValidator().validate(pattern);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
输出日志
value 不符合格式: ###aaabbb09
@AssertFalse/@AssertTrue
只能校验 Boolean 类型的字段
@Future/@FutureOrPresent/@Past/@PastOrPresent - 校验时间
比如生日一定是过去的一个时间,预定入住时间肯定是将来的一个时间
有机会再学习
@Digits - 校验字段必须是数字
有机会再学习
@Positive/@PositiveOrZero/@Negative/@NegativeOrZero - 校验数字大于等于 0 还是小于等于 0
有机会再学习
@Email - 校验邮箱地址
有机会再学习
@GroupSequence - 指定校验组的校验顺序
默认情况下,不同组别的约束验证是无序的
在某些情况下,约束验证的顺序是非常的重要的,比如如下两个场景:
- 第二个组的约束验证依赖于第一个约束执行完成的结果(必须第一个约束正确了,第二个约束执行才有意义)
- 某个 Group 组的校验非常耗时,并且会消耗比较大的 CPU/内存。那么我们的做法应该是把这种校验放到最后,所以对顺序提出了要求
写个例子
校验组
public interface Simple {
}
public interface Advanced {
}
@GroupSequence
用在 JavaBean 上表示此 bean 默认的校验组校验序列,其中可以将当前类放在最前面,表示此 JavaBean 中属于 Default 组的校验,
@Data
@GroupSequence({UserWithGroupSequence.class,Simple.class, Advanced.class})
public class UserWithGroupSequence {
@NotBlank(message = "名字不能为空")
private String name;
@NotBlank(message = "性别不能为空")
private String gender;
@NotBlank(message = "职位不能为空", groups = Simple.class)
private String occupation;
@Min(value = 18, message = "年龄至少为18岁", groups = Advanced.class)
private int age;
}
测试类
public class UserWithGroupSequenceTest {
@Test
public void testUser() {
UserWithGroupSequence user = new UserWithGroupSequence();
Set<ConstraintViolation<UserWithGroupSequence>> result = ValidationUtil.getDefaultValidator().validate(user);
// 对结果进行遍历输出
System.out.println("---------------");
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
@Test
public void testUser2() {
UserWithGroupSequence user = new UserWithGroupSequence();
user.setName("xiashuo");
user.setGender("男");
Set<ConstraintViolation<UserWithGroupSequence>> result = ValidationUtil.getDefaultValidator().validate(user);
// 对结果进行遍历输出
System.out.println("---------------");
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
输出日志
testUser 的日志,因为默认分组的校验值没有通过,所以后面的校验组都没有输出
---------------
gender 性别不能为空: null
name 名字不能为空: null
testUser2 的日志,因为默认分组的校验值通过了,开始校验 Simple 分组的,但是 Simple 分组的没通过,所以 Advanced 分组的不会进行校验
---------------
occupation 职位不能为空: null
我们还可以将特定的某一种序列放到接口上,然后将此接口当成一个校验组(包含校验组序列)来传递给 validate 方法,这样,我们就可以 存储
很多不同的序列,想用那种用那种,
校验组
public interface Simple {
}
public interface Advanced {
}
校验组组序列,默认把 Default 分组放到最前面
@GroupSequence({Default.class,Simple.class, Advanced.class})
public interface GroupSequence1 {
}
测试
public class UserWithGroupSequenceInterfaceTest {
@Test
public void testUser() {
UserWithGroupSequenceInterface user = new UserWithGroupSequenceInterface();
Set<ConstraintViolation<UserWithGroupSequenceInterface>> result = ValidationUtil.getDefaultValidator().validate(user, GroupSequence1.class);
// 对结果进行遍历输出
System.out.println("---------------");
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
@Test
public void testUser2() {
UserWithGroupSequenceInterface user = new UserWithGroupSequenceInterface();
user.setName("xiashuo");
user.setGender("男");
Set<ConstraintViolation<UserWithGroupSequenceInterface>> result = ValidationUtil.getDefaultValidator().validate(user,GroupSequence1.class);
// 对结果进行遍历输出
System.out.println("---------------");
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
testUser 的日志,因为默认分组的校验值没有通过,所以后面的校验组都没有输出
---------------
gender 性别不能为空: null
name 名字不能为空: null
testUser2 的日志,因为默认分组的校验值通过了,开始校验 Simple 分组的,但是 Simple 分组的没通过,所以 Advanced 分组的不会进行校验
---------------
occupation 职位不能为空: null
跟直接在 JavaBean 上使用 @GroupSequence({UserWithGroupSequence.class,Simple.class, Advanced.class})
的效果一模一样。
当然,我们还可以创建别的校验组序列,灵活度大大提升。
注解校验顺序
数据校验注解是可以标注在字段、属性、方法、构造器以及 Class
类级别上的。它们的校验顺序,我们是可控的,当然,顺序只能控制在分组级别,无法控制在约束注解级别。因为一个类内的约束(同一分组内),它的顺序是 Set<MetaConstraint<?>> metaConstraints
来保证的,所以可以认为同一分组内的校验器是木有执行的先后顺序的(不管是类、属性、方法、构造器...)。
Hibernate Validator 实现引入的注解
@ScriptAssert - 类校验 重点
@ScriptAssert
可以实现跨字段逻辑校验,
@ScriptAssert
支持写脚本来完成验证逻辑,这里使用的是 javascript(缺省情况下的唯一选择,也是默认选择)
@ScriptAssert
对 null 值并不免疫,不管咋样它都会执行的,所以如果 customer 或者 ticketType 为 null,调用其方法,就会报空指针错误。所以需要我们自己在脚本里判断。
/**
* 顾客总数不能超过售票的总数
* 票的类型数也不能超过票的个数
*/
@Data
//在 lang = "javascript" 时,只能通过 size() 获取Set的长度
@ScriptAssert(lang = "javascript", alias = "_", script = "_.ticketCount >= _.customer.size()")
//在 lang = "javascript" 时,既可以通过 size(),也可以通过 length属性 获取List的长度
@ScriptAssert(lang = "javascript", alias = "_", script = "_.ticketCount >= _.ticketType.length")
public class TicketOffice {
@NonNull
@Min(1)
private Integer ticketCount;
@NonNull
@Size(min = 0)
private Set<@NotNull @Valid User> customer;
@NonNull
@UniqueElements
@Size(min = 0)
private List<@NotNull String> ticketType;
}
测试类
public class TicketOfficeTest {
@Test
public void test() {
HashSet<@NotNull @Valid User> customer = new HashSet<>();
User user = new User("", "", "", 2);
customer.add(user);
ArrayList<@NotNull String> ticketType = new ArrayList<>();
ticketType.add("住建部");
ticketType.add("住建部");
TicketOffice ticketOffice = new TicketOffice(0, customer, ticketType);
Set<ConstraintViolation<TicketOffice>> result = ValidationUtil.getDefaultValidator().validate(ticketOffice);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
输入日志
customer[].name 名字不能为空:
ticketCount 最小不能小于1: 0
customer[].age 年龄至少为18岁: 2
customer[].gender 性别不能为空:
ticketType must only contain unique elements: [住建部, 住建部]
customer[].age 年龄至少为18岁: 2
customer[].occupation 职位不能为空:
执行脚本表达式"_.ticketCount >= _.customer.size()"没有返回期望结果: TicketOffice(ticketCount=0, customer=[User(name=, gender=, occupation=, age=2)], ticketType=[住建部, 住建部])
执行脚本表达式"_.ticketCount >= _.ticketType.length"没有返回期望结果: TicketOffice(ticketCount=0, customer=[User(name=, gender=, occupation=, age=2)], ticketType=[住建部, 住建部])
@ParameterScriptAssert - 方法校验 重点
@ParameterScriptAssert
可以实现一个方法中跨参数的逻辑校验,重点学习
注意,当我们没有在 pom 中配置编译 -parameters
参数的时候,脚本中只能使用 arg0、arg1 来表示参数,但是配置了编译参数之后,就可以直接在脚本中使用原参数名
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
测试如下:
/**
* 顾客总数不能超过售票的总数
* 票的类型数也不能超过票的个数
*/
@Data
public class TicketOffice4Sell {
@NonNull
@Min(1)
private Integer ticketCount;
@SneakyThrows
@ParameterScriptAssert(lang = "javascript", script = "remainTicketCount >= ticket2sell")
public void sellTicket(@NotNull @Min(1) Integer remainTicketCount, @NotNull @Min(1) Integer ticket2sell) {
Method currMethod = this.getClass().getMethod("sellTicket", Integer.class,Integer.class);
Set<ConstraintViolation<TicketOffice4Sell>> validResult = ValidationUtil.getDefaultExecutableValidator().validateParameters(this, currMethod, new Object[]{remainTicketCount, ticket2sell});
if (!validResult.isEmpty()) {
// ... 输出错误详情validResult
validResult.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
throw new IllegalArgumentException("参数错误");
}
}
}
测试类
public class TicketOffice4SellTest {
@Test
public void test() {
TicketOffice4Sell test = new TicketOffice4Sell(10);
test.sellTicket(test.getTicketCount(), 80);
}
}
输出日志:
sellTicket.<cross-parameter> 执行脚本表达式"remainTicketCount >= ticket2sell"没有返回期望结果: [Ljava.lang.Object;@29d80d2b
@UniqueElements - 集合中是否有重复元素
有机会再学习
@URL - 是否是合法的 URL
有机会在学习
@DurationMax/@DurationMin - 校验时间段
校验 java.time.DurationMax
对象
有机会再学习
@Range - 数字范围 跟@Min/@Max 类似
应用于数值或数值的字符串
@Length - 字符串长度范围 跟@Size 差不多
验证包含的字符串是否在 min 和 max 之间。
@ISBN - 字符串序列是否是 ISBN 序列
International Standard Book Number (ISBN) 国际书号,任何一本出版的书都有一个 ISBN 编号
有机会再学习
@EAN- 字符串序列是否是 EAN 序列
International Article Number (also known as European Article Number or EAN)
有机会再学习
@Currency - 校验货币单位
有机会再学习
@CreditCardNumber - 校验信用卡卡号
有机会再学习
@GroupSequenceProvider - 根据 bean 的字段值动态确定生效的校验组
根据当前对象实例的状态,动态来决定加载那些校验组进入默认校验组,即在运行时动态确定校验组,非常方便。此注解需要配合 Hibernate Validation
提供给我们的 DefaultGroupSequenceProvider
接口才能完成功能。
DefaultGroupSequenceProvider 接口实现:
public class ReservationGroupSequenceProvider implements DefaultGroupSequenceProvider<ReservationWithGroupSequenceProvider> {
@Override
public List<Class<?>> getValidationGroups(ReservationWithGroupSequenceProvider bean) {
List<Class<?>> defaultGroupSequence = new ArrayList<>();
// 这一步不能省,否则Default分组都不会执行了,会抛错的
defaultGroupSequence.add(ReservationWithGroupSequenceProvider.class);
if (bean != null) { // 这块判空请务必要做
Integer type = bean.getRoomType();
if (type >= 1 && type <= 2) {
defaultGroupSequence.add(RoomTypeSmall.class);
System.err.println("房间类型为:" + type + ",执行RoomTypeSmall组的校验逻辑");
} else if (type >= 3 && type <= 4) {
defaultGroupSequence.add(RoomTypeMiddle.class);
System.err.println("房间类型为:" + type + ",执行RoomTypeMiddle组的校验逻辑");
} else if (type >= 5 && type <= 6) {
defaultGroupSequence.add(RoomTypeMax.class);
System.err.println("房间类型为:" + type + ",执行RoomTypeMax组的校验逻辑");
}
}
return defaultGroupSequence;
}
}
被校验的 JavaBean
@Data
@GroupSequenceProvider(ReservationGroupSequenceProvider.class)
public class ReservationWithGroupSequenceProvider {
@NonNull
@Min(1)
@Max(6)
private int roomType;
/**
* 如果roomType 为 1-2 customer2Come 人数应该是在5以下
* 如果roomType 为 3-4 customer2Come 人数应该是在5-10
* 如果roomType 为 5-6 customer2Come 人数应该是在10-15
* 其他的值不校验
*/
@NonNull
@UniqueElements
@Size(max = 5, groups = RoomTypeSmall.class)
@Size(min = 5, max = 10, groups = RoomTypeMiddle.class)
@Size(min = 10, max = 15, groups = RoomTypeMax.class)
private List<@Valid User> customer2Come;
}
分组
public interface RoomTypeSmall {
}
public interface RoomTypeMiddle {
}
public interface RoomTypeMax {
}
测试类
public class ReservationWithGroupSequenceProviderTest {
@Test
public void testRoomType1() {
int roomType = 1;
List<User> customers = new ArrayList<>();
customers.add(new User("1", "1", "1", 18));
customers.add(new User("1", "2", "1", 18));
customers.add(new User("1", "3", "1", 18));
customers.add(new User("1", "4", "1", 18));
customers.add(new User("1", "5", "1", 18));
customers.add(new User("1", "6", "1", 18));
ReservationWithGroupSequenceProvider reservation = new ReservationWithGroupSequenceProvider(roomType,customers);
Set<ConstraintViolation<ReservationWithGroupSequenceProvider>> result = ValidationUtil.getDefaultValidator().validate(reservation);
// 对结果进行遍历输出
System.out.println("---------------");
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
@Test
public void testRoomType2() {
int roomType = 3;
List<User> customers = new ArrayList<>();
customers.add(new User("1", "1", "1", 18));
customers.add(new User("1", "2", "1", 18));
ReservationWithGroupSequenceProvider reservation = new ReservationWithGroupSequenceProvider(roomType,customers);
Set<ConstraintViolation<ReservationWithGroupSequenceProvider>> result = ValidationUtil.getDefaultValidator().validate(reservation);
// 对结果进行遍历输出
System.out.println("---------------");
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
日志输出:
testRoomType1 的日志,
房间类型为:1,执行RoomTypeSmall组的校验逻辑
房间类型为:1,执行RoomTypeSmall组的校验逻辑
---------------
customer2Come 个数必须在0和5之间: [User(name=1, gender=1, occupation=1, age=18), User(name=1, gender=2, occupation=1, age=18), User(name=1, gender=3, occupation=1, age=18), User(name=1, gender=4, occupation=1, age=18), User(name=1, gender=5, occupation=1, age=18), User(name=1, gender=6, occupation=1, age=18)]
testRoomType2 的日志,
房间类型为:3,执行RoomTypeMiddle组的校验逻辑
房间类型为:3,执行RoomTypeMiddle组的校验逻辑
---------------
customer2Come 个数必须在5和10之间: [User(name=1, gender=1, occupation=1, age=18), User(name=1, gender=2, occupation=1, age=18)]
为什么明明只校验了一次,却输出两次日志?
看此博客 解决多字段联合逻辑校验问题【享学Spring MVC】 - YourBatman - 博客园 的原理解析小节。
自定义校验注解
自定义校验注解,是最终的解决方案
自定义注解 MyAnnotation:
@Target({TYPE, ANNOTATION_TYPE,FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = {MyAnnotationConstraintValidator.class})
public @interface MyAnnotation {
String name();
int age();
String message() default "自定义提示信息";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注解处理类 MyAnnotationConstraintValidator
public class MyAnnotationConstraintValidator implements ConstraintValidator<MyAnnotation, Object> {
@Override
public void initialize(MyAnnotation constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
//初始化方法中可以获取注解当前的各种属性值
int age = constraintAnnotation.age();
String name = constraintAnnotation.name();
Class<?>[] groups = constraintAnnotation.groups();
String message = constraintAnnotation.message();
Class<? extends Payload>[] payload = constraintAnnotation.payload();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//value 被校验的值
if (value == null) {
return false;
}
Map<String, Object> attributes = ((ConstraintValidatorContextImpl) context).getConstraintDescriptor().getAttributes();
// 这两个时注解陪着信息,想怎么判断,就怎么判断
System.out.println(attributes.toString());
boolean isValid = false;
return isValid;
}
}
使用
@Data
public class MyAnnotationObj {
@NonNull
@MyAnnotation(name = "aaa", age = 1)
private String property;
}
测试
public class MyAnnotationObjTest {
@Test
public void test() {
MyAnnotationObj obj = new MyAnnotationObj("bbbbb");
Set<ConstraintViolation<MyAnnotationObj>> result = ValidationUtil.getDefaultValidator().validate(obj);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
日志
property 自定义提示信息: bbbbb
常见自定义注解
@EnumValue - 约定属性的值未特定的范围(字符串数组、数值数组、枚举)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValue.EnumValueValidator.class})
public @interface EnumValue {
//默认错误消息
String message() default "必须为指定值";
//支持string数组验证
String[] strValues() default {};
//支持int数组验证
int[] intValues() default {};
//支持枚举列表验证
Class<?>[] enumValues() default {};
//分组
Class<?>[] groups() default {};
//负载
Class<? extends Payload>[] payload() default {};
//指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
/**
* 校验类逻辑定义
*/
class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
//字符串类型数组
private String[] strValues;
//int类型数组
private int[] intValues;
//枚举类
private Class<?>[] enumValues;
/**
* 初始化方法
*
* @param constraintAnnotation
*/
@Override
public void initialize(EnumValue constraintAnnotation) {
strValues = constraintAnnotation.strValues();
intValues = constraintAnnotation.intValues();
enumValues = constraintAnnotation.enumValues();
}
/**
* 校验方法
*
* @param value
* @param context
* @return
*/
@SneakyThrows
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
//针对字符串数组的校验匹配
if (strValues != null && strValues.length > 0) {
if (value instanceof String) {
for (String s : strValues) {//判断值类型是否为Integer类型
if (s.equals(value)) {
return true;
}
}
}
}
//针对整型数组的校验匹配
if (intValues != null && intValues.length > 0) {
if (value instanceof Integer) {//判断值类型是否为Integer类型
for (Integer s : intValues) {
if (s == value) {
return true;
}
}
}
}
//针对枚举类型的校验匹配
if (enumValues != null && enumValues.length > 0) {
for (Class<?> cl : enumValues) {
if (cl.isEnum()) {
//枚举类验证
Object[] objs = cl.getEnumConstants();
//这里需要注意,定义枚举时,枚举值名称统一用value表示
Method method = cl.getMethod("getValue");
for (Object obj : objs) {
Object code = method.invoke(obj, null);
if (value.equals(code.toString())) {
return true;
}
}
}
}
}
return false;
}
}
}
自定义校验 message
每个约束定义中都包含有一个用于提示验证结果的消息模版 message
,并且在声明一个约束条件的时候,你可以通过这个约束注解中的message 属性来重写默认的消息模版(这是自定义 message
最简单的一种方式)
如果在校验的时候,这个约束条件没有通过,那么你配置的 MessageInterpolator 插值器会被用来当成解析器来解析这个约束中定义的消息模版, 从而得到最终的验证失败提示信息。默认使用的插值器是 org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator,继承自 AbstractMessageInterpolator,ResourceBundleMessageInterpolator 借助 org.hibernate.validator.spi.resourceloading.ResourceBundleLocator 来获取到国际化资源属性文件从而填充模版内容。ResourceBundleLocator 默认使用的实现是 PlatformResourceBundleLocator
,
MessageInterpolator 会尝试解析模版中的占位符 ( 大括号括起来的字符串,形如这样 {xxx}
)。解析过程参考 AbstractMessageInterpolator 中的 interpolateMessage 方法和 resolveMessage 方法,大概过程如下:
- 若没占位符符号
{
需要处理,直接返回(比如我们自定义 message 属性值全是文字,就直接返回了) - 有占位符或者 EL,交给
resolveMessage()
方法从资源文件里拿内容来处理,即我们所说的动态解析。 - 拿取资源文件,按照如下三个步骤寻找:
1.userResourceBundleLocator
:去用户自己的classpath
里面去找资源文件(默认名字是ValidationMessages.properties
,当然你也可以使用国际化名)
2.contributorResourceBundleLocator
:加载贡献的资源包
3.defaultResourceBundle
:默认的策略。去这里于/org/hibernate/validator
加载ValidationMessages.properties
- 需要注意的是,如上是加载资源的顺序。无论怎么样,这三处的资源文件都会加载进内存的(并无短路逻辑)。进行占位符匹配的时候,依旧遵守这规律:
- 最先用自己当前项目
classpath
下的资源去匹配资源占位符,若没匹配上再用下一级别的资源。 - 规律同上,依次类推,递归的匹配所有的占位符(若占位符没匹配上,原样输出,并不是输出
null
哦)
- 最先用自己当前项目
简单实践
在 resources
文件夹下创建自定义注解 message 文件 ValidationMessages.properties,说明:因为我的平台是中文的,因此文件命名为 ValidationMessages_zh_CN.properties
的效果也是一样的,因为 Hibernate Validation
提供了 Locale
国际化的支持。
注意,在 message 信息中可以使用占位符{value}读取注解对应属性上的值。此外,因为 {
在此处是特殊字符,若你就想输出 {
,请转义:\{
// 此处可以使用占位符{value}读取注解对应属性上的值
xyz.xiashuo.pojo.User.name.NotBlank = [自定义消息]姓名不能为空,实际值
xyz.xiashuo.pojo.User.gender.NotBlank = [自定义消息]性别不能为空,实际值
xyz.xiashuo.pojo.User.occupation.NotBlank = [自定义消息]职位不能为空,实际值
xyz.xiashuo.pojo.User.age.Min = [自定义消息]年龄最小值不能小于{value},实际值
待校验的 bean
@Data
public class UserWithCustomizeMessage {
@NonNull
@NotBlank(message = "{xyz.xiashuo.pojo.User.name.NotBlank}")
private String name;
@NonNull
@NotBlank(message = "{xyz.xiashuo.pojo.User.gender.NotBlank}")
private String gender;
@NotBlank(message = "{xyz.xiashuo.pojo.User.occupation.NotBlank}")
private String occupation;
@NonNull
@Min(value = 18, message = "{xyz.xiashuo.pojo.User.age.Min}")
private int age;
}
测试类
public class UserWithCustomizeMessageTest {
@Test
public void getName() {
UserWithCustomizeMessage user = new UserWithCustomizeMessage( "", "", 1);
Set<ConstraintViolation<UserWithCustomizeMessage>> result = ValidationUtil.getDefaultValidator().validate(user);
// 对结果进行遍历输出
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValueprintln;
}
}
输出日志
gender [自定义消息]性别不能为空,实际值:
age [自定义消息]年龄最小值不能小于18,实际值: 1
name [自定义消息]姓名不能为空,实际值:
occupation [自定义消息]职位不能为空,实际值: null
源码结构 - 简单梳理
3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸 - YourBatman - 博客园
4. Validator校验器的五大核心组件,一个都不能少 - YourBatman - 博客园

ValidatorContext
校验器上下文,根据此上下文创建 Validator 实例。不同的上下文可以创建出不同实例(这里的不同指的是内部组件不同),满足各种个性化的定制需求。
ValidatorContext 接口提供设置方法可以定制校验器的核心组件,它们就是 Validator 校验器的五大核心组件
messageInterpolator
消息插值器。按字面不太好理解:简单的说就是对 message 内容进行格式化,若有占位符 {}
或者 el 表达式 ${}
就执行替换和计算。对于语法错误应该尽量的宽容。
traversableResolver
遍历解析器。从字面上可以解释为:确定某个属性是否能被 ValidationProvider 访问,当每访问一个属性时都会通过它来判断一下子。
constraintValidatorFactory
约束校验器工厂。ConstraintValidator 约束校验器我们应该不陌生:每个约束注解都得指定一个/多个约束校验器,形如这样:@Constraint(validatedBy = { xxx.class })
。ConstraintValidatorFactory 就是工厂:可以根据 Class 生成对象实例,在 自定义校验注解
的时候使用过。
parameterNameProvider
参数名提供器。这个组件和 Spring 的 ParameterNameDiscoverer
作用是一毛一样的:获取方法/构造器的参数名。一样的,若你想要打印出明确的参数名,请在编译参数上加上 -parameters
参数。
clockProvider
时钟提供器。这个接口很简单,就是提供一个 Clock,给 @Past、@Future
等阅读判断提供参考。唯一实现为 DefaultClockProvider
ValueExtractor
值提取器。2.0 版本新增一个比较重要的组件 API,作用:把值从容器内提取出来。这里的容器包括:数组、集合、Map、Optional 等等。在 自定义ValueExtractor
中使用过。