本文主要讲述Bean Validation声明式验证四大级别:字段、属性、容器元素、类。
Jakarta Bean它的验证约束是通过声明式方式(注解)来表达的,我们知道Java注解几乎可以标注在任何地方(package上都可标注注解你敢信?),那么Jakarta Bean支持哪些呢?
Jakarta Bean共支持四个级别的约束:
值得注意的是,并不是所有的约束注解都能够标注在上面四种级别上。现实情况是:Bean Validation
自带的22个标准约束全部支持1/2/3级别,且全部不支持第4级别(类级别)约束。当然喽,作为补充的Hibernate-Validator
它提供了一些专门用于类级别的约束注解,如org.hibernate.validator.constraints.@ScriptAssert
就是一常用案例。
说明:为简化接下来示例代码,共用工具代码提前展示如下:
public abstract class ValidatorUtil {
public static ValidatorFactory obtainValidatorFactory() {
return Validation.buildDefaultValidatorFactory();
}
public static Validator obtainValidator() {
return obtainValidatorFactory().getValidator();
}
public static ExecutableValidator obtainExecutableValidator() {
return obtainValidator().forExecutables();
}
public static <T> void printViolations(Set<ConstraintViolation<T>> violations) {
violations.stream().map(v -> v.getPropertyPath() + v.getMessage() + ",但你的值是: " + v.getInvalidValue()).forEach(System.out::println);
}
}
这是我们最为常用的一种约束方式:
public class Room {
@NotNull
public String name;
@AssertTrue
public boolean finished;
}
书写测试用例:
public static void main(String[] args) {
Room bean = new Room();
bean.finished = false;
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(bean));
}
运行程序,输出:
finished只能为true,但你的值是: false
name不能为null,但你的值是: null
当把约束标注在Field字段上时,Bean Validation将使用字段的访问策略来校验,不会调用任何方法,即使你提供了对应的get/set方法也不会触碰。
话外音:使用Field#get()得到字段的值
若你的对象会被字节码增强,那么请不要使用Field约束,而是使用下面介绍的属性级别约束更为合适。
原因:增强过的类并不一定能通过字段反射去获取到它的值
动态代理分为: cglib和jdk动态代理,cglib动态代理生产的代理对象是继承了目标对象的,但是getDeclaredFields方法,只能获取到当前对象内部的所有属性,不包括父类的。
绝大多数情况下,对Field字段做约束的话均是POJO,被增强的可能性极小,因此此种方式是被推荐的,看着清爽。
若一个Bean遵循Java Bean规范,那么也可以使用属性约束来代替字段约束。比如上例可改写为如下
public class Room {
public String name;
public boolean finished;
@NotNull
public String getName() {
return name;
}
@AssertTrue
public boolean isFinished() {
return finished;
}
}
执行上面相同的测试用例,输出:
finished只能为true,但你的值是: false
name不能为null,但你的值是: null
效果“完全”一样。
当把约束标注在Property属性上时,将采用属性访问策略来获取要验证的值。说白了:会调用你的Method来获取待校验的值。
如果大家了解Spring的属性访问策略的话,应该知道BeanWrapper和DirectFieldAccessor,前者通过Java内省的方式访问对象属性,即Getter和Setter方法的方式,后者基于反射的方式访问对象的属性。
这里对于属性校验逻辑也是同理,默认会去查看属性上是否标注了相关注解和属性对应的getter方法上是否标注了相关注解,然后都会去执行。
如果getter方法上标注了相关约束注解,那么会执行getter方法,获取到其对应的返回值,然后执行校验逻辑。
如果再进行Java Bean对象校验时,将约束注解标注在了非getter方法上,则对应不会去执行,但是如果标注在了非getter方法上,并且该方法返回值为void,则会抛出异常。
还有一种非常非常常见的验证场景:验证容器内(每个)元素,也就验证参数化类型parameterized type。形如List希望里面装的每个Room都是合法的,传统的做法是在for循环里对每个room进行验证:
List<Room> beans = new ArrayList<>();
for (Room bean : beans) {
validate(bean);
...
}
很明显这么做至少存在下面两个不足:
从Bean Validation 2.0开始就支持容器元素校验了,下面我们来体验一把:
public class Room {
@NotNull
public String name;
@AssertTrue
public boolean finished;
}
书写测试用例:
public static void main(String[] args) {
List<@NotNull Room> rooms = new ArrayList<>();
rooms.add(null);
rooms.add(new Room());
Room room = new Room();
room.name = "DHY";
rooms.add(room);
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
}
运行程序,没有任何输出,也就是说并没有对rooms立面的元素进行验证。这里有一个误区:Bean Validator是基于Java Bean进行验证的,而此处你的rooms仅仅只是一个容器类型的变量而已,因此不会验证。
其实它是把List当作一个Bean,去验证List里面的标注有约束注解的属性/方法。很显然,List里面不可能标注有约束注解嘛,所以什么都不输出
为了让验证生效,我们只需这么做:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Rooms {
private List<@Valid @NotNull Room> rooms;
}
public static void main(String[] args) {
List<@NotNull Room> beans = new ArrayList<>();
beans.add(null);
beans.add(new Room());
Room room = new Room();
room.name = "DHY";
beans.add(room);
// 必须基于Java Bean,验证才会生效
Rooms rooms = new Rooms(beans);
ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
}
运行程序,输出:
rooms[0].<list element>不能为null,但你的值是: null
rooms[2].finished只能为true,但你的值是: false
rooms[1].name不能为null,但你的值是: null
rooms[1].finished只能为true,但你的值是: false
rooms[1].finished只能为true,但你的值是: false
从日志中可以看出,元素的验证顺序是不保证的。
小贴士:在HV 6.0 之前的版本中,验证容器元素时@Valid是必须,也就是必须写成这样:List<@Valid @NotNull Room> rooms才有效。在HV 6.0之后@Valid这个注解就不是必须的了
若约束注解想标注在容器元素上,那么注解定义的@Target里必须包含TYPE_USE(Java8新增)这个类型
BV和HV(除了Class级别)的所有注解均能标注在容器元素上
BV规定了可以验证容器内元素,HV提供实现。它默认支持如下容器类型:
java.util.Iterable的实现(如List、Set)
java.util.Map的实现,支持key和value
java.util.Optional/OptionalInt/OptionalDouble…
JavaFX的javafx.beans.observable.ObservableValue
自定义容器类型(自定义很重要,详见下篇文章)
类级别的约束验证是很多同学不太熟悉的一块,但它却很是重要。
字段(Field) VS 属性(Property)本身就属于一对“近义词”,很多时候口头上我们并不做区分,是因为在POJO里他俩一般都同时存在,因此大多数情况下可以对等沟通。比如:
@Data
public class Room {
@NotNull
private String name;
@AssertTrue
private boolean finished;
}
字段和属性的区别:
知晓了字段和属性的区别,再去理解字段约束和属性约束的差异就简单了,它俩的差异仅仅体现在待验证值访问策略上的区别:
小贴士:如果你希望执行了验证就输出一句日志,又或者你的POJO被字节码增强了,那么属性约束更适合你。否则,推荐使用字段约束
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://cjdhy.blog.csdn.net/article/details/125945263
内容来源于网络,如有侵权,请联系作者删除!