5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 1024,撸起袖子继续干

1024,代码改变世界。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。

✍前言

你好,我是YourBatman。又一年1024程序员节,你快乐吗?还是在加班上线呢?

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

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

版本约定

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

✍正文

Jakarta Bean它的验证约束是通过声明式方式(注解)来表达的,我们知道Java注解几乎可以标注在任何地方(package上都可标注注解你敢信?),那么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就是一常用案例。

说明:为简化接下来示例代码,共用工具代码提前展示如下:

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);
    }

}

1、字段级别约束(Field)

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

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()得到字段的值

使用细节

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

若你的对象会被字节码增强,那么请不要使用Field约束,而是使用下面介绍的属性级别约束更为合适。

原因:增强过的类并不一定能通过字段反射去获取到它的值

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

2、属性级别约束(Property)

若一个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来获取待校验的值。

使用细节

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

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

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

List<Room> beans = new ArrayList<>();
for (Room bean : beans) {
    validate(bean);
    ...
}

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

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

在本专栏第一篇知道了从Bean Validation 2.0开始就支持容器元素校验了(本专栏使用版本为:2.02),下面我们来体验一把:

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 = "YourBatman";
    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 = "YourBatman";
    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这个注解就不是必须的了

使用细节

  1. 若约束注解想标注在容器元素上,那么注解定义的@Target里必须包含TYPE_USE(Java8新增)这个类型

    1. BV和HV(除了Class级别)的所有注解均能标注在容器元素上
  2. BV规定了可以验证容器内元素,HV提供实现。它默认支持如下容器类型:

    1. java.util.Iterable的实现(如List、Set)
    2. java.util.Map的实现,支持key和value
    3. java.util.Optional/OptionalInt/OptionalDouble...
    4. JavaFX的javafx.beans.observable.ObservableValue
    5. 自定义容器类型(自定义很重要,详见下篇文章)

4、类级别约束(Class)

类级别的约束验证是很多同学不太熟悉的一块,但它却很是重要。

其实Hibernate-Validator已内置提供了一部分能力,但可能还不够,很多场景需要自己动手优雅解决。为了体现此part的重要性,我决定专门撰文描述,当然还有自定义容器类型类型的校验喽,我们下文见。

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

字段(Field) VS 属性(Property)本身就属于一对“近义词”,很多时候口头上我们并不做区分,是因为在POJO里他俩一般都同时存在,因此大多数情况下可以对等沟通。比如:

@Data
public class Room {
    @NotNull
    private String name;
    @AssertTrue
    private boolean finished;
}

字段和属性的区别

  1. 字段具有存储功能:字段是类的一个成员,值在内存中真实存在;而属性它不具有存储功能,属于Java Bean规范抽象出来的一个叫法
  2. 字段一般用于类内部(一般是private),而属性可供外部访问(get/set一般是public)

    1. 这指的是一般情况下的规律
  3. 字段的本质是Field,属性的本质是Method
  4. 属性并不依赖于字段而存在,只是他们一般都成双成对出现

    1. getClass()你可认为它有名为class的属性,但是它并没有名为class的字段

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

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

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

✍总结

嗯,这篇文章还不错吧,总体浏览下来行文简单,但内容还是挺干的哈,毕竟1024节嘛,不来点的干的心里有愧。

作为此part姊妹篇的上篇,它是每个同学都有必要掌握的使用方式。而下篇我觉得应该更为兴奋些,毕竟那里才能加分。1024,撸起袖子继续干。

✔推荐阅读:
目录
相关文章
|
4月前
|
存储 C++ 容器
【C++】STL容器——vector类的使用指南(含代码演示)(11)
【C++】STL容器——vector类的使用指南(含代码演示)(11)
|
3月前
|
安全 Java 编译器
容器【泛型类、泛型接口、泛型方法 、泛型方法与可变参数 】(一)-全面详解(学习总结---从入门到深化)
容器【泛型类、泛型接口、泛型方法 、泛型方法与可变参数 】(一)-全面详解(学习总结---从入门到深化)
27 0
|
4月前
|
XML Java 数据格式
掌握 Spring IoC 容器与 Bean 作用域:详解 singleton 与 prototype 的使用与配置
在您的应用程序中,由 Spring IoC 容器管理的形成其核心的对象被称为 "bean"。一个 bean 是由 Spring IoC 容器实例化、组装和管理的对象
48 0
|
22天前
|
Java 数据库连接 Spring
|
25天前
|
存储 安全 算法
【C++ 17 包裹类 泛型容器 std::any】深入理解与应用C++ std::any:从泛型编程到多态设计
【C++ 17 包裹类 泛型容器 std::any】深入理解与应用C++ std::any:从泛型编程到多态设计
47 1
|
1月前
|
小程序 前端开发 定位技术
【微信小程序】-- 常用视图容器类组件介绍 -- view、scroll-view和swiper(六)
【微信小程序】-- 常用视图容器类组件介绍 -- view、scroll-view和swiper(六)
|
1月前
|
安全 Java API
Java并发 - J.U.C并发容器类 list、set、queue
Queue API 阻塞是通过 condition 来实现的,可参考 Java 并发 - Lock 接口 ArrayBlockingQueue 阻塞 LinkedBlockingQueue 阻塞 ArrayQueue 非阻塞 LinkedQueue 非阻塞
|
1月前
|
XML Java 数据格式
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界 (下)
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界
|
1月前
|
XML Java 数据格式
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界 (上)
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界 (上)
|
3月前
|
安全 Java 编译器
Java并发编程学习6-同步容器类和并发容器
【1月更文挑战第6天】本篇介绍同步容器类和并发容器的相关内容(Vector、ConcurrentHashMap、CopyOnWriteArrayList)
34 3
Java并发编程学习6-同步容器类和并发容器