前言

上篇文章介绍了 Bean Validation 校验器的五大核心组件,在结合前面几篇所讲,相信你对 Bean Validation 已经有了一个整体认识。


本文将非常实用,因为将要讲述的是 Bean Validation 在 4 个层级上的验证方式,它将覆盖你使用过程中的方方面面。

版本约定

  • Bean Validation 版本:2.0.2
  • Hibernate Validator 版本:6.1.5.Final

    正文

Jakarta Bean 它的验证约束是通过声明式方式(注解)来表达的,我们知道 Java 注解几乎可以标注在任何地方,那么 Jakarta Bean 支持哪些呢?


Jakarta Bean 共支持四个级别的约束:

  1. 字段约束(Field)
  2. 属性约束(Property)
  3. 容器元素约束(Container Element)
  4. 类约束(Class)

值得注意的是,并不是所有的约束注解都能够标注在上面四种级别上。现实情况是 Bean Validation 自带的 22 个标准约束全部支持 1/2/3 级别,且全部不支持第 4 级别(类级别)约束。作为补充的 Hibernate-Validator 它提供了一些专门用于类级别的约束注解,如 org.hibernate.validator.constraints.@ScriptAssert 就是一常用案例。

为了简化接下来示例代码,对一些简单、公用的方法抽取如下:

  1. public class ValidatorUtil {
  2. public static ValidatorFactory getValidatorFactory() {
  3. return Validation.buildDefaultValidatorFactory();
  4. }
  5. public static Validator getValidator() {
  6. return obtainValidatorFactory().getValidator();
  7. }
  8. public static ExecutableValidator getExecutableValidator() {
  9. return obtainValidator().forExecutables();
  10. }
  11. public static <T> void printViolations(Set<ConstraintViolation<T>> violations) {
  12. violations.stream().map(v -> v.getPropertyPath() + v.getMessage() + ",但你的值是: " + v.getInvalidValue()).forEach(System.out::println);
  13. }
  14. }

字段级别约束(Field)

这是我们最为常用的一种约束方式:

  1. public class Room {
  2. @NotNull
  3. public String name;
  4. @AssertTrue
  5. public boolean finished;
  6. }

测试用例:

  1. public static void main(String[] args) {
  2. Room bean = new Room();
  3. bean.finished = false;
  4. ValidatorUtil.printViolations(ValidatorUtil.getValidator().validate(bean));
  5. }

运行程序,控制台输出:

  1. finished只能为true,但你的值是: false
  2. name不能为null,但你的值是: null

当把约束标注在 Field 字段上时,Bean Validation 将使用字段的访问策略来校验(使用 Field#get() 得到字段的值),不会调用任何方法,即使你提供了对应的 get/set 方法也不会触碰。

使用细节

  • 字段约束可以应用于任何访问修饰符的字段;
  • 不支持对静态字段的约束(static 静态字段使用约束无效)。

若你的对象会被字节码增强,那么请不要使用 Field 约束,而是使用下面介绍的属性级别约束更为合适,因为增强过的类并不一定能通过字段反射获取到它的值。

绝大多数情况下,对 Field 字段做约束的话均是 POJO,被增强的可能性极小,因此此种方式是被推荐的,看着清爽。

属性级别约束(Property)

若一个 Bean 遵循 Java Bean 规范,那么也可以使用属性约束来代替字段约束。比如上例可改写为如下:

  1. public class Room {
  2. public String name;
  3. public boolean finished;
  4. @NotNull
  5. public String getName() {
  6. return name;
  7. }
  8. @AssertTrue
  9. public boolean isFinished() {
  10. return finished;
  11. }
  12. }

执行上面相同的测试用例,输出:

  1. finished只能为true,但你的值是: false
  2. name不能为null,但你的值是: null

执行结果一样, 当把约束标注在 Property 属性上时,将采用属性访问策略来获取要验证的值,说白了,会调用你的 Method 来获取待校验的值。

使用细节

  • 约束放在 get 方法上优于放在 set 方法上,这样只读属性(没有 set 方法)依然可以执行约束逻辑;
  • 不要在属性和字段上都标注注解,否则会重复执行约束逻辑(有多少个注解就执行多少次);
  • 不要既在属性的 get 方法上又在 set 方法上标注约束注解。

    容器元素级别约束(Container Element)

还有一种非常常见的验证场景,验证容器内(每个)元素,也就是验证参数化类型 parameterized type。比如 List 希望里面装的每个 Room 对象都是合法的,传统的做法是在 for 循环里对每个 Room 进行验证:

  1. List<Room> beans = new ArrayList<>();
  2. for (Room bean : beans) {
  3. validate(bean);
  4. ...
  5. }

很明显这么做至少存在下面两个不足:

  1. 验证逻辑具有侵入性;
  2. 验证逻辑是黑匣子(不看内部源码无法知道你有哪些约束),非声明式。

从 Bean Validation 2.0 开始支持容器元素校验,下面我们来体验一下:

  1. public class Room {
  2. @NotNull
  3. public String name;
  4. @AssertTrue
  5. public boolean finished;
  6. }

测试用例:

  1. public static void main(String[] args) {
  2. List<@NotNull Room> rooms = new ArrayList<>();
  3. rooms.add(null);
  4. rooms.add(new Room());
  5. Room room = new Room();
  6. room.name = "YourBatman";
  7. rooms.add(room);
  8. ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
  9. }

运行程序,没有任何输出,也就是说并没有对 rooms 里面的元素进行验证。这里有一个误区,Bean Validator 是基于 Java Bean 进行验证的,而此处你的 rooms 仅仅只是一个容器类型的变量,因此不会验证。

其实它是把 List 当作一个 Bean,去验证 List 里面的标注有约束注解的属性和方法。很显然,List 里面不可能标注有约束注解,所以什么都不会输出。

为了让验证生效,我们只需这么做:

  1. @Data
  2. @NoArgsConstructor
  3. @AllArgsConstructor
  4. public class Rooms {
  5. private List<@Valid @NotNull Room> rooms;
  6. }
  7. public static void main(String[] args) {
  8. List<@NotNull Room> beans = new ArrayList<>();
  9. beans.add(null);
  10. beans.add(new Room());
  11. Room room = new Room();
  12. room.name = "YourBatman";
  13. beans.add(room);
  14. // 必须基于Java Bean,验证才会生效
  15. Rooms rooms = new Rooms(beans);
  16. ValidatorUtil.printViolations(ValidatorUtil.obtainValidator().validate(rooms));
  17. }

运行程序,输出:

  1. rooms[0].<list element>不能为null,但你的值是: null
  2. rooms[2].finished只能为true,但你的值是: false
  3. rooms[1].name不能为null,但你的值是: null
  4. rooms[1].finished只能为true,但你的值是: false
  5. rooms[1].finished只能为true,但你的值是: false

从日志中可以看出,元素的验证顺序是不保证的。

在 Hibernate Validator 6.0 之前的版本中,验证容器元素时 @Valid 是必须,也就是必须写成这样:List<@Valid @NotNull Room> rooms 才有效。在 Hibernate Validator 6.0 之后 @Valid 这个注解就不是必须的了。

使用细节

  • 若约束注解想标注在容器元素上,那么注解定义的 @Target 里必须包含 TYPE_USE(Java 8 新增)这个类型;
    • Bean Validation 和 Hibernate Validator(除了 Class 级别)的所有注解均能标注在容器元素上;
    • image.png
  • Bean Validation 规定了可以验证容器内元素,Hibernate Validator 提供实现,它默认支持如下容器类型:
    • java.util.Iterable 的实现(如 List、Set)
    • java.util.Map 的实现,支持 key 和 value
    • java.util.Optional/OptionalInt/OptionalDouble…
    • JavaFX 的 javafx.beans.observable.ObservableValue
    • 自定义容器类型(自定义很重要,详见下篇文章)

      类级别约束(Class)


其实 Hibernate-Validator 已内置提供了一部分能力,但可能还不够,很多场景需要自己动手解决。

为了体现此部分的重要性,我决定专门撰文描述,当然还有自定义容器类型的校验,我们下文见。

字段约束和属性约束的区别

  1. 字段具有存储功能,字段是类的一个成员,值在内存中真实存在,而属性它不具有存储功能,属于 Java Bean 规范抽象出来的一个叫法;
  2. 字段一般用于类内部(一般是 private),而属性可供外部访问(get / set一般是 public);
    1. 这指的是一般情况下的规律。
  3. 字段的本质是 Field,属性的本质是 Method;
  4. 属性并不依赖于字段而存在,只是他们一般都成双成对出现;
    1. 如 getClass() 你可认为它有名为 class 的属性,但是它并没有名为 class 的字段。

知晓了字段和属性的区别,再去理解字段约束和属性约束的差异就简单了,它俩的差异仅仅体现在待验证值访问策略上的区别:

  • 字段约束:直接反射访问字段的值 -> Field#get(不会执行 get 方法体)
  • 属性约束:调用属性 get 方法 -> getXXX(会执行 get 方法体)

如果你希望执行了验证就输出一句日志,又或者你的 POJO 被字节码增强了,那么属性约束更适合你,否则,推荐使用字段约束。

总结

本文描述了 Bean Validation 在 4 个层级上的验证方式,类级别的验证我们很少考虑,其实使用场景还是蛮多的,比如关联字段的验证,后文会详细描述。

转载

打个广告,方便的话,可以关注一下 A哥(YourBatman) 的公众号。
image.png

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/egicbt 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。