Bean Validation 数据校验实战
一.Bean Validation 数据校验实战
1.1 为什么会出现数据校验Bean Validation
在日常的开发工作中会出现大量的、重复的Check、Assert这种代码。影响代码的美观,也增加了维护成本。
类似下面这种
public String query(QueryParam param) {
checkNotNull(param.getName(), “Name must be not null”);
checkNotNull(param.getNo(), “No must be not null”);
AssertUtils.isFalse(param.isRegistered);
…
}
这时,JavaEE推出了一套标准,根据注解来标识变量、方法、类来做通用校验,便于开发者写一些重复枯燥的代码。
百度百科:
Bean Validation是Java EE 6数据验证新框架,Validation API并不依赖特定的应用层或是编程模型,这样同一套验证可由应用的所有层共享。它还提供了通过扩展Validation API来增加客户化验证约束的机制以及查询约束元数据仓库的手段。
后来,也是到现在的Hibernate Validator也是对Bean Validation的参考实现,提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
1.2 Bean Validation的发展历史
1.2.1 JSR 303(2009) Bean Validation 1.0
第一版的考虑场景并不会很多,主要是基于JavaBean的属性进行验证,还未涉及到方法和返回值。更多的应该是树立了几个目标:
- 减少校验代码冗余
- 减少各层之间代码管理成本
- 保证错误提示语义一致性
Bean Validation实现 | 版本 |
---|---|
Hibernate Validator | 4.3.1.Final |
Apache BVal | 0.5 |
参考链接:
http://jcp.org/en/jsr/detail?id=303
1.2.2 JSR 349(2013) Bean Validation 1.1
在有了第一版的基础,1.1算是一次1.0的加强版,支持了各种方法校验、验证组件的引入等等。
主要更新如下:
- 增加了方法级别的验证(参数或返回值的验证)
- Bean验证组件的依赖注入
- 用EL表达式的错误消息的补充
。。。
Bean Validation实现 | 版本 |
---|---|
Hibernate Validator | 5.1.1.Final |
Apache BVal | 1.1.2 |
参考链接:
http://jcp.org/en/jsr/detail?id=349
https://beanvalidation.org/1.1/
1.2.3 JSR 380(2019) Bean Validation 2.0
这次算是极大的变化,支持Java8,最高支持到Java9,与Hibernate Validator进行了吸取和融合,目前的唯一实现,只有Hibernate Validator。对应的Maven版本hibernate-validator
6.0.17.Final。同时,一些在2.0以前的hibernate-validator的注解也成为了「正式员工」加入了Bean Validation的标准中。
题外话,在2018年,JavaEE已经改名为Jakarta,虽然包路径还是原理JavaEE风格以 javax.xxx开头,后续会修改。
Bean Validation实现 | 版本 |
---|---|
Hibernate Validator | 6.0.17.Final |
PS:据说是因为Hibernate的员工跳槽到Eclipse基金会,所以后面基本上只会有Hibernate Validator这一个实现了。
注:JSR是什么?
答:JSR是JavaSpecification Requests的缩写,意思是“Java 规范提案”。是指向JCP(JavaCommunity Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。简单的就是jsr是java开发者以及授权者指定的标准,而java开发者以及授权者形成一个jcp国际组织。职能是指定java标准。
1.3 Bean Validation的未来
后续Hibernate Validator要推出的7.0版,也会支持后续推出的Jakarta Validation 3.0。不过新特性据说没有什么更新= =。
当然,你如果有兴趣的话:
Hibernate validator 7.0 的文档也可以看看:
https://docs.jboss.org/hibernate/validator/7.0/reference/en-US/html_single/
二.Bean Validation的综合使用 – Hibernate Validator 6.0.17.Final
2.1 常见的22个注解
注意:这里只是根据发布版本分类,在目前的Jakarta Bean Validation 2.0中均可使用。
JSR-303
注解 | 详细信息 |
---|---|
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
@AssertTrue | 只能为true |
@AssertFalse | 只能是false |
JSR-349
相比1.0没有新增。
JSR-380
注解 | 详细信息 |
---|---|
元素必须为电子邮箱地址 | |
@NotEmpty | 集合的Size必须大于0 |
@NotBlank | 字符串必须包含至少一个非空白的字符 |
@Positive | 元素必须为正数(不包括0) |
@PositiveOrZero | 同上(包括0) |
@Negative | 元素必须为负数(不包括0) |
@NegativeOrZero | 同上(包括0) |
@PastOrPresent | 在@Past基础上包括相等 |
@FutureOrPresent | 在@Futrue基础上包括相等 |
2.2 声明验证类别
2.2.1 Field-level constraints (字段级别的约束 - 非常常用)
验证引擎会直接访问到该对象的值,并对值进行校验,这种方式不会调用getter方法。
public class Car {
@NotNull
private String manufacturer;
@AssertTrue
private boolean isRegistered;
public Car(String manufacturer, boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
//getters and setters...
}
2.2.2 Property-level constraints (属性级别的约束)
当你的模型遵循JavaBean的规范的时候,可以把校验注解放到getter方法上进行校验。
public class Car {
private String manufacturer;
private boolean isRegistered;
public Car(String manufacturer, boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
@NotNull
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
@AssertTrue
public boolean isRegistered() {
return isRegistered;
}
public void setRegistered(boolean isRegistered) {
this.isRegistered = isRegistered;
}
}
2.2.3 Container element constraints (容器元素约束 - 2.0新增)
这里的注解@Target需要增加ElementType.TYPE_USE,支持的容器如下。
- 实现java.util.Iterable(例如List,Set)
- 实现java.util.Map,并支持键和值.
- java.util.Optional,java.util.OptionalInt,java.util.OptionalDouble,java.util.OptionalLong
- JavaFX的各种实现javafx.beans.observable.ObservableValue.
public class Car {
private Set<@NotNull String> parts = new HashSet<>();
public void addPart(String part) {
parts.add( part );
}
//...
}
测试类:
public class ValidTest {
private static Validator validator;
//用于验证参数和返回值的validator
private static ExecutableValidator executableValidator;
//使用init方法先进行初始化
@BeforeClass
public static void init(){
//创建校验工厂
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
//获取校验实例对象
validator = validatorFactory.getValidator();
executableValidator = validator.forExecutables();
}
@Test
public void test(){
Car car = new Car();
car.addPart( "Wheel" );
car.addPart( null );
Set<ConstraintViolation<Car6>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
ConstraintViolation<Car6> constraintViolation =
constraintViolations.iterator().next();
assertEquals(
"'null' is not a valid car part.",
constraintViolation.getMessage()
);
assertEquals( "parts[].<iterable element>",
constraintViolation.getPropertyPath().toString() );
}
}
2.2.4 Class-level constraints(类级别的约束)
即不是对某个属性起作用,而是针对的整个对象。配合自定义Validatator来使用(后面会讲到)。
2.2.5 Constraint inheritance(约束可继承)
说白了,就是子类在在做校验时,父类中的Validation的注解仍然有效。
//父类
public class Car3 {
@NotNull
private String manufacturer;//制造商
public Car3(@NotNull String manufacturer) {
this.manufacturer = manufacturer;
}
}
//子类 出租车
public class RentalCar extends Car3 {
public RentalCar(@NotNull String manufacturer, @DecimalMin(value = "10") Double price) {
super(manufacturer);
this.price = price;
}
@DecimalMin(value = "10")
private Double price;//起价费
}
测试类:
public class ValidTest {
private static Validator validator;
//用于验证参数和返回值的validator
private static ExecutableValidator executableValidator;
//使用init方法先进行初始化
@BeforeClass
public static void init(){
//创建校验工厂
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
//获取校验实例对象
validator = validatorFactory.getValidator();
executableValidator = validator.forExecutables();
}
@Test
public void test(){
RentalCar rc = new RentalCar(null,19.0);
Set<ConstraintViolation<RentalCar>> validate = validator.validate(rc);
for (ConstraintViolation<RentalCar> carConstraintViolation : validate) {
System.out.println("共有"+validate.size()+"条错误信息");
System.out.println("错误字段:"+carConstraintViolation.getPropertyPath());
System.out.println("错误信息:"+carConstraintViolation.getMessage());
}
}
}
2.2.6 Object graphs (级联验证,即@Valid)
当在使用的类中还含有其他需要校验的JavaBean,即可对这个字段或者属性加上@Valid,进行级联校验。
//Car4
public class Car4 {
@NotNull
@Valid //校验Car4的同时也会校验 Person
private Person person;
public Car4(@NotNull Person person) {
this.person = person;
}
}
// Person
public class Person {
@NotNull
private String name;
public Person(@NotNull String name) {
this.name = name;
}
}
测试类
public class ValidTest {
private static Validator validator;
//用于验证参数和返回值的validator
private static ExecutableValidator executableValidator;
//使用init方法先进行初始化
@BeforeClass
public static void init(){
//创建校验工厂
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
//获取校验实例对象
validator = validatorFactory.getValidator();
executableValidator = validator.forExecutables();
}
@Test
public void test(){
Person p = new Person(null);
Car4 car4 = new Car4(p);
Set<ConstraintViolation<Car4>> validate = validator.validate(car4);
for (ConstraintViolation<Car4> carConstraintViolation : validate) {
System.out.println("共有"+validate.size()+"条错误信息");
System.out.println("错误字段:"+carConstraintViolation.getPropertyPath());
System.out.println("错误信息:"+carConstraintViolation.getMessage());
}
}
}
2.3 验证约束
其实在前面的案例代码中已经出现了,就是Validator对象用于对constraint(约束)的校验。
以下会以代码片段的形式展示。
2.3.1 获取验证器实例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
2.3.2 验证器方法 - validate()
Car car = new Car( null, true );
//使用validate校验
Set<ConstraintViolation> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals( “must not be null”, constraintViolations.iterator().next().getMessage() );
2.3.3 验证器方法 - validateProperty()
Car car = new Car( null, true );
Set<ConstraintViolation> constraintViolations = validator.validateProperty(
car,
“manufacturer”
);
assertEquals( 1, constraintViolations.size() );
assertEquals( “must not be null”, constraintViolations.iterator().next().getMessage() );
2.3.4 验证器方法 - validateValue()
Set<ConstraintViolation> constraintViolations = validator.validateValue(
Car.class,
“manufacturer”,
null
);
assertEquals( 1, constraintViolations.size() );
assertEquals( “must not be null”, constraintViolations.iterator().next().getMessage() );
2.4 约束违反 - ConstraintViolation
即验证后如果出现不满足条件的结果集。
常用方法
方法 | 含义 |
---|---|
String getMessage(); | 错误信息 |
Path getPropertyPath(); | 返回字段节点返回字段节点路径,即字段名 |
2.5 自定义容器验证
2.5.1 @ScriptAssert(仅适合简单场景)
满足 JSR 223定义的Java支持的脚本规范,只要在classpath下配置了兼容引擎,几乎支持任何脚本语言
//这里以javascript举例
@ScriptAssert(lang = "javascript",alias = "this",script = "this.x > 2 && this.list.length > this.x")
/**
* 在Spring环境下,支持Spring的表达式
* https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
*/
//@ScriptAssert(lang = "spring",script = "x > 2")
public class Car12 {
@Positive
private int x;
public Car12(@Positive int x) {
this.x = x;
}
private List<String> list = new ArrayList<>();
public void addList(String str){
list.add(str);
}
}
测试类:
@Test
public void testBeanScript(){
Car12 car12 = new Car12(1);
Set<ConstraintViolation<Car12>> validate = validator.validate(car12);
for (ConstraintViolation<Car12> carConstraintViolation : validate) {
System.out.println("共有"+validate.size()+"条错误信息");
System.out.println("错误字段:"+carConstraintViolation.getPropertyPath());
System.out.println("错误信息:"+carConstraintViolation.getMessage());
}
}
执行结果:
共有1条错误信息
错误字段:
错误信息:执行脚本表达式"this.x > 2 && this.list.length > this.x"没有返回期望结果
2.5.2 继承ConstraintValidator
步骤:
-
自定义注解,定义
@Target
范围,定义必须要有的四个方法必须在自定义注解类定义的方法 含义 String message() 错误消息 value(); 自定义的value值 lass<? extends Payload>[] payload() bean Validator API的使用者可以通过约束条件指定严重级别 lass<?>[] groups() 分组的类 -
自定义类并继承ConstraintValidator,并且关联前面自定义的注解.话不多说,直接上代码
//自定义注解
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.PACKAGE,ElementType.CONSTRUCTOR,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
//关联下面的校验器类
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
String message() default "车牌号必须大写字母";
Class<?>[] groups() default {};
//bean Validator API的使用者可以通过贵约束条件指定严重级别
Class<? extends Payload>[] payload() default {};
CaseMode value();
}
**
* @program
* @description:
* @author:
* @create: 2020/11/23 12:33 AM
*
* 这里需要提供两个泛型
* * 第一个是校验器所服务的注解类型
* * 第二个是这个校验器所支持对的被校验元素的类型,@CheckCase,防止到String类型的字段上,那么第二个参数就是String
**/
public class CheckCaseValidator implements ConstraintValidator<CheckCase,String> {
private CaseMode caseMode;
/**
* 初始化需要完成的方法
* 可以用来获取@CheckCase 中的value的属性值
* @param constraintAnnotation
*/
@Override
public void initialize(CheckCase constraintAnnotation) {
caseMode = constraintAnnotation.value();
}
/**
* 进行校验的方法
* 传递的两个参数
* String value 传递的值
* @param value
* @param context
* @return true 通过 false 未通过
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// System.out.println("传递的值: "+value);
// System.out.println("使用的CaseMode: "+caseMode);
if (value == null){
return true;
}
boolean isValid;
if (CaseMode.UPPER.equals(caseMode)){
//如果都是大写,那转换大写后的结果应该与原字符串一致
isValid = value.equals(value.toUpperCase());
}else {
//如果都是小写,那转换小写后的结果应该与原字符串一致
isValid = value.equals(value.toLowerCase());
}
if (!isValid){
//自定义错误信息
//1.关闭默认的校验信息,即CheckCase注解中的message信息
context.disableDefaultConstraintViolation();
//2.自定义错误信息
context.buildConstraintViolationWithTemplate("自定义错误:车牌号必须都是大写")
.addPropertyNode("error").addConstraintViolation();
}
return isValid;
}
}
测试类:
@Test
public void testMyValid(){
Car5 car5 = new Car5(null,null,1);
Set<ConstraintViolation<Car5>> validate = validator.validate(car5);
for (ConstraintViolation<RentalCar> carConstraintViolation : validate) {
System.out.println("共有"+validate.size()+"条错误信息");
System.out.println("错误字段:"+carConstraintViolation.getPropertyPath());
System.out.println("错误信息:"+carConstraintViolation.getMessage());
}
}
2.6 ExecutableValidator - 用于验证参数和返回值的validator
2.6.1 获取一个ExecutableValidator实例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();
2.6.2 ExecutableValidator方法
用于方法验证:
- validateParameters() 入参验证
- validateReturnValue() 返回值验证
用于构造函数验证: - validateConstructorParameters() 构造函数入参验证
- validateConstructorReturnValue() 返回值验证
2.6.2.1 ExecutableValidator#validateParameters()
分别有三个关键参数
object | 被校验类 | Object对象 |
method | 被校验方法 | Method对象 |
parameterValues | 参数数组 | Object[] |
示例代码:
Car object = new Car( "Morris" );
Method method = Car.class.getMethod( "drive", int.class );
Object[] parameterValues = { 80 };
Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters(
object,
method,
parameterValues
);
assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
.next()
.getConstraintDescriptor()
.getAnnotation()
.annotationType();
assertEquals( Max.class, constraintType );
2.6.2.2 ExecutableValidator#validateReturnValue()
分别有三个关键参数
object | 被校验类 | Object对象 |
method | 被校验方法 | Method对象 |
returnValue | 返回值 | Objec对象 |
示例代码:
Car object = new Car( "Morris" );
Method method = Car.class.getMethod( "getPassengers" );
Object returnValue = Collections.<Passenger>emptyList();
Set<ConstraintViolation<Car>> violations = executableValidator.validateReturnValue(
object,
method,
returnValue
);
assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
.next()
.getConstraintDescriptor()
.getAnnotation()
.annotationType();
assertEquals( Size.class, constraintType );
2.6.2.3 ExecutableValidator#validateConstructorParameters()
Constructor<Car> constructor = Car.class.getConstructor( String.class );
Object[] parameterValues = { null };
Set<ConstraintViolation<Car>> violations = executableValidator.validateConstructorParameters(
constructor,
parameterValues
);
assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
.next()
.getConstraintDescriptor()
.getAnnotation()
.annotationType();
assertEquals( NotNull.class, constraintType );
2.6.2.4 ExecutableValidator#validateConstructorReturnValue()
//constructor for creating racing cars
Constructor constructor = Car.class.getConstructor( String.class, String.class );
Car createdObject = new Car( “Morris”, null );
Set<ConstraintViolation> violations = executableValidator.validateConstructorReturnValue(
constructor,
createdObject
);
assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
.next()
.getConstraintDescriptor()
.getAnnotation()
.annotationType();
assertEquals( ValidRacingCar.class, constraintType );
2.7 message自定义
2.7.1 在注解的参数列表里面都会有message()字样,定义message
public class Car1 {
@NotNull(message = "制造商名称不能为空")
private String manufacturer;//制造商
@AssertTrue(message = "出厂时,必须注册,设置为true")
private boolean isRegistered;//是否进行了注册
public Car1(@NotNull String manufacturer, @AssertTrue boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
}
2.7.2 自定义ValidationMessages.properties(可以支持动态的错误提示)
在resources下新创建一个properties,命名为ValidationMessages.properties
# 先用工具转换成unicode 然后把其中的动态参数使用{}包起来,即可动态展示了
com.hv.domain.Car.licensePlate = \u8f66\u724c\u53f7\u5fc5\u987b\u5728{min}\u5230{max}\u4f4d\u4e4b\u95f4
public class Car5 {
/**
* 这里的CarChecks.class表示一个组,校验的时候可以针对标注组的标签进行判断
*/
@NotNull(message = "制造商不能为空",groups = CarChecks.class)
private String manufacturer;//制造商
@NotNull
//注意这一行 min、max都是属于@Size注解的动态参数
//message的参数用{}中的key就是properties里面配置的自定义message
@Size(min = 2, max = 14,message = "{com.hv.domain.Car.licensePlate}")
private String licensePlate;//车牌号
@Min(2)
private int seatCount;//座位
public Car5(String manufacturer, String licensePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licensePlate;
this.seatCount = seatCount;
}
//...
}
测试类:
@Test
public void testMyValid(){
Car5 car5 = new Car5(null,"1",1);
Set<ConstraintViolation<Car5>> validate = validator.validate(car5);
for (ConstraintViolation<Car5> carConstraintViolation : validate) {
System.out.println("共有"+validate.size()+"条错误信息");
System.out.println("校验字段出现错误:字段:"+carConstraintViolation.getPropertyPath()+carConstraintViolation.getMessage());
}
}
输出结果:
共有2条错误信息
校验字段出现错误:字段:licensePlate车牌号必须在2到14位之间 (这里就是我们在配置文件中动态错误提示)
校验字段出现错误:字段:seatCount最小不能小于2
三.与Spring AOP整合,在Service层支持Bean Validation(用于网关层)
目前在Spring中暂时支持到Spring-mvc,在Service层还没有得到支持,同时,结合公司的框架,也是属于Service层,刚好可以利用ExecutableValidator校验器和Spring AOP来帮助我们解决这个问题。
具体的配置如下了。
@Configuration
@Aspect
@Slf4j
public class AdviceConfiguration{
//定义Service使用validate
private static Validator validator;
static {
validator = Validation.byDefaultProvider()
.configure()
.buildValidatorFactory()
.getValidator();
}
@Pointcut(value = "execution(public * com.xzk.blog.service..*(..))")
private void validateMethod(){
}
public void beforeValidCheck2(JoinPoint joinPoint){
System.out.println(joinPoint);
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 执行方法参数的校验
ExecutableValidator executableValidator = validator.forExecutables();
Set<ConstraintViolation<Object>> constraintViolations = executableValidator.validateParameters(joinPoint.getThis(), signature.getMethod(), args);
List<String> messages = Lists.newArrayList();
for (ConstraintViolation<Object> error : constraintViolations) {
messages.add(error.getMessage());
}
if(!messages.isEmpty()){
throw new RuntimeException(JSONObject.toJSONString(messages));
}
}
使用方法,直接在接口参数列表添加Validation注解。
如果是接口的形式
没有接口的形式:
⚠️注意事项:使用注解时,一定要放在接口层,实现类可加可不加,不能只在实现类加,接口层不加!
四.补充
本次的内容不算很多,还有好多特性,像group、ValueExtractor等概念和一些验证器的加载(SPI)等内容还未提到, 更多的内容,我已经放到了附录文档,便于各位学习,文档永远是学习框架最快的。
Hibernate Validator 6.0英文原版文档链接
https://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/
打不开,PDF下载链接
📎hibernate_validator_reference.pdf.Bean Validation 数据校验实战