前言

验证数据是在所有应用程序层中进行的常见任务,从表示层到持久层。通常在每个层中实现相同的验证逻辑,这既耗时又容易出错。为了避免这些验证的重复,开发人员经常将验证逻辑直接捆绑到域模型中,使域类充斥着验证代码,而这些代码实际上是关于类本身的元数据。

application layers

Jakarta Bean Validation 3.0 定义了实体和方法验证的元数据模型和 API。默认的元数据源是注释,可以通过使用 XML 来覆盖和扩展元数据。API 不绑定到特定的应用程序层或编程模型。它尤其没有绑定到 Web 层或持久层,并且可用于服务器端应用程序编程以及富客户端 Swing 应用程序开发人员。

application layers2

Hibernate Validator 是 Jakarta Bean Validation 的参考实现。该实现本身以及 Jakarta Bean Validation API 和 TCK 都在 Apache 软件许可证 2.0 下提供和分发。

Hibernate Validator 8 和 Jakarta Bean Validation 3.0 需要 Java 11 或更高版本。

1. 入门

本章将向您展示如何开始使用 Hibernate Validator,Jakarta Bean Validation 的参考实现 (RI)。以下快速入门需要

  • JDK 8

  • Apache Maven

  • 互联网连接(Maven 需要下载所有必要的库)

1.1. 项目设置

为了在 Maven 项目中使用 Hibernate Validator,只需将以下依赖项添加到您的 pom.xml

示例 1.1:Hibernate Validator Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>

这将传递性地引入对 Jakarta Bean Validation API 的依赖项 (jakarta.validation:jakarta.validation-api:3.0.2)。

1.1.1. 统一 EL

Hibernate Validator 需要 Jakarta 表达式语言 的实现来评估约束违规消息中的动态表达式(请参阅 第 4.1 节,“默认消息插值”)。当您的应用程序在 Jakarta EE 容器(如 WildFly/JBoss EAP)中运行时,容器已经提供了 EL 实现。但是,在 Java SE 环境中,您必须将实现作为依赖项添加到您的 POM 文件中。例如,您可以添加以下依赖项来使用 Jakarta EL 的 参考实现

示例 1.2:统一 EL 参考实现的 Maven 依赖项
<dependency>
    <groupId>org.glassfish.expressly</groupId>
    <artifactId>expressly</artifactId>
    <version>5.0.0</version>
</dependency>

对于无法提供 EL 实现的环境,Hibernate Validator 提供了 第 12.10 节,“ParameterMessageInterpolator。但是,使用此插值器不符合 Jakarta Bean Validation 规范。

1.1.2. CDI

Jakarta Bean Validation 定义了与 CDI 的集成点 (Jakarta EE 的上下文和依赖注入)。如果您的应用程序在没有开箱即用提供此集成的环境中运行,您可以使用 Hibernate Validator CDI 可移植扩展,方法是将以下 Maven 依赖项添加到您的 POM 中

示例 1.3:Hibernate Validator CDI 可移植扩展 Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-cdi</artifactId>
    <version>8.0.1.Final</version>
</dependency>

请注意,对于在 Jakarta EE 应用程序服务器上运行的应用程序,通常不需要添加此依赖项。您可以在 第 11.3 节,“CDI” 中详细了解 Jakarta Bean Validation 和 CDI 的集成。

1.1.3. 在安全管理器下运行

Hibernate Validator 支持在启用了 安全管理器 的情况下运行。为此,您必须将几个权限分配给 Hibernate Validator、Jakarta Bean Validation API、Classmate 和 JBoss Logging 的代码库,以及调用 Jakarta Bean Validation 的代码库。以下展示了如何通过 策略文件 来实现,该文件由 Java 默认策略实现处理

示例 1.4:使用安全管理器的 Hibernate Validator 策略文件
grant codeBase "file:path/to/hibernate-validator-8.0.1.Final.jar" {
    permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
    permission java.lang.RuntimePermission "accessDeclaredMembers";
    permission java.lang.RuntimePermission "setContextClassLoader";

    permission org.hibernate.validator.HibernateValidatorPermission "accessPrivateMembers";

    // Only needed when working with XML descriptors (validation.xml or XML constraint mappings)
    permission java.util.PropertyPermission "mapAnyUriToUri", "read";
};

grant codeBase "file:path/to/jakarta.validation-api-3.0.2.jar" {
    permission java.io.FilePermission "path/to/hibernate-validator-8.0.1.Final.jar", "read";
};

grant codeBase "file:path/to/jboss-logging-3.4.3.Final.jar" {
    permission java.util.PropertyPermission "org.jboss.logging.provider", "read";
    permission java.util.PropertyPermission "org.jboss.logging.locale", "read";
};

grant codeBase "file:path/to/classmate-1.5.1.jar" {
    permission java.lang.RuntimePermission "accessDeclaredMembers";
};

grant codeBase "file:path/to/validation-caller-x.y.z.jar" {
    permission org.hibernate.validator.HibernateValidatorPermission "accessPrivateMembers";
};

1.1.4. 在 WildFly 中更新 Hibernate Validator

WildFly 应用程序服务器 自带 Hibernate Validator。为了将 Jakarta Bean Validation API 和 Hibernate Validator 的服务器模块更新到最新版本,可以使用 WildFly 的修补机制。

您可以从 SourceForge 或使用以下依赖项从 Maven Central 下载修补程序文件

示例 1.5:WildFly 27.0.0.Alpha4 修补程序文件的 Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-modules</artifactId>
    <version>8.0.1.Final</version>
    <classifier>wildfly-27.0.0.Alpha4-patch</classifier>
    <type>zip</type>
</dependency>

我们还为 WildFly 提供了一个修补程序

示例 1.6:WildFly 修补程序文件的 Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-modules</artifactId>
    <version>8.0.1.Final</version>
    <classifier>wildfly--patch</classifier>
    <type>zip</type>
</dependency>

下载完修补程序文件后,您可以通过运行以下命令将其应用于 WildFly

示例 1.7:应用 WildFly 修补程序
$JBOSS_HOME/bin/jboss-cli.sh patch apply hibernate-validator-modules-8.0.1.Final-wildfly-27.0.0.Alpha4-patch.zip

如果您想撤销修补程序并返回到服务器最初提供的 Hibernate Validator 版本,请运行以下命令

示例 1.8:回滚 WildFly 修补程序
$JBOSS_HOME/bin/jboss-cli.sh patch rollback --reset-configuration=true

您可以在以下位置了解有关 WildFly 修补基础设施的更多一般信息:这里这里

1.1.5. 在 Java 11 及更高版本上运行

对 Java 11 和 Java 平台模块系统 (JPMS) 的支持处于初步阶段。没有提供 JPMS 模块描述符,但 Hibernate Validator 可用作自动模块。

这些是使用 Automatic-Module-Name 标头声明的模块名称

  • Jakarta Bean Validation API:jakarta.validation

  • Hibernate Validator 核心:org.hibernate.validator

  • Hibernate Validator CDI 扩展:org.hibernate.validator.cdi

  • Hibernate Validator 测试实用程序:org.hibernate.validator.testutils

  • Hibernate Validator 注解处理器:org.hibernate.validator.annotationprocessor

这些模块名称是初步的,在未来的版本中提供真正的模块描述符时可能会更改。

1.2. 应用约束

让我们直接深入一个示例,看看如何应用约束。

示例 1.9:使用约束注释的类 Car
package org.hibernate.validator.referenceguide.chapter01;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

    @Min(2)
    private int seatCount;

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    //getters and setters ...
}

@NotNull@Size@Min 注解用于声明应应用于 Car 实例字段的约束

  • manufacturer 绝不能为 null

  • licensePlate 绝不能为 null 并且必须介于 2 到 14 个字符之间

  • seatCount 必须至少为 2

您可以在 GitHub 上的 Hibernate Validator 源代码库 中找到此参考指南中使用的所有示例的完整源代码。

1.3. 验证约束

要执行这些约束的验证,您将使用 Validator 实例。让我们看一下 Car 的单元测试

示例 1.10:类 CarTest 显示验证示例
package org.hibernate.validator.referenceguide.chapter01;

import java.util.Set;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;

import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class CarTest {

    private static Validator validator;

    @BeforeClass
    public static void setUpValidator() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void manufacturerIsNull() {
        Car car = new Car( null, "DD-AB-123", 4 );

        Set<ConstraintViolation<Car>> constraintViolations =
                validator.validate( car );

        assertEquals( 1, constraintViolations.size() );
        assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
    }

    @Test
    public void licensePlateTooShort() {
        Car car = new Car( "Morris", "D", 4 );

        Set<ConstraintViolation<Car>> constraintViolations =
                validator.validate( car );

        assertEquals( 1, constraintViolations.size() );
        assertEquals(
                "size must be between 2 and 14",
                constraintViolations.iterator().next().getMessage()
        );
    }

    @Test
    public void seatCountTooLow() {
        Car car = new Car( "Morris", "DD-AB-123", 1 );

        Set<ConstraintViolation<Car>> constraintViolations =
                validator.validate( car );

        assertEquals( 1, constraintViolations.size() );
        assertEquals(
                "must be greater than or equal to 2",
                constraintViolations.iterator().next().getMessage()
        );
    }

    @Test
    public void carIsValid() {
        Car car = new Car( "Morris", "DD-AB-123", 2 );

        Set<ConstraintViolation<Car>> constraintViolations =
                validator.validate( car );

        assertEquals( 0, constraintViolations.size() );
    }
}

setUp() 方法中,从 ValidatorFactory 获取 Validator 对象。Validator 实例是线程安全的,可以多次重复使用。因此,它可以安全地存储在静态字段中,并在测试方法中用于验证不同的 Car 实例。

validate() 方法返回一组 ConstraintViolation 实例,您可以遍历这些实例以查看发生了哪些验证错误。前三个测试方法显示了一些预期的约束违规

  • manufacturer 上的 @NotNull 约束在 manufacturerIsNull() 中被违反

  • licensePlate 上的 @Size 约束在 licensePlateTooShort() 中被违反

  • seatCount 上的 @Min 约束在 seatCountTooLow() 中被违反

如果对象验证成功,validate() 将返回一个空集,如您在 carIsValid() 中看到的那样。

请注意,仅使用 jakarta.validation 包中的类。这些来自 Bean Validation API。没有直接引用 Hibernate Validator 的类,从而导致可移植代码。

1.4. 接下来的步骤

这结束了对 Hibernate Validator 和 Jakarta Bean Validation 世界的 5 分钟之旅。继续探索代码示例或查看 第 14 章,进一步阅读 中引用的其他示例。

要了解有关 Bean 和属性验证的更多信息,请继续阅读 第 2 章,声明和验证 Bean 约束。如果您有兴趣将 Jakarta Bean Validation 用于方法预条件和后条件的验证,请参阅 第 3 章,声明和验证方法约束。如果您的应用程序具有特定的验证要求,请查看 第 6 章,创建自定义约束

2. 声明和验证 Bean 约束

在本章中,您将学习如何声明(参见 第 2.1 节,“声明 Bean 约束”)和验证(参见 第 2.2 节,“验证 Bean 约束”)Bean 约束。第 2.3 节,“内置约束”概述了 Hibernate Validator 附带的所有内置约束。

如果您有兴趣将约束应用于方法参数和返回值,请参阅 第 3 章,声明和验证方法约束

2.1. 声明 Bean 约束

Jakarta Bean Validation 中的约束通过 Java 注解来表达。在本节中,您将学习如何使用这些注解来增强对象模型。有四种类型的 Bean 约束

  • 字段约束

  • 属性约束

  • 容器元素约束

  • 类约束

并非所有约束都可以在所有这些级别上放置。事实上,Jakarta Bean Validation 定义的默认约束中没有一个可以放在类级别。约束注解本身中的 java.lang.annotation.Target 注解决定了可以在哪些元素上放置约束。有关更多信息,请参见 第 6 章,创建自定义约束

2.1.1. 字段级约束

通过注释类的字段可以表达约束。示例 2.1,“字段级约束” 显示了字段级配置示例

示例 2.1:字段级约束
package org.hibernate.validator.referenceguide.chapter02.fieldlevel;

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.1.2. 属性级约束

如果您的模型类符合 JavaBeans 标准,也可以注释 Bean 类的属性而不是其字段。示例 2.2,“属性级约束” 使用与 示例 2.1,“字段级约束” 相同的实体,但是,使用了属性级约束。

示例 2.2:属性级约束
package org.hibernate.validator.referenceguide.chapter02.propertylevel;

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

必须注释属性的 getter 方法,而不是其 setter 方法。这样,没有 setter 方法的只读属性也可以被约束。

使用属性级约束时,使用属性访问策略来访问要验证的值,即验证引擎通过属性访问器方法访问状态。

建议在一个类中坚持使用字段属性注解。不建议注释字段以及伴随的 getter 方法,因为这会导致字段被验证两次。

2.1.3. 容器元素约束

可以直接在参数化类型的类型参数上指定约束:这些约束称为容器元素约束。

这要求在约束定义中通过 @Target 指定 ElementType.TYPE_USE。从 Jakarta Bean Validation 2.0 开始,内置的 Jakarta Bean Validation 以及 Hibernate Validator 特定的约束指定 ElementType.TYPE_USE,并且可以直接在此上下文中使用。

Hibernate Validator 验证以下标准 Java 容器上指定的容器元素约束

  • java.util.Iterable 的实现(例如,ListSet),

  • java.util.Map 的实现,支持键和值,

  • java.util.Optionaljava.util.OptionalIntjava.util.OptionalDoublejava.util.OptionalLong

  • JavaFX 的 javafx.beans.observable.ObservableValue 的各种实现。

它还支持自定义容器类型上的容器元素约束(有关更多信息,请参见 第 7 章,值提取)。

在 6 之前的版本中,支持容器元素约束的子集。在容器级别需要一个 @Valid 注解来启用它们。从 Hibernate Validator 6 开始,这不再是必需的。

我们将在下面给出几个示例,说明对各种 Java 类型进行容器元素约束。

在这些示例中,@ValidPart 是一个允许在 TYPE_USE 上下文中使用的自定义约束。

2.1.3.1. 使用 Iterable

当对 Iterable 类型参数应用约束时,Hibernate Validator 将验证每个元素。示例 2.3,“Set 上的容器元素约束” 显示了带有容器元素约束的 Set 示例。

示例 2.3:Set 上的容器元素约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.set;

public class Car {

    private Set<@ValidPart String> parts = new HashSet<>();

    public void addPart(String part) {
        parts.add( part );
    }

    //...

}
Car car = new Car();
car.addPart( "Wheel" );
car.addPart( null );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation =
        constraintViolations.iterator().next();
assertEquals(
        "'null' is not a valid car part.",
        constraintViolation.getMessage()
);
assertEquals( "parts[].<iterable element>",
        constraintViolation.getPropertyPath().toString() );

请注意,属性路径清楚地表明违规来自可迭代的元素。

2.1.3.2. 使用 List

当对 List 类型参数应用约束时,Hibernate Validator 将验证每个元素。示例 2.4,“List 上的容器元素约束” 显示了带有容器元素约束的 List 示例。

示例 2.4:List 上的容器元素约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.list;

public class Car {

    private List<@ValidPart String> parts = new ArrayList<>();

    public void addPart(String part) {
        parts.add( part );
    }

    //...

}
Car car = new Car();
car.addPart( "Wheel" );
car.addPart( null );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation =
        constraintViolations.iterator().next();
assertEquals(
        "'null' is not a valid car part.",
        constraintViolation.getMessage()
);
assertEquals( "parts[1].<list element>",
        constraintViolation.getPropertyPath().toString() );

在这里,属性路径还包含无效元素的索引。

2.1.3.3. 使用 Map

容器元素约束也验证在映射键和值上。示例 2.5,“映射键和值上的容器元素约束” 显示了带有键约束和值约束的 Map 示例。

示例 2.5:映射键和值上的容器元素约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.map;

public class Car {

    public enum FuelConsumption {
        CITY,
        HIGHWAY
    }

    private Map<@NotNull FuelConsumption, @MaxAllowedFuelConsumption Integer> fuelConsumption = new HashMap<>();

    public void setFuelConsumption(FuelConsumption consumption, int value) {
        fuelConsumption.put( consumption, value );
    }

    //...

}
Car car = new Car();
car.setFuelConsumption( Car.FuelConsumption.HIGHWAY, 20 );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation =
        constraintViolations.iterator().next();
assertEquals(
        "20 is outside the max fuel consumption.",
        constraintViolation.getMessage()
);
assertEquals(
        "fuelConsumption[HIGHWAY].<map value>",
        constraintViolation.getPropertyPath().toString()
);
Car car = new Car();
car.setFuelConsumption( null, 5 );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation =
        constraintViolations.iterator().next();
assertEquals(
        "must not be null",
        constraintViolation.getMessage()
);
assertEquals(
        "fuelConsumption<K>[].<map key>",
        constraintViolation.getPropertyPath().toString()
);

违规的属性路径特别有趣

  • 无效元素的键包含在属性路径中(在第二个示例中,键为 null)。

  • 在第一个示例中,违规涉及 <map value>,在第二个示例中,违规涉及 <map key>

  • 在第二个示例中,您可能已经注意到 <K> 类型参数的存在,稍后会详细说明。

2.1.3.4. 使用 java.util.Optional

当对 Optional 的类型参数应用约束时,Hibernate Validator 将自动解包类型并验证内部值。示例 2.6,“Optional 上的容器元素约束” 显示了带有容器元素约束的 Optional 示例。

示例 2.6:Optional 上的容器元素约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.optional;

public class Car {

    private Optional<@MinTowingCapacity(1000) Integer> towingCapacity = Optional.empty();

    public void setTowingCapacity(Integer alias) {
        towingCapacity = Optional.of( alias );
    }

    //...

}
Car car = new Car();
car.setTowingCapacity( 100 );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation = constraintViolations.iterator().next();
assertEquals(
        "Not enough towing capacity.",
        constraintViolation.getMessage()
);
assertEquals(
        "towingCapacity",
        constraintViolation.getPropertyPath().toString()
);

在这里,属性路径只包含属性的名称,因为我们将 Optional 视为“透明”容器。

2.1.3.5. 使用自定义容器类型

容器元素约束也可以与自定义容器一起使用。

必须为自定义类型注册一个 ValueExtractor,以便检索要验证的值(有关如何实现自己的 ValueExtractor 以及如何注册它的更多信息,请参见 第 7 章,值提取)。

示例 2.7,“自定义容器类型上的容器元素约束” 显示了带有类型参数约束的自定义参数化类型的示例。

示例 2.7:自定义容器类型上的容器元素约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.custom;

public class Car {

    private GearBox<@MinTorque(100) Gear> gearBox;

    public void setGearBox(GearBox<Gear> gearBox) {
        this.gearBox = gearBox;
    }

    //...

}
package org.hibernate.validator.referenceguide.chapter02.containerelement.custom;

public class GearBox<T extends Gear> {

    private final T gear;

    public GearBox(T gear) {
        this.gear = gear;
    }

    public Gear getGear() {
        return this.gear;
    }
}
package org.hibernate.validator.referenceguide.chapter02.containerelement.custom;

public class Gear {
    private final Integer torque;

    public Gear(Integer torque) {
        this.torque = torque;
    }

    public Integer getTorque() {
        return torque;
    }

    public static class AcmeGear extends Gear {
        public AcmeGear() {
            super( 60 );
        }
    }
}
package org.hibernate.validator.referenceguide.chapter02.containerelement.custom;

public class GearBoxValueExtractor implements ValueExtractor<GearBox<@ExtractedValue ?>> {

    @Override
    public void extractValues(GearBox<@ExtractedValue ?> originalValue, ValueExtractor.ValueReceiver receiver) {
        receiver.value( null, originalValue.getGear() );
    }
}
Car car = new Car();
car.setGearBox( new GearBox<>( new Gear.AcmeGear() ) );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation =
        constraintViolations.iterator().next();
assertEquals(
        "Gear is not providing enough torque.",
        constraintViolation.getMessage()
);
assertEquals(
        "gearBox",
        constraintViolation.getPropertyPath().toString()
);
2.1.3.6. 嵌套容器元素

约束也支持嵌套容器元素。

在验证如 示例 2.8,"嵌套容器元素的约束" 中所示的 Car 对象时,PartManufacturer 上的 @NotNull 约束都将被强制执行。

示例 2.8:嵌套容器元素的约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.nested;

public class Car {

    private Map<@NotNull Part, List<@NotNull Manufacturer>> partManufacturers =
            new HashMap<>();

    //...
}

2.1.4. 类级约束

最后但并非最不重要的是,约束也可以放在类级别。在这种情况下,不是单个属性是验证的主题,而是整个对象。如果验证依赖于对象多个属性之间的相关性,则类级约束很有用。

示例 2.9,"类级约束" 中的 Car 类具有两个属性 seatCountpassengers,应该确保乘客列表的条目数不超过可用座位数。为此,@ValidPassengerCount 约束在类级别添加。该约束的验证器可以访问完整的 Car 对象,从而允许比较座位数和乘客数。

参考 第 6.2 章,"类级约束" 以详细了解如何实现此自定义约束。

示例 2.9:类级约束
package org.hibernate.validator.referenceguide.chapter02.classlevel;

@ValidPassengerCount
public class Car {

    private int seatCount;

    private List<Person> passengers;

    //...
}

2.1.5. 约束继承

当一个类实现一个接口或扩展另一个类时,在超类型上声明的所有约束注解都以与在类本身指定的约束相同的方式应用。为了更清楚地说明,让我们看一下以下示例

示例 2.10:约束继承
package org.hibernate.validator.referenceguide.chapter02.inheritance;

public class Car {

    private String manufacturer;

    @NotNull
    public String getManufacturer() {
        return manufacturer;
    }

    //...
}
package org.hibernate.validator.referenceguide.chapter02.inheritance;

public class RentalCar extends Car {

    private String rentalStation;

    @NotNull
    public String getRentalStation() {
        return rentalStation;
    }

    //...
}

这里,类 RentalCarCar 的子类,并添加了属性 rentalStation。如果验证 RentalCar 的实例,不仅会评估 rentalStation 上的 @NotNull 约束,还会评估父类中的 manufacturer 上的约束。

如果 Car 不是超类而是 RentalCar 实现的接口,情况也会一样。

如果方法被覆盖,约束注解将被聚合。因此,如果 RentalCar 覆盖了 Car 中的 getManufacturer() 方法,则在覆盖方法上注解的任何约束都将与超类中的 @NotNull 约束一起评估。

2.1.6. 对象图

Jakarta Bean Validation API 不仅允许验证单个类实例,还可以验证完整对象图(级联验证)。为此,只需使用 @Valid 注解表示对另一个对象的引用的字段或属性,如 示例 2.11,"级联验证" 中所示。

示例 2.11:级联验证
package org.hibernate.validator.referenceguide.chapter02.objectgraph;

public class Car {

    @NotNull
    @Valid
    private Person driver;

    //...
}
package org.hibernate.validator.referenceguide.chapter02.objectgraph;

public class Person {

    @NotNull
    private String name;

    //...
}

如果验证 Car 的实例,引用的 Person 对象也将被验证,因为 driver 字段用 @Valid 注解。因此,如果引用的 Person 实例的 name 字段为 null,则 Car 的验证将失败。

对象图的验证是递归的,也就是说,如果标记为级联验证的引用指向本身具有用 @Valid 注解的属性的对象,那么验证引擎也会跟踪这些引用。验证引擎将确保在级联验证期间不会出现无限循环,例如,如果两个对象相互引用。

请注意,null 值在级联验证期间会被忽略。

作为约束,对象图验证也适用于容器元素。这意味着容器的任何类型参数都可以用 @Valid 注解,这将导致在验证父对象时验证每个包含的元素。

嵌套容器元素也支持级联验证。

示例 2.12:容器的级联验证
package org.hibernate.validator.referenceguide.chapter02.objectgraph.containerelement;

public class Car {

    private List<@NotNull @Valid Person> passengers = new ArrayList<Person>();

    private Map<@Valid Part, List<@Valid Manufacturer>> partManufacturers = new HashMap<>();

    //...
}
package org.hibernate.validator.referenceguide.chapter02.objectgraph.containerelement;

public class Part {

    @NotNull
    private String name;

    //...
}
package org.hibernate.validator.referenceguide.chapter02.objectgraph.containerelement;

public class Manufacturer {

    @NotNull
    private String name;

    //...
}

在验证 示例 2.12,"容器的级联验证" 中所示的 Car 类的实例时,将创建一个 ConstraintViolation

  • 如果 passengers 列表中包含的任何 Person 对象的 namenull

  • 如果映射键中包含的任何 Part 对象的 namenull

  • 如果映射值中嵌套的列表中包含的任何 Manufacturer 对象的 namenull

在 6 之前的版本中,Hibernate Validator 支持对容器元素子集进行级联验证,并且是在容器级别实现的(例如,您将使用 @Valid private List<Person> 来为 Person 启用级联验证)。

这仍然支持,但不推荐。请改用容器元素级别的 @Valid 注解,因为它更具表达力。

2.2. 验证 Bean 约束

Validator 接口是 Jakarta Bean Validation 中最重要的对象。下一节将介绍如何获取 Validator 实例。之后,您将了解如何使用 Validator 接口的不同方法。

2.2.1. 获取 Validator 实例

验证实体实例的第一步是获取 Validator 实例。获取该实例的路径要通过 Validation 类和 ValidatorFactory。最简单的方法是使用静态方法 Validation#buildDefaultValidatorFactory()

示例 2.13:Validation#buildDefaultValidatorFactory()
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

这将在默认配置中引导验证器。参考 第 9 章,"引导" 以详细了解不同的引导方法以及如何获取特定配置的 Validator 实例。

2.2.2. 验证器方法

Validator 接口包含三种方法,可用于验证整个实体或仅验证实体的单个属性。

所有三种方法都返回 Set<ConstraintViolation>。如果验证成功,则该集合为空。否则,将为每个违反的约束添加 ConstraintViolation 实例。

所有验证方法都有一个可变参数参数,可用于指定在执行验证时应考虑哪些验证组。如果未指定该参数,则使用默认验证组(jakarta.validation.groups.Default)。验证组的主题将在 第 5 章,"对约束进行分组" 中详细讨论。

2.2.2.1. Validator#validate()

使用 validate() 方法来执行对给定 Bean 的所有约束的验证。 示例 2.14,"使用 Validator#validate()" 显示了对来自 示例 2.2,"属性级约束"Car 类的实例的验证,该实例未能满足 manufacturer 属性上的 @NotNull 约束。因此,验证调用返回一个 ConstraintViolation 对象。

示例 2.14:使用 Validator#validate()
Car car = new Car( null, true );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
2.2.2.2. Validator#validateProperty()

借助 validateProperty(),您可以验证给定对象的单个命名属性。属性名称是 JavaBeans 属性名称。

示例 2.15:使用 Validator#validateProperty()
Car car = new Car( null, true );

Set<ConstraintViolation<Car>> constraintViolations = validator.validateProperty(
        car,
        "manufacturer"
);

assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
2.2.2.3. Validator#validateValue()

通过使用 validateValue() 方法,您可以检查给定类的单个属性是否可以成功验证,如果该属性具有指定的值

示例 2.16:使用 Validator#validateValue()
Set<ConstraintViolation<Car>> constraintViolations = validator.validateValue(
        Car.class,
        "manufacturer",
        null
);

assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );

@Valid 不被 validateProperty()validateValue() 认可。

Validator#validateProperty() 例如用于将 Jakarta Bean Validation 集成到 JSF 2 中(参见 第 11.2 节,"JSF & Seam"),以在将值传播到模型之前执行对输入到表单中的值的验证。

2.2.3. ConstraintViolation

2.2.3.1. ConstraintViolation 方法

现在是仔细看看 ConstraintViolation 是什么的时候了。使用 ConstraintViolation 的不同方法,可以确定有关验证失败原因的大量有用信息。以下概述了这些方法。列出 "示例" 中的值指的是 示例 2.14,"使用 Validator#validate()"

getMessage()

插入的错误消息

示例

"不能为空"

getMessageTemplate()

未插入的错误消息

示例

"{…​ NotNull.message}"

getRootBean()

正在验证的根 Bean

示例

car

getRootBeanClass()

正在验证的根 Bean 的类

示例

Car.class

getLeafBean()

如果是 Bean 约束,则为应用约束的 Bean 实例;如果是属性约束,则为托管应用约束的属性的 Bean 实例

示例

car

getPropertyPath()

从根 Bean 到验证值的属性路径

示例

包含一个类型为 PROPERTY 且名为 "manufacturer" 的节点

getInvalidValue()

未能通过约束的值

示例

null

getConstraintDescriptor()

报告为失败的约束元数据

示例

@NotNull 的描述符

2.2.3.2. 利用属性路径

要确定触发违规的元素,您需要利用 getPropertyPath() 方法的结果。

返回的 Path 由描述到元素路径的 Node 组成。

有关 Path 的结构和各种 Node 类型的更多信息,请参见 Jakarta Bean Validation 规范的 ConstraintViolation 部分

2.3. 内置约束

Hibernate Validator 包含一组最常用的约束。这些首先是 Jakarta Bean Validation 规范中定义的约束(参见 第 2.3.1 节,"Jakarta Bean Validation 约束")。此外,Hibernate Validator 还提供有用的自定义约束(参见 第 2.3.2 节,"附加约束")。

2.3.1. Jakarta Bean Validation 约束

下面列出了 Jakarta Bean Validation API 中指定的所有约束。所有这些约束都适用于字段/属性级别,Jakarta Bean Validation 规范中没有定义类级约束。如果您使用的是 Hibernate 对象关系映射器,则某些约束在为您的模型创建 DDL 时会被考虑在内(参见 "Hibernate 元数据影响")。

Hibernate Validator 允许对比 Jakarta Bean Validation 规范要求的更多数据类型应用某些约束(例如,@Max 可以应用于字符串)。依赖此功能可能会影响您的应用程序在 Jakarta Bean Validation 提供程序之间的可移植性。

@AssertFalse

检查注释元素是否为 false

支持的数据类型

Boolean, boolean

Hibernate 元数据影响

@AssertTrue

检查注释元素是否为 true

支持的数据类型

Boolean, boolean

Hibernate 元数据影响

@DecimalMax(value=, inclusive=)

inclusive=false 时,检查注释值是否小于指定的最大值。否则,检查该值是否小于或等于指定的最大值。参数值为根据 BigDecimal 字符串表示形式的 max 值的字符串表示形式。

支持的数据类型

BigDecimal, BigInteger, CharSequence, byte, short, int, long 和相应的基本类型的包装器;此外,HV 还支持:Number 的任何子类型和 javax.money.MonetaryAmount(如果 JSR 354 API 和实现位于类路径上)

Hibernate 元数据影响

@DecimalMin(value=, inclusive=)

inclusive=false 时,检查标注的值是否大于指定的最小值。否则检查该值是否大于或等于指定的最小值。参数值是根据 BigDecimal 字符串表示形式的最小值的字符串表示形式。

支持的数据类型

BigDecimalBigIntegerCharSequencebyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

@Digits(integer=, fraction=)

检查标注的值是否是一个最多有 integer 位整数和小数部分 fraction 位小数的数字

支持的数据类型

BigDecimal、BigIntegerCharSequencebyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

定义列精度和小数位数

@Email

检查指定的字符序列是否是一个有效的电子邮件地址。可选参数 regexpflags 允许指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)。

支持的数据类型

CharSequence

Hibernate 元数据影响

@Future

检查标注的日期是否在将来

支持的数据类型

java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate;如果 Joda Time 日期/时间 API 在类路径上,则 HV 还支持:ReadablePartialReadableInstant 的任何实现

Hibernate 元数据影响

@FutureOrPresent

检查标注的日期是否在现在或将来

支持的数据类型

java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate;如果 Joda Time 日期/时间 API 在类路径上,则 HV 还支持:ReadablePartialReadableInstant 的任何实现

Hibernate 元数据影响

@Max(value=)

检查标注的值是否小于或等于指定的最大值

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:CharSequence 的任何子类型(评估字符序列表示的数字值)、Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

在列上添加检查约束

@Min(value=)

检查标注的值是否大于或等于指定的最小值

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:CharSequence 的任何子类型(评估字符序列表示的数字值)、Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

在列上添加检查约束

@NotBlank

检查标注的字符序列不为空,并且修剪后的长度大于 0。与 @NotEmpty 的区别在于,此约束只能应用于字符序列,并且会忽略尾随空格。

支持的数据类型

CharSequence

Hibernate 元数据影响

@NotEmpty

检查标注的元素是否不为空也不为空

支持的数据类型

CharSequenceCollectionMap 和数组

Hibernate 元数据影响

@NotNull

检查标注的值不为 null

支持的数据类型

任何类型

Hibernate 元数据影响

列不可为空

@Negative

检查元素是否严格为负数。零值被视为无效。

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:CharSequence 的任何子类型(评估字符序列表示的数字值)、Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

@NegativeOrZero

检查元素是否为负数或零。

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:CharSequence 的任何子类型(评估字符序列表示的数字值)、Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

@Null

检查标注的值为 null

支持的数据类型

任何类型

Hibernate 元数据影响

@Past

检查标注的日期是否在过去

支持的数据类型

java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate;如果 Joda Time 日期/时间 API 在类路径上,则 HV 还支持:ReadablePartialReadableInstant 的任何实现

Hibernate 元数据影响

@PastOrPresent

检查标注的日期是否在过去或现在

支持的数据类型

java.util.Datejava.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate;如果 Joda Time 日期/时间 API 在类路径上,则 HV 还支持:ReadablePartialReadableInstant 的任何实现

Hibernate 元数据影响

@Pattern(regex=, flags=)

检查标注的字符串是否匹配正则表达式 regex,并考虑给定的标志 match

支持的数据类型

CharSequence

Hibernate 元数据影响

@Positive

检查元素是否严格为正数。零值被视为无效。

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:CharSequence 的任何子类型(评估字符序列表示的数字值)、Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

@PositiveOrZero

检查元素是否为正数或零。

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的相应包装器;此外,HV 还支持:CharSequence 的任何子类型(评估字符序列表示的数字值)、Number 的任何子类型和 javax.money.MonetaryAmount

Hibernate 元数据影响

@Size(min=, max=)

检查标注的元素的大小是否在 minmax(含)之间

支持的数据类型

CharSequenceCollectionMap 和数组

Hibernate 元数据影响

列长度将设置为 max

除了上面列出的参数外,每个约束都具有参数 message、groups 和 payload。这是 Jakarta Bean Validation 规范的要求。

2.3.2. 附加约束

除了 Jakarta Bean Validation API 定义的约束外,Hibernate Validator 还提供了一些有用的自定义约束,如下所示。除了一个例外,这些约束也适用于字段/属性级别,只有 @ScriptAssert 是一个类级约束。

@CreditCardNumber(ignoreNonDigitCharacters=)

检查标注的字符序列是否通过了 Luhn 校验和测试。注意,此验证旨在检查用户错误,而不是信用卡有效性!另请参见 信用卡号码的解剖ignoreNonDigitCharacters 允许忽略非数字字符。默认值为 false

支持的数据类型

CharSequence

Hibernate 元数据影响

@Currency(value=)

检查标注的 javax.money.MonetaryAmount 的货币单位是否属于指定的货币单位。

支持的数据类型

javax.money.MonetaryAmount 的任何子类型(如果 JSR 354 API 和实现位于类路径上)

Hibernate 元数据影响

@DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=)

检查标注的 java.time.Duration 元素是否不超过根据注释参数构造的元素。如果 inclusive 标志设置为 true,则允许相等。

支持的数据类型

java.time.Duration

Hibernate 元数据影响

@DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=)

检查标注的 java.time.Duration 元素是否不小于根据注释参数构造的元素。如果 inclusive 标志设置为 true,则允许相等。

支持的数据类型

java.time.Duration

Hibernate 元数据影响

@EAN

检查标注的字符序列是否是一个有效的 EAN 条形码。type 确定条形码的类型。默认值为 EAN-13。

支持的数据类型

CharSequence

Hibernate 元数据影响

@ISBN

检查标注的字符序列是否是一个有效的 ISBNtype 确定 ISBN 的类型。默认值为 ISBN-13。

支持的数据类型

CharSequence

Hibernate 元数据影响

@Length(min=, max=)

验证标注的字符序列是否在 minmax(含)之间

支持的数据类型

CharSequence

Hibernate 元数据影响

列长度将设置为 max

@CodePointLength(min=, max=, normalizationStrategy=)

验证标注的字符序列的代码点长度是否在 minmax(含)之间。如果设置了 normalizationStrategy,则验证规范化后的值。

支持的数据类型

CharSequence

Hibernate 元数据影响

@LuhnCheck(startIndex= , endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=)

检查标注的字符序列中的数字是否通过了 Luhn 校验和算法(另请参见 Luhn 算法)。startIndexendIndex 允许仅对指定的子字符串运行算法。checkDigitIndex 允许使用字符序列中的任意数字作为校验位。如果未指定,则假设校验位是指定范围的一部分。最后,ignoreNonDigitCharacters 允许忽略非数字字符。

支持的数据类型

CharSequence

Hibernate 元数据影响

@Mod10Check(multiplier=, weight=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=)

检查标注的字符序列中的数字是否通过了通用 mod 10 校验和算法。multiplier 确定奇数的乘数(默认为 3),weight 确定偶数的权重(默认为 1)。startIndexendIndex 允许仅对指定的子字符串运行算法。checkDigitIndex 允许使用字符序列中的任意数字作为校验位。如果未指定,则假设校验位是指定范围的一部分。最后,ignoreNonDigitCharacters 允许忽略非数字字符。

支持的数据类型

CharSequence

Hibernate 元数据影响

@Mod11Check(threshold=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=, treatCheck10As=, treatCheck11As=)

检查标注的字符序列中的数字是否通过了 mod 11 校验和算法。threshold 指定 mod 11 乘数增长的阈值;如果未指定值,则乘数将无限增长。treatCheck10AstreatCheck11As 分别指定当 mod 11 校验和等于 10 或 11 时要使用的校验位。分别默认为 X 和 0。startIndexendIndex checkDigitIndexignoreNonDigitCharacters@Mod10Check 中具有相同的语义。

支持的数据类型

CharSequence

Hibernate 元数据影响

@Normalized(form=)

验证标注的字符序列是否根据给定的 form 规范化。

支持的数据类型

CharSequence

Hibernate 元数据影响

@Range(min=, max=)

检查标注的值是否在(含)指定的最小值和最大值之间

支持的数据类型

BigDecimalBigIntegerCharSequencebyteshortintlong 以及原始类型的相应包装器

Hibernate 元数据影响

@ScriptAssert(lang=, script=, alias=, reportOn=)

检查给定的脚本是否可以成功地针对标注的元素进行评估。为了使用此约束,Java Scripting API 的实现(如 JSR 223(“JavaTM 平台的脚本”)中定义的)必须是类路径的一部分。要评估的表达式可以用任何脚本或表达式语言编写,这些语言可以为其找到与 JSR 223 兼容的引擎在类路径中。即使这是一个类级约束,也可以使用 reportOn 属性在特定属性上报告约束违规,而不是整个对象。

支持的数据类型

任何类型

Hibernate 元数据影响

@UniqueElements

检查标注的集合是否仅包含唯一元素。相等性使用 equals() 方法确定。默认消息不包括重复元素的列表,但可以通过覆盖消息并使用 {duplicates} 消息参数来包括它。重复元素的列表也包含在约束违规的动态有效负载中。

支持的数据类型

Collection

Hibernate 元数据影响

@URL(protocol=, host=, port=, regexp=, flags=)

检查标注的字符序列是否根据 RFC2396 形成一个有效的 URL。如果指定了任何可选参数 protocolhostport,则相应的 URL 片段必须匹配指定的值。可选参数 regexpflags 允许指定 URL 必须匹配的附加正则表达式(包括正则表达式标志)。默认情况下,此约束使用 java.net.URL 构造函数来验证给定的字符串是否表示一个有效的 URL。还提供了一个基于正则表达式的版本 - RegexpURLValidator - 它可以通过 XML(参见 第 8.2 节,“通过 constraint-mappings 映射约束”)或编程 API(参见 第 12.15.2 节,“以编程方式添加约束定义”)进行配置。

支持的数据类型

CharSequence

Hibernate 元数据影响

@UUID(allowEmpty=, allowNil=, version=, variant=, letterCase=)

检查标注的字符序列是否根据 RFC 4122 形成一个有效的通用唯一标识符。null 始终有效。选项 allowEmpty 允许为空的字符序列。allowNil 包括空 UUID(00000000-0000-0000-0000-000000000000)。versionvariant 参数控制允许哪些 UUID 版本和变体。letterCase 确保使用小写或大写,但也可以配置为不区分大小写。

支持的数据类型

CharSequence

Hibernate 元数据影响

2.3.2.1. 国家/地区特定约束

Hibernate Validator 还提供了一些国家/地区特定的约束,例如用于验证社会保险号的约束。

如果你必须实现一个国家/地区特定的约束,请考虑将其贡献给 Hibernate Validator!

@CNPJ

检查标注的字符序列是否表示巴西公司纳税人登记号(Cadastro de Pessoa Jurídica)

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

巴西

@CPF

检查注释的字符序列是否代表巴西个人纳税人登记号(Cadastro de Pessoa Física)

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

巴西

@TituloEleitoral

检查注释的字符序列是否代表巴西选民证号码(Título Eleitoral

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

巴西

@NIP

检查注释的字符序列是否代表波兰增值税识别号(NIP

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

波兰

@PESEL

检查注释的字符序列是否代表波兰国民身份证号码(PESEL

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

波兰

@REGON

检查注释的字符序列是否代表波兰纳税人识别号(REGON)。可应用于 9 位和 14 位版本的 REGON

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

波兰

@INN

检查注释的字符序列是否代表俄罗斯纳税人识别号(INN)。可应用于个人和法人版本的 INN

支持的数据类型

CharSequence

Hibernate 元数据影响

Country

俄罗斯

在某些情况下,Jakarta Bean Validation 约束或 Hibernate Validator 提供的自定义约束都无法满足您的要求。在这种情况下,您可以轻松编写自己的约束。您可以在第 6 章,创建自定义约束中找到更多信息。

3. 声明和验证方法约束

从 Bean Validation 1.1 开始,约束不仅可以应用于 JavaBeans 及其属性,还可以应用于任何 Java 类型的构造方法和方法的参数和返回值。这样,Jakarta Bean Validation 约束可以用来指定

  • 调用者在调用方法或构造方法之前必须满足的先决条件(通过将约束应用于可执行文件的参数)

  • 方法或构造方法调用返回后保证给调用者的后置条件(通过将约束应用于可执行文件的返回值)

出于本参考指南的目的,术语方法约束指的是方法和构造方法约束,除非另有说明。在某些情况下,术语可执行文件用于指代方法和构造方法。

这种方法比传统的方法检查参数和返回值的正确性具有几个优点

  • 不需要手动执行检查(例如,通过抛出IllegalArgumentException或类似异常),从而减少了编写和维护代码量

  • 不需要在可执行文件的文档中再次表达可执行文件的先决条件和后置条件,因为约束注释将自动包含在生成的 JavaDoc 中。这避免了冗余并减少了实现与文档之间不一致的可能性

为了使注释在注释元素的 JavaDoc 中显示,注释类型本身必须用元注释 @Documented 进行注释。这是所有内置约束的情况,对于任何自定义约束来说也是一个最佳实践。

在本节的剩余部分,您将了解如何声明参数和返回值约束,以及如何使用ExecutableValidator API 验证它们。

3.1. 声明方法约束

3.1.1. 参数约束

您可以通过将约束注释添加到方法或构造方法的参数来指定方法或构造方法的先决条件,如示例 3.1,“声明方法和构造方法参数约束”中所示。

示例 3.1:声明方法和构造方法参数约束
package org.hibernate.validator.referenceguide.chapter03.parameter;

public class RentalStation {

    public RentalStation(@NotNull String name) {
        //...
    }

    public void rentCar(
            @NotNull Customer customer,
            @NotNull @Future Date startDate,
            @Min(1) int durationInDays) {
        //...
    }
}

这里声明了以下先决条件

  • 传递给RentalCar构造方法的name不能为null

  • 在调用rentCar()方法时,给定的customer不能为null,租车的开始日期也不能为null,并且必须在未来,最后租车的持续时间必须至少为一天

请注意,声明方法或构造方法约束本身不会自动在调用可执行文件时导致其验证。相反,必须使用ExecutableValidator API(参见第 3.2 节,“验证方法约束”)来执行验证,这通常使用诸如 AOP、代理对象等方法拦截机制来完成。

约束只能应用于实例方法,即不支持在静态方法上声明约束。根据您用于触发方法验证的拦截机制,可能会有其他限制,例如,关于支持作为拦截目标的方法的可见性。请参阅拦截技术的文档,了解是否存在任何此类限制。

3.1.1.1. 交叉参数约束

有时验证不仅取决于单个参数,还取决于方法或构造方法的多个参数,甚至所有参数。这种要求可以通过交叉参数约束来实现。

交叉参数约束可以被认为是方法验证等效于类级约束。两者都可以用来实现基于多个元素的验证要求。而类级约束应用于 bean 的多个属性,交叉参数约束应用于可执行文件的多个参数。

与单参数约束不同,交叉参数约束是在方法或构造方法上声明的,如示例 3.2,“声明交叉参数约束”中所示。这里,在load()方法上声明的交叉参数约束@LuggageCountMatchesPassengerCount用于确保没有乘客携带超过两件行李。

示例 3.2:声明交叉参数约束
package org.hibernate.validator.referenceguide.chapter03.crossparameter;

public class Car {

    @LuggageCountMatchesPassengerCount(piecesOfLuggagePerPassenger = 2)
    public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
        //...
    }
}

正如您将在下一节中了解的那样,返回值约束也是在方法级别上声明的。为了区分交叉参数约束和返回值约束,约束目标是在ConstraintValidator实现中使用@SupportedValidationTarget注释进行配置。您可以在第 6.3 节,“交叉参数约束”中找到相关细节,该章节展示了如何实现您自己的交叉参数约束。

在某些情况下,约束可以应用于可执行文件的参数(即它是交叉参数约束),也可以应用于返回值。自定义约束就是一个例子,自定义约束允许使用表达式或脚本语言来指定验证规则。

此类约束必须定义一个成员validationAppliesTo(),它可以在声明时用于指定约束目标。如示例 3.3,“指定约束的目标”中所示,您可以通过指定validationAppliesTo = ConstraintTarget.PARAMETERS将约束应用于可执行文件的参数,而ConstraintTarget.RETURN_VALUE用于将约束应用于可执行文件的返回值。

示例 3.3:指定约束的目标
package org.hibernate.validator.referenceguide.chapter03.crossparameter.constrainttarget;

public class Garage {

    @ELAssert(expression = "...", validationAppliesTo = ConstraintTarget.PARAMETERS)
    public Car buildCar(List<Part> parts) {
        //...
        return null;
    }

    @ELAssert(expression = "...", validationAppliesTo = ConstraintTarget.RETURN_VALUE)
    public Car paintCar(int color) {
        //...
        return null;
    }
}

虽然这种约束适用于可执行文件的参数和返回值,但目标通常可以自动推断出来。如果是这种情况,则约束是在以下情况下声明的

  • 带有参数的 void 方法(约束应用于参数)

  • 带有返回值但没有参数的可执行文件(约束应用于返回值)

  • 既不是方法也不是构造方法,而是字段、参数等(约束应用于注释的元素)

在这些情况下,您不需要指定约束目标。如果这样做可以提高源代码的可读性,仍然建议这样做。如果在无法自动确定约束目标的情况下没有指定约束目标,则会引发ConstraintDeclarationException

3.1.2. 返回值约束

方法或构造方法的后置条件是通过将约束注释添加到可执行文件来声明的,如示例 3.4,“声明方法和构造方法返回值约束”中所示。

示例 3.4:声明方法和构造方法返回值约束
package org.hibernate.validator.referenceguide.chapter03.returnvalue;

public class RentalStation {

    @ValidRentalStation
    public RentalStation() {
        //...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getCustomers() {
        //...
        return null;
    }
}

以下约束适用于RentalStation的可执行文件

  • 任何新创建的RentalStation对象都必须满足@ValidRentalStation约束

  • getCustomers()返回的客户列表不能为null,并且必须包含至少一个元素

  • getCustomers()返回的客户列表不能包含null对象

如上例所示,容器元素约束支持在方法返回值上使用。它们也支持在方法参数上使用。

3.1.3. 级联验证

与 JavaBeans 属性的级联验证类似(参见第 2.1.6 节,“对象图”),@Valid注释可以用来标记可执行文件参数和返回值以进行级联验证。在验证用@Valid注释的参数或返回值时,也会验证在参数或返回值对象上声明的约束。

示例 3.5,“标记可执行文件参数和返回值以进行级联验证”中,Garage#checkCar()方法的car参数以及Garage构造方法的返回值被标记为级联验证。

示例 3.5:标记可执行文件参数和返回值以进行级联验证
package org.hibernate.validator.referenceguide.chapter03.cascaded;

public class Garage {

    @NotNull
    private String name;

    @Valid
    public Garage(String name) {
        this.name = name;
    }

    public boolean checkCar(@Valid @NotNull Car car) {
        //...
        return false;
    }
}
package org.hibernate.validator.referenceguide.chapter03.cascaded;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

    public Car(String manufacturer, String licencePlate) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
    }

    //getters and setters ...
}

在验证checkCar()方法的参数时,也会评估传递的Car对象的属性上的约束。类似地,当验证Garage构造方法的返回值时,会检查Garage的 name 字段上的@NotNull约束。

通常,级联验证对可执行文件的工作方式与对 JavaBeans 属性的工作方式完全相同。

特别是,null值在级联验证期间会被忽略(当然,这在构造方法返回值验证期间不可能发生),并且级联验证是递归执行的,即,如果被标记为级联验证的参数或返回值对象本身具有用@Valid标记的属性,则也会验证引用的元素上声明的约束。

与字段和属性一样,级联验证也可以声明在返回值和参数的容器元素(例如,集合、映射或自定义容器的元素)上。

在这种情况下,容器包含的每个元素都会被验证。因此,在验证示例 3.6,“方法参数的容器元素被标记为级联验证”checkCars()方法的参数时,传递的列表中的每个元素实例都会被验证,如果包含的任何Car实例无效,则会创建一个ConstraintViolation

示例 3.6:方法参数的容器元素被标记为级联验证
package org.hibernate.validator.referenceguide.chapter03.cascaded.containerelement;

public class Garage {

    public boolean checkCars(@NotNull List<@Valid Car> cars) {
        //...
        return false;
    }
}

3.1.4. 继承层次结构中的方法约束

在继承层次结构中声明方法约束时,务必注意以下规则

  • 调用者对方法必须满足的先决条件不能在子类型中被加强

  • 保证给调用者方法的返回值的后置条件不能在子类型中被削弱

这些规则的动机是行为子类型的概念,该概念要求,在任何使用类型T的地方,也可以使用T的子类型S,而不会改变程序的行为。

例如,考虑一个类在具有静态类型T的对象上调用方法。如果该对象的运行时类型为S,并且S施加了额外的先决条件,则客户端类可能无法满足这些先决条件,因为它不知道这些先决条件。行为子类型的规则也称为Liskov 替换原则

Jakarta Bean Validation 规范通过禁止对覆盖或实现超类型(超类或接口)中声明的方法的方法的参数约束来实现第一个规则。示例 3.7,“子类型中的非法方法参数约束” 显示了违反此规则的情况。

示例 3.7:子类型中的非法方法参数约束
package org.hibernate.validator.referenceguide.chapter03.inheritance.parameter;

public interface Vehicle {

    void drive(@Max(75) int speedInMph);
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.parameter;

public class Car implements Vehicle {

    @Override
    public void drive(@Max(55) int speedInMph) {
        //...
    }
}

@MaxCar#drive() 的约束是非法的,因为此方法实现了接口方法 Vehicle#drive()。请注意,如果超类型方法本身没有声明任何参数约束,则也不允许对覆盖方法的参数约束。

此外,如果一个方法覆盖或实现多个并行超类型(例如,两个不相互扩展的接口或一个类和一个该类未实现的接口)中声明的方法,则不能在任何参与类型中为该方法指定参数约束。示例 3.8,“层次结构中并行类型中的非法方法参数约束” 中的类型证明了违反该规则的情况。方法 RacingCar#drive() 覆盖 Vehicle#drive() 以及 Car#drive()。因此,Vehicle#drive() 上的约束是非法的。

示例 3.8:层次结构中并行类型中的非法方法参数约束
package org.hibernate.validator.referenceguide.chapter03.inheritance.parallel;

public interface Vehicle {

    void drive(@Max(75) int speedInMph);
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.parallel;

public interface Car {

    void drive(int speedInMph);
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.parallel;

public class RacingCar implements Car, Vehicle {

    @Override
    public void drive(int speedInMph) {
        //...
    }
}

前面描述的限制仅适用于参数约束。相反,可以在覆盖或实现任何超类型方法的方法中添加返回值约束。

在这种情况下,所有方法的返回值约束都适用于子类型方法,即子类型方法本身声明的约束以及覆盖/实现的超类型方法的任何返回值约束。这是合法的,因为实施额外的返回值约束永远不会代表对调用者保证的方法的后置条件的弱化。

因此,当验证示例 3.9,“超类型和子类型方法的返回值约束” 中显示的方法 Car#getPassengers() 的返回值时,方法本身的 @Size 约束以及实现的接口方法 Vehicle#getPassengers()@NotNull 约束都适用。

示例 3.9:超类型和子类型方法的返回值约束
package org.hibernate.validator.referenceguide.chapter03.inheritance.returnvalue;

public interface Vehicle {

    @NotNull
    List<Person> getPassengers();
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.returnvalue;

public class Car implements Vehicle {

    @Override
    @Size(min = 1)
    public List<Person> getPassengers() {
        //...
        return null;
    }
}

如果验证引擎检测到违反上述任何规则,则将引发 ConstraintDeclarationException

本节中描述的规则仅适用于方法,而不适用于构造函数。根据定义,构造函数永远不会覆盖超类型构造函数。因此,在验证构造函数调用的参数或返回值时,仅适用在构造函数本身声明的约束,而永远不会适用在超类型构造函数声明的任何约束。

通过在创建 Validator 实例之前设置 HibernateValidatorConfiguration 属性的 MethodValidationConfiguration 中包含的配置参数,可以放宽对这些规则的执行。另请参阅 第 12.3 节,“类层次结构中方法验证要求的放宽”

3.2. 验证方法约束

方法约束的验证使用 ExecutableValidator 接口完成。

第 3.2.1 节,“获取 ExecutableValidator 实例” 中,您将了解如何获取 ExecutableValidator 实例,而 第 3.2.2 节,“ExecutableValidator 方法” 显示了如何使用此接口提供的不同方法。

通常,不是直接从应用程序代码中调用 ExecutableValidator 方法,而是通过 AOP、代理对象等方法拦截技术调用它们。这会导致在方法或构造函数调用时自动且透明地验证可执行约束。通常,如果违反了任何约束,集成层将引发 ConstraintViolationException

3.2.1. 获取 ExecutableValidator 实例

您可以通过 Validator#forExecutables() 获取 ExecutableValidator 实例,如 示例 3.10,“获取 ExecutableValidator 实例” 所示。

示例 3.10:获取 ExecutableValidator 实例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();

在示例中,可执行验证器是从默认验证器工厂中检索的,但如果需要,您也可以像在 第 9 章,“引导” 中所述的那样引导一个专门配置的工厂,例如为了使用特定的参数名称提供程序(请参阅 第 9.2.4 节,“ParameterNameProvider)。

3.2.2. ExecutableValidator 方法

ExecutableValidator 接口总共提供四种方法

  • 用于方法验证的 validateParameters()validateReturnValue()

  • 用于构造函数验证的 validateConstructorParameters()validateConstructorReturnValue()

Validator 上的方法一样,所有这些方法都返回一个 Set<ConstraintViolation>,其中包含每个违反约束的 ConstraintViolation 实例,如果验证成功,则为空。此外,所有方法都有一个可变参数组参数,您可以通过该参数传递要考虑用于验证的验证组。

以下各节中的示例基于 示例 3.11,“具有约束方法和构造函数的类 Car 中显示的 Car 类构造函数上的方法。

示例 3.11:具有约束方法和构造函数的类 Car
package org.hibernate.validator.referenceguide.chapter03.validation;

public class Car {

    public Car(@NotNull String manufacturer) {
        //...
    }

    @ValidRacingCar
    public Car(String manufacturer, String team) {
        //...
    }

    public void drive(@Max(75) int speedInMph) {
        //...
    }

    @Size(min = 1)
    public List<Passenger> getPassengers() {
        //...
        return Collections.emptyList();
    }
}
3.2.2.1. ExecutableValidator#validateParameters()

方法 validateParameters() 用于验证方法调用的参数。示例 3.12,“使用 ExecutableValidator#validateParameters() 显示了一个示例。验证导致违反了 drive() 方法参数的 @Max 约束。

示例 3.12:使用 ExecutableValidator#validateParameters()
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 );

请注意,validateParameters() 验证方法的所有参数约束,即对单个参数的约束以及跨参数约束。

3.2.2.2. ExecutableValidator#validateReturnValue()

使用 validateReturnValue() 可以验证方法的返回值。示例 3.13,“使用 ExecutableValidator#validateReturnValue() 中的验证产生了一个约束违反,因为 getPassengers() 方法预计至少返回一个 Passenger 实例。

示例 3.13:使用 ExecutableValidator#validateReturnValue()
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 );
3.2.2.3. ExecutableValidator#validateConstructorParameters()

可以使用 validateConstructorParameters() 验证构造函数调用的参数,如方法 示例 3.14,“使用 ExecutableValidator#validateConstructorParameters() 所示。由于 manufacturer 参数的 @NotNull 约束,验证调用返回一个约束违反。

示例 3.14:使用 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 );
3.2.2.4. ExecutableValidator#validateConstructorReturnValue()

最后,通过使用 validateConstructorReturnValue(),您可以验证构造函数的返回值。在 示例 3.15,“使用 ExecutableValidator#validateConstructorReturnValue() 中,validateConstructorReturnValue() 返回一个约束违反,因为构造函数返回的 Car 实例不满足 @ValidRacingCar 约束(未显示)。

示例 3.15:使用 ExecutableValidator#validateConstructorReturnValue()
//constructor for creating racing cars
Constructor<Car> constructor = Car.class.getConstructor( String.class, String.class );
Car createdObject = new Car( "Morris", null );
Set<ConstraintViolation<Car>> violations = executableValidator.validateConstructorReturnValue(
        constructor,
        createdObject
);

assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
        .next()
        .getConstraintDescriptor()
        .getAnnotation()
        .annotationType();
assertEquals( ValidRacingCar.class, constraintType );

3.2.3. 用于方法验证的 ConstraintViolation 方法

除了在 第 2.2.3 节,“ConstraintViolation 中介绍的方法之外,ConstraintViolation 还提供另外两种专门用于验证可执行参数和返回值的方法。

在方法或构造函数参数验证的情况下,ConstraintViolation#getExecutableParameters() 返回已验证的参数数组,而在返回值验证的情况下,ConstraintViolation#getExecutableReturnValue() 提供对已验证对象的访问。

所有其他 ConstraintViolation 方法通常以与验证 Bean 相同的方式用于方法验证。请参阅 JavaDoc 以了解有关 Bean 和方法验证期间各个方法的行为及其返回值的更多信息。

请注意,getPropertyPath() 在获取有关已验证参数或返回值的详细信息时非常有用,例如用于记录目的。特别是,您可以从路径节点中检索所关注方法的名称和参数类型以及所关注参数的索引。如何在 示例 3.16,“检索方法和参数信息” 中完成此操作。

示例 3.16:检索方法和参数信息
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() );
Iterator<Node> propertyPath = violations.iterator()
        .next()
        .getPropertyPath()
        .iterator();

MethodNode methodNode = propertyPath.next().as( MethodNode.class );
assertEquals( "drive", methodNode.getName() );
assertEquals( Arrays.<Class<?>>asList( int.class ), methodNode.getParameterTypes() );

ParameterNode parameterNode = propertyPath.next().as( ParameterNode.class );
assertEquals( "speedInMph", parameterNode.getName() );
assertEquals( 0, parameterNode.getParameterIndex() );

参数名称是使用当前 ParameterNameProvider 确定的(请参阅 第 9.2.4 节,“ParameterNameProvider)。

3.3. 内置方法约束

除了在 第 2.3 节,“内置约束” 中讨论的内置 Bean 和属性级约束之外,Hibernate Validator 目前提供一个方法级约束 @ParameterScriptAssert。这是一个通用的跨参数约束,它允许使用任何与 JSR 223 兼容的(“JavaTM 平台的脚本”)脚本语言来实现验证例程,前提是该语言的引擎在类路径上可用。

要在表达式中引用可执行文件的参数,请使用从活动参数名称提供程序获取的名称(请参阅 第 9.2.4 节,“ParameterNameProvider)。示例 3.17,“使用 @ParameterScriptAssert 显示了如何使用 @ParameterScriptAssert 来表达 示例 3.2,“声明跨参数约束” 中的 @LuggageCountMatchesPassengerCount 约束的验证逻辑。

示例 3.17:使用 @ParameterScriptAssert
package org.hibernate.validator.referenceguide.chapter03.parameterscriptassert;

public class Car {

    @ParameterScriptAssert(lang = "groovy", script = "luggage.size() <= passengers.size() * 2")
    public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
        //...
    }
}

4. 嵌入约束错误消息

消息嵌入是为违反的 Jakarta Bean Validation 约束创建错误消息的过程。在本章中,您将了解如何定义和解析此类消息,以及如何在默认算法不足以满足您的要求的情况下插入自定义消息嵌入器。

4.1. 默认消息嵌入

约束违反消息从所谓的消息描述符中检索。每个约束都使用 message 属性定义其默认消息描述符。在声明时,可以使用特定值覆盖默认描述符,如 示例 4.1,“使用 message 属性指定消息描述符” 所示。

示例 4.1:使用 message 属性指定消息描述符
package org.hibernate.validator.referenceguide.chapter04;

public class Car {

    @NotNull(message = "The manufacturer name must not be null")
    private String manufacturer;

    //constructor, getters and setters ...
}

如果违反了约束,则验证引擎将使用当前配置的 MessageInterpolator 嵌入其描述符。然后,可以通过调用 ConstraintViolation#getMessage() 从生成的约束违反中检索嵌入的错误消息。

消息描述符可以包含消息参数以及在嵌入期间将解析的消息表达式。消息参数是包含在 {} 中的字符串文字,而消息表达式是包含在 ${} 中的字符串文字。在方法嵌入期间将应用以下算法

  1. 通过将消息参数用作资源捆绑包ValidationMessages的键来解析任何消息参数。如果此捆绑包包含给定消息参数的条目,则该参数将在消息中替换为捆绑包中相应的 value。如果替换后的 value 再次包含消息参数,则此步骤将递归执行。预期资源捆绑包由应用程序开发人员提供,例如,通过将名为ValidationMessages.properties的文件添加到类路径。您还可以通过提供此捆绑包的特定于区域设置的变体来创建本地化的错误消息,例如ValidationMessages_en_US.properties。默认情况下,在捆绑包中查找消息时,将使用 JVM 的默认区域设置 (Locale#getDefault())。

  2. 通过使用它们作为包含 Jakarta Bean Validation 规范附录 B 中定义的内置约束的标准错误消息的资源捆绑包的键来解析任何消息参数。在 Hibernate Validator 的情况下,此捆绑包名为org.hibernate.validator.ValidationMessages。如果此步骤触发替换,则再次执行步骤 1,否则应用步骤 3。

  3. 通过将它们替换为约束注释成员的同名 value 来解析任何消息参数。这允许在错误消息中引用约束的属性 value(例如,Size#min())(例如,"必须至少为 ${min}")。

  4. 通过将它们评估为统一表达式语言的表达式来解析任何消息表达式。有关在错误消息中使用统一 EL 的更多信息,请参见第 4.1.2 节,“使用消息表达式进行插值”

您可以在 Jakarta Bean Validation 规范的6.3.1.1 节中找到插值算法的正式定义。

4.1.1. 特殊字符

由于字符{}$ 在消息描述符中具有特殊含义,因此如果您想按字面意义使用它们,则需要对其进行转义。以下规则适用

  • \{ 被视为字面{

  • \} 被视为字面}

  • \$ 被视为字面$

  • \\ 被视为字面\

4.1.2. 使用消息表达式进行插值

从 Hibernate Validator 5(Bean Validation 1.1)开始,可以在约束违规消息中使用Jakarta 表达式语言。这允许根据条件逻辑定义错误消息,并支持高级格式选项。验证引擎在 EL 上下文中提供以下对象

  • 映射到属性名称的约束的属性 value

  • 当前验证的 value(属性、bean、方法参数等)在名为validatedValue

  • 映射到名为 formatter 的 bean,它公开 var-arg 方法format(String format, Object…​ args),该方法的行为类似于java.util.Formatter.format(String format, Object…​ args)

表达式语言非常灵活,Hibernate Validator 提供了几个功能级别,您可以使用它们通过ExpressionLanguageFeatureLevel枚举启用表达式语言功能

  • NONE:完全禁用表达式语言插值。

  • VARIABLES:允许插值通过addExpressionVariable()、资源捆绑包注入的变量,以及使用formatter对象。

  • BEAN_PROPERTIES:允许VARIABLES允许的所有内容,以及 bean 属性的插值。

  • BEAN_METHODS:还允许执行 bean 方法。可以认为对硬编码的约束消息是安全的,但对自定义违规则不安全,因为需要格外小心。

约束消息的默认功能级别是BEAN_PROPERTIES

您可以在引导ValidatorFactory时定义表达式语言功能级别。

以下部分提供了一些在错误消息中使用 EL 表达式的示例。

4.1.3. 示例

示例 4.2,“指定消息描述符”展示了如何利用不同选项来指定消息描述符。

示例 4.2:指定消息描述符
package org.hibernate.validator.referenceguide.chapter04.complete;

public class Car {

    @NotNull
    private String manufacturer;

    @Size(
            min = 2,
            max = 14,
            message = "The license plate '${validatedValue}' must be between {min} and {max} characters long"
    )
    private String licensePlate;

    @Min(
            value = 2,
            message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
    )
    private int seatCount;

    @DecimalMax(
            value = "350",
            message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher " +
                    "than {value}"
    )
    private double topSpeed;

    @DecimalMax(value = "100000", message = "Price must not be higher than ${value}")
    private BigDecimal price;

    public Car(
            String manufacturer,
            String licensePlate,
            int seatCount,
            double topSpeed,
            BigDecimal price) {
        this.manufacturer = manufacturer;
        this.licensePlate = licensePlate;
        this.seatCount = seatCount;
        this.topSpeed = topSpeed;
        this.price = price;
    }

    //getters and setters ...
}

验证无效的Car 实例会产生约束违规,这些约束违规具有示例 4.3,“预期错误消息”中的断言所示的消息

  • @NotNull 约束在manufacturer 字段上导致错误消息“必须不为 null”,因为这是 Jakarta Bean Validation 规范定义的默认消息,并且消息属性中没有给出特定的描述符

  • @Size 约束在licensePlate 字段上显示消息参数的插值 ({min}{max}),以及如何使用 EL 表达式${validatedValue} 将验证的 value 添加到错误消息中

  • @Min 约束在seatCount 上演示了如何使用带有三元表达式的 EL 表达式来动态选择单数或复数形式,具体取决于约束的属性("必须至少有 1 个座位" 与 "必须至少有 2 个座位")

  • @DecimalMax 约束在topSpeed 上的消息显示了如何使用 formatter 实例格式化验证的 value

  • 最后,@DecimalMax 约束在price 上显示参数插值优先于表达式计算,导致$ 符号显示在最大价格之前

只能使用形式为{attributeName}的消息参数来插值实际的约束属性。当引用验证的 value 或添加到插值上下文的自定义表达式变量时(请参见第 12.13.1 节,“HibernateConstraintValidatorContext),必须使用形式为${attributeName}的 EL 表达式。

示例 4.3:预期错误消息
Car car = new Car( null, "A", 1, 400.123456, BigDecimal.valueOf( 200000 ) );

String message = validator.validateProperty( car, "manufacturer" )
        .iterator()
        .next()
        .getMessage();
assertEquals( "must not be null", message );

message = validator.validateProperty( car, "licensePlate" )
        .iterator()
        .next()
        .getMessage();
assertEquals(
        "The license plate 'A' must be between 2 and 14 characters long",
        message
);

message = validator.validateProperty( car, "seatCount" ).iterator().next().getMessage();
assertEquals( "There must be at least 2 seats", message );

message = validator.validateProperty( car, "topSpeed" ).iterator().next().getMessage();
assertEquals( "The top speed 400.12 is higher than 350", message );

message = validator.validateProperty( car, "price" ).iterator().next().getMessage();
assertEquals( "Price must not be higher than $100000", message );

4.2. 自定义消息插值

如果默认消息插值算法不符合您的要求,您也可以插入自定义的MessageInterpolator实现。

自定义插值器必须实现接口jakarta.validation.MessageInterpolator。请注意,实现必须是线程安全的。建议自定义消息插值器将最终实现委托给默认插值器,可以通过Configuration#getDefaultMessageInterpolator()获取默认插值器。

为了使用自定义消息插值器,必须在 Jakarta Bean Validation XML 描述符META-INF/validation.xml 中对其进行注册(请参见第 8.1 节,“在validation.xml 中配置验证器工厂”),或者在引导ValidatorFactoryValidator时将其传递(请参见第 9.2.1 节,“MessageInterpolator第 9.3 节,“配置验证器”)。

4.2.1. ResourceBundleLocator

在某些情况下,您希望使用 Bean Validation 规范定义的消息插值算法,但从除ValidationMessages之外的其他资源捆绑包中检索错误消息。在这种情况下,Hibernate Validator 的ResourceBundleLocator SPI 可以提供帮助。

Hibernate Validator 中的默认消息插值器ResourceBundleMessageInterpolator将资源捆绑包的检索委托给该 SPI。使用替代捆绑包只需要在引导ValidatorFactory时传递PlatformResourceBundleLocator的实例,其中包含捆绑包名称,如示例 4.4,“使用特定资源捆绑包”所示。

示例 4.4:使用特定资源捆绑包
Validator validator = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
                new ResourceBundleMessageInterpolator(
                        new PlatformResourceBundleLocator( "MyMessages" )
                )
        )
        .buildValidatorFactory()
        .getValidator();

当然,您也可以实现完全不同的ResourceBundleLocator,例如,它返回由数据库中的记录支持的捆绑包。在这种情况下,您可以通过HibernateValidatorConfiguration#getDefaultResourceBundleLocator()获取默认定位器,例如,您可以将其用作自定义定位器的后备。

除了PlatformResourceBundleLocator之外,Hibernate Validator 还开箱即用地提供了另一个资源捆绑包定位器实现,即AggregateResourceBundleLocator,它允许从多个资源捆绑包中检索错误消息。例如,您可以在多模块应用程序中使用此实现,您希望每个模块都有一个消息捆绑包。示例 4.5,“使用AggregateResourceBundleLocator展示了如何使用AggregateResourceBundleLocator

示例 4.5:使用AggregateResourceBundleLocator
Validator validator = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
                new ResourceBundleMessageInterpolator(
                        new AggregateResourceBundleLocator(
                                Arrays.asList(
                                        "MyMessages",
                                        "MyOtherMessages"
                                )
                        )
                )
        )
        .buildValidatorFactory()
        .getValidator();

请注意,捆绑包的处理顺序与传递给构造函数的顺序相同。这意味着,如果多个捆绑包包含给定消息键的条目,则将从列表中包含该键的第一个捆绑包中获取 value。

5. 分组约束

ValidatorExecutableValidator上讨论的所有验证方法(在前面的章节中)也采用 var-arg 参数groups。到目前为止,我们一直在忽略此参数,但现在是仔细研究它的时机了。

5.1. 请求组

组允许您限制在验证期间应用的约束集。验证组的一个用例是 UI 向导,在每个步骤中,只有指定的约束子集应该得到验证。目标组作为 var-arg 参数传递给相应的验证方法。

让我们看一个示例。示例 5.1,“示例类Person 中的Person 类在name 上有一个@NotNull 约束。由于此注释没有指定组,因此假定默认组jakarta.validation.groups.Default

当请求多个组时,组评估的顺序是不确定的。如果未指定组,则假定默认组jakarta.validation.groups.Default

示例 5.1:示例类Person
package org.hibernate.validator.referenceguide.chapter05;

public class Person {

    @NotNull
    private String name;

    public Person(String name) {
        this.name = name;
    }

    // getters and setters ...
}

示例 5.2,“Driver” 中的Driver 类扩展了Person 类,并添加了属性agehasDrivingLicense。驾驶员必须年满 18 岁 (@Min(18)) 并持有驾驶执照 (@AssertTrue)。这两个属性上定义的约束都属于组DriverChecks,它只是一个简单的标记接口。

使用接口使组的使用类型安全,并允许轻松重构。它还意味着组可以通过类继承来继承。请参见第 5.2 节,“组继承”

示例 5.2:Driver
package org.hibernate.validator.referenceguide.chapter05;

public class Driver extends Person {

    @Min(
            value = 18,
            message = "You have to be 18 to drive a car",
            groups = DriverChecks.class
    )
    public int age;

    @AssertTrue(
            message = "You first have to pass the driving test",
            groups = DriverChecks.class
    )
    public boolean hasDrivingLicense;

    public Driver(String name) {
        super( name );
    }

    public void passedDrivingTest(boolean b) {
        hasDrivingLicense = b;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
package org.hibernate.validator.referenceguide.chapter05;

public interface DriverChecks {
}

最后,Car 类 (示例 5.3,“Car”) 有一些属于默认组的约束,以及@AssertTrueCarChecks 组中的passedVehicleInspection 属性上,该属性指示汽车是否通过了路检。

示例 5.3:Car
package org.hibernate.validator.referenceguide.chapter05;

public class Car {
    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

    @Min(2)
    private int seatCount;

    @AssertTrue(
            message = "The car has to pass the vehicle inspection first",
            groups = CarChecks.class
    )
    private boolean passedVehicleInspection;

    @Valid
    private Driver driver;

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    public boolean isPassedVehicleInspection() {
        return passedVehicleInspection;
    }

    public void setPassedVehicleInspection(boolean passedVehicleInspection) {
        this.passedVehicleInspection = passedVehicleInspection;
    }

    public Driver getDriver() {
        return driver;
    }

    public void setDriver(Driver driver) {
        this.driver = driver;
    }

    // getters and setters ...
}
package org.hibernate.validator.referenceguide.chapter05;

public interface CarChecks {
}

总共在示例中使用了三个不同的组

  • Person.nameCar.manufacturerCar.licensePlateCar.seatCount 上的约束都属于Default

  • Driver.ageDriver.hasDrivingLicense 上的约束属于DriverChecks

  • Car.passedVehicleInspection 上的约束属于CarChecks

示例 5.4,“使用验证组”展示了将不同的组组合传递给Validator#validate() 方法如何导致不同的验证结果。

示例 5.4:使用验证组
// create a car and check that everything is ok with it.
Car car = new Car( "Morris", "DD-AB-123", 2 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );

// but has it passed the vehicle inspection?
constraintViolations = validator.validate( car, CarChecks.class );
assertEquals( 1, constraintViolations.size() );
assertEquals(
        "The car has to pass the vehicle inspection first",
        constraintViolations.iterator().next().getMessage()
);

// let's go to the vehicle inspection
car.setPassedVehicleInspection( true );
assertEquals( 0, validator.validate( car, CarChecks.class ).size() );

// now let's add a driver. He is 18, but has not passed the driving test yet
Driver john = new Driver( "John Doe" );
john.setAge( 18 );
car.setDriver( john );
constraintViolations = validator.validate( car, DriverChecks.class );
assertEquals( 1, constraintViolations.size() );
assertEquals(
        "You first have to pass the driving test",
        constraintViolations.iterator().next().getMessage()
);

// ok, John passes the test
john.passedDrivingTest( true );
assertEquals( 0, validator.validate( car, DriverChecks.class ).size() );

// just checking that everything is in order now
assertEquals(
        0, validator.validate(
        car,
        Default.class,
        CarChecks.class,
        DriverChecks.class
).size()
);

示例 5.4,“使用验证组”中,第一次调用validate()没有使用显式组。即使属性passedVehicleInspection默认情况下为false,也没有验证错误,因为在此属性上定义的约束不属于默认组。

使用CarChecks组进行的下一个验证将失败,直到汽车通过车辆检查。在汽车上添加驾驶员并针对DriverChecks再次进行验证,由于驾驶员尚未通过驾驶测试,因此会导致一个约束违规。只有在将passedDrivingTest设置为true之后,针对DriverChecks的验证才能通过。

最后一次validate()调用最终表明,通过针对所有定义的组进行验证,所有约束都已通过。

5.2. 组继承

示例 5.4,“使用验证组”中,我们需要为每个验证组调用validate(),或者逐个指定所有组。

在某些情况下,您可能希望定义一个包含另一个组的约束组。您可以使用组继承来实现这一点。

示例 5.5,“SuperCar”中,我们定义了一个SuperCar和一个RaceCarChecks组,该组扩展了Default组。SuperCar必须有安全带才能被允许参加比赛。

示例 5.5:SuperCar
package org.hibernate.validator.referenceguide.chapter05.groupinheritance;

public class SuperCar extends Car {

    @AssertTrue(
            message = "Race car must have a safety belt",
            groups = RaceCarChecks.class
    )
    private boolean safetyBelt;

    // getters and setters ...

}
package org.hibernate.validator.referenceguide.chapter05.groupinheritance;

import jakarta.validation.groups.Default;

public interface RaceCarChecks extends Default {
}

在下面的示例中,我们将检查一个只有一座且没有安全带的SuperCar是否是一辆有效的汽车,以及它是否是一辆有效的赛车。

示例 5.6:使用组继承
// create a supercar and check that it's valid as a generic Car
SuperCar superCar = new SuperCar( "Morris", "DD-AB-123", 1  );
assertEquals( "must be greater than or equal to 2", validator.validate( superCar ).iterator().next().getMessage() );

// check that this supercar is valid as generic car and also as race car
Set<ConstraintViolation<SuperCar>> constraintViolations = validator.validate( superCar, RaceCarChecks.class );

assertThat( constraintViolations ).extracting( "message" ).containsOnly(
        "Race car must have a safety belt",
        "must be greater than or equal to 2"
);

在第一次调用validate()时,我们没有指定组。有一个验证错误,因为汽车必须至少有一座。这是来自Default组的约束。

在第二次调用中,我们只指定了RaceCarChecks组。有两个验证错误:一个关于缺少座位来自Default组,另一个关于没有安全带来自RaceCarChecks组。

5.3. 定义组序列

默认情况下,约束以无特定顺序进行评估,而不管它们属于哪个组。但是,在某些情况下,控制约束评估顺序很有用。

例如,在来自示例 5.4,“使用验证组”的示例中,可能要求首先通过所有默认的汽车约束,然后再检查汽车的路况。最后,在开车离开之前,应该检查实际的驾驶员约束。

为了实现这种验证顺序,您只需定义一个接口并用@GroupSequence对其进行注释,定义组必须验证的顺序(请参阅示例 5.7,“定义组序列”)。如果序列中的一个组中至少有一个约束失败,则序列中后续组的任何约束都不会被验证。

示例 5.7:定义组序列
package org.hibernate.validator.referenceguide.chapter05;

import jakarta.validation.GroupSequence;
import jakarta.validation.groups.Default;

@GroupSequence({ Default.class, CarChecks.class, DriverChecks.class })
public interface OrderedChecks {
}

定义序列的组和构成序列的组在直接或间接地,通过级联序列定义或组继承,都不能参与循环依赖。如果评估包含这种循环的组,则会引发GroupDefinitionException

然后,您可以按示例 5.8,“使用组序列”中所示使用新序列。

示例 5.8:使用组序列
Car car = new Car( "Morris", "DD-AB-123", 2 );
car.setPassedVehicleInspection( true );

Driver john = new Driver( "John Doe" );
john.setAge( 18 );
john.passedDrivingTest( true );
car.setDriver( john );

assertEquals( 0, validator.validate( car, OrderedChecks.class ).size() );

5.4. 重新定义默认组序列

5.4.1. @GroupSequence

除了定义组序列之外,@GroupSequence注释还允许为给定类重新定义默认组。为此,只需将@GroupSequence注释添加到类中,并在注释中指定替换此类的Default的组序列。

示例 5.9,“具有重新定义的默认组的RentalCar类”介绍了一个新的RentalCar类,它具有重新定义的默认组。

示例 5.9:具有重新定义的默认组的RentalCar
package org.hibernate.validator.referenceguide.chapter05;

@GroupSequence({ RentalChecks.class, CarChecks.class, RentalCar.class })
public class RentalCar extends Car {
    @AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
    private boolean rented;

    public RentalCar(String manufacturer, String licencePlate, int seatCount) {
        super( manufacturer, licencePlate, seatCount );
    }

    public boolean isRented() {
        return rented;
    }

    public void setRented(boolean rented) {
        this.rented = rented;
    }
}
package org.hibernate.validator.referenceguide.chapter05;

public interface RentalChecks {
}

使用此定义,您可以通过仅请求Default组来评估属于RentalChecksCarChecksRentalCar的约束,如示例 5.10,“验证具有重新定义的默认组的对象”所示。

示例 5.10:验证具有重新定义的默认组的对象
RentalCar rentalCar = new RentalCar( "Morris", "DD-AB-123", 2 );
rentalCar.setPassedVehicleInspection( true );
rentalCar.setRented( true );

Set<ConstraintViolation<RentalCar>> constraintViolations = validator.validate( rentalCar );

assertEquals( 1, constraintViolations.size() );
assertEquals(
        "Wrong message",
        "The car is currently rented out",
        constraintViolations.iterator().next().getMessage()
);

rentalCar.setRented( false );
constraintViolations = validator.validate( rentalCar );

assertEquals( 0, constraintViolations.size() );

由于组和组序列定义中不能有循环依赖,因此不能将Default简单地添加到重新定义类Default的序列中。相反,必须添加类本身。

Default组序列覆盖是在其定义的类中局部生效,不会传播到关联对象。例如,这意味着将DriverChecks添加到RentalCar的默认组序列中不会有任何影响。只有Default组将传播到驾驶员关联。

请注意,您可以通过声明组转换规则来控制传播的组(请参阅第 5.5 节,“组转换”)。

5.4.2. @GroupSequenceProvider

除了通过@GroupSequence静态地重新定义默认组序列之外,Hibernate Validator 还提供了一个 SPI,用于根据对象状态动态地重新定义默认组序列。

为此,您需要实现DefaultGroupSequenceProvider接口,并通过@GroupSequenceProvider注释将此实现注册到目标类。例如,在租车场景中,您可以像示例 5.11,“实现和使用默认组序列提供者”中所示那样动态地添加CarChecks

示例 5.11:实现和使用默认组序列提供者
package org.hibernate.validator.referenceguide.chapter05.groupsequenceprovider;

public class RentalCarGroupSequenceProvider
        implements DefaultGroupSequenceProvider<RentalCar> {

    @Override
    public List<Class<?>> getValidationGroups(RentalCar car) {
        List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>();
        defaultGroupSequence.add( RentalCar.class );

        if ( car != null && !car.isRented() ) {
            defaultGroupSequence.add( CarChecks.class );
        }

        return defaultGroupSequence;
    }
}
package org.hibernate.validator.referenceguide.chapter05.groupsequenceprovider;

@GroupSequenceProvider(RentalCarGroupSequenceProvider.class)
public class RentalCar extends Car {

    @AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
    private boolean rented;

    public RentalCar(String manufacturer, String licencePlate, int seatCount) {
        super( manufacturer, licencePlate, seatCount );
    }

    public boolean isRented() {
        return rented;
    }

    public void setRented(boolean rented) {
        this.rented = rented;
    }
}

5.5. 组转换

如果您想一起验证与汽车相关的检查和驾驶员检查怎么办?当然,您可以将所需的组显式传递给验证调用,但是如果您想让这些验证作为Default组验证的一部分发生怎么办?这里@ConvertGroup发挥了作用,它允许您在级联验证期间使用与最初请求的组不同的组。

让我们看一下示例 5.12,“@ConvertGroup用法”。这里@GroupSequence({ CarChecks.class, Car.class })用于将与汽车相关的约束组合在Default组下(请参阅第 5.4 节,“重新定义默认组序列”)。还有一个@ConvertGroup(from = Default.class, to = DriverChecks.class),它确保在驾驶员关联的级联验证期间,Default组被转换为DriverChecks组。

示例 5.12:@ConvertGroup用法
package org.hibernate.validator.referenceguide.chapter05.groupconversion;

public class Driver {

    @NotNull
    private String name;

    @Min(
            value = 18,
            message = "You have to be 18 to drive a car",
            groups = DriverChecks.class
    )
    public int age;

    @AssertTrue(
            message = "You first have to pass the driving test",
            groups = DriverChecks.class
    )
    public boolean hasDrivingLicense;

    public Driver(String name) {
        this.name = name;
    }

    public void passedDrivingTest(boolean b) {
        hasDrivingLicense = b;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    // getters and setters ...
}
package org.hibernate.validator.referenceguide.chapter05.groupconversion;

@GroupSequence({ CarChecks.class, Car.class })
public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

    @Min(2)
    private int seatCount;

    @AssertTrue(
            message = "The car has to pass the vehicle inspection first",
            groups = CarChecks.class
    )
    private boolean passedVehicleInspection;

    @Valid
    @ConvertGroup(from = Default.class, to = DriverChecks.class)
    private Driver driver;

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    public boolean isPassedVehicleInspection() {
        return passedVehicleInspection;
    }

    public void setPassedVehicleInspection(boolean passedVehicleInspection) {
        this.passedVehicleInspection = passedVehicleInspection;
    }

    public Driver getDriver() {
        return driver;
    }

    public void setDriver(Driver driver) {
        this.driver = driver;
    }

    // getters and setters ...
}

因此,即使hasDrivingLicense上的约束属于DriverChecks组,并且在validate()调用中只请求了Default组,示例 5.13,“@ConvertGroup的测试用例”中的验证也会成功。

示例 5.13:@ConvertGroup的测试用例
// create a car and validate. The Driver is still null and does not get validated
Car car = new Car( "VW", "USD-123", 4 );
car.setPassedVehicleInspection( true );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );

// create a driver who has not passed the driving test
Driver john = new Driver( "John Doe" );
john.setAge( 18 );

// now let's add a driver to the car
car.setDriver( john );
constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals(
        "The driver constraint should also be validated as part of the default group",
        constraintViolations.iterator().next().getMessage(),
        "You first have to pass the driving test"
);

您可以在任何可以使用@Valid的地方定义组转换,即关联以及方法和构造函数参数和返回值。可以使用@ConvertGroup.List指定多个转换。

但是,以下限制适用

  • @ConvertGroup必须仅与@Valid一起使用。如果单独使用,则会抛出ConstraintDeclarationException

  • 在同一元素上使用相同的 from 值,不允许有多个转换规则。在这种情况下,将引发ConstraintDeclarationException

  • from属性不能引用组序列。在这种情况下,将引发ConstraintDeclarationException

规则不会递归执行。将使用第一个匹配的转换规则,并忽略后续规则。例如,如果一组@ConvertGroup声明将组A链接到B,并将B链接到C,则组A将被转换为B,而不是C

6. 创建自定义约束

Jakarta Bean Validation API 定义了一整套标准约束注释,例如@NotNull@Size等。在这些内置约束不足的情况下,您可以轻松地创建定制的约束,以满足您的特定验证需求。

6.1. 创建一个简单的约束

要创建自定义约束,需要执行以下三个步骤

  • 创建约束注释

  • 实现验证器

  • 定义默认错误消息

6.1.1. 约束注释

本节将展示如何编写一个约束注释,该注释可用于确保给定的字符串要么完全是大写,要么完全是小写。稍后,此约束将应用于来自第 1 章,入门Car类的licensePlate字段,以确保该字段始终为大写字符串。

首先需要一种方法来表达两种情况模式。虽然您可以使用String常量,但更好的方法是为此目的使用枚举

示例 6.1:枚举CaseMode来表达大写和小写
package org.hibernate.validator.referenceguide.chapter06;

public enum CaseMode {
    UPPER,
    LOWER;
}

下一步是定义实际的约束注释。如果您以前从未设计过注释,这可能看起来有点吓人,但实际上并不难

示例 6.2:定义@CheckCase约束注释
package org.hibernate.validator.referenceguide.chapter06;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
@Repeatable(List.class)
public @interface CheckCase {

    String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase." +
            "message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    CaseMode value();

    @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CheckCase[] value();
    }
}

使用@interface关键字定义注释类型。注释类型的所有属性都以类似方法的方式声明。Jakarta Bean Validation API 的规范要求任何约束注释定义

  • 一个名为message的属性,它返回在违反约束的情况下创建错误消息的默认键

  • 一个名为groups的属性,它允许指定此约束所属的验证组(请参阅第 5 章,分组约束)。这必须默认为类型为 Class<?>的空数组。

  • 一个名为payload的属性,可由 Jakarta Bean Validation API 的客户端用于将自定义有效负载对象分配给约束。此属性不被 API 本身使用。自定义有效负载的一个示例可能是严重程度的定义

    public class Severity {
        public interface Info extends Payload {
        }
    
        public interface Error extends Payload {
        }
    }
    public class ContactDetails {
        @NotNull(message = "Name is mandatory", payload = Severity.Error.class)
        private String name;
    
        @NotNull(message = "Phone number not specified, but not mandatory",
                payload = Severity.Info.class)
        private String phoneNumber;
    
        // ...
    }

    现在,客户端可以在验证ContactDetails实例后,使用ConstraintViolation.getConstraintDescriptor().getPayload()访问约束的严重程度,并根据严重程度调整其行为。

除了这三个必需的属性之外,还有一个value属性,它允许指定所需的案例模式。value是一个特殊的名称,如果它是唯一指定的属性,则在使用注释时可以省略它,例如在@CheckCase(CaseMode.UPPER)中。

此外,约束注释还用几个元注释进行装饰

  • @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}):定义约束支持的目标元素类型。@CheckCase可以用于字段(元素类型FIELD)、JavaBeans 属性以及方法返回值(METHOD)、方法/构造函数参数(PARAMETER)和参数化类型的类型参数(TYPE_USE)。元素类型ANNOTATION_TYPE允许基于@CheckCase创建复合约束(请参阅第 6.4 节,“约束组合”)。

    在创建类级约束(请参阅第 2.1.4 节,“类级约束”)时,必须使用元素类型TYPE。针对构造函数返回值的约束需要支持元素类型CONSTRUCTOR。跨参数约束(请参阅第 6.3 节,“跨参数约束”),用于一起验证方法或构造函数的所有参数,必须分别支持METHODCONSTRUCTOR

  • @Retention(RUNTIME):指定此类型的注释将在运行时通过反射的方式可用

  • @Constraint(validatedBy = CheckCaseValidator.class): 将注释类型标记为约束注释,并指定用于验证使用 @CheckCase 注释的元素的验证器。如果一个约束可以在多个数据类型上使用,则可以指定多个验证器,每个数据类型一个。

  • @Documented: 表示使用 @CheckCase 将包含在使用它注释的元素的 JavaDoc 中。

  • @Repeatable(List.class): 表示注释可以在同一个地方重复多次,通常具有不同的配置。List 是包含注释类型。

此包含注释类型名为 List,如示例所示。它允许在同一个元素上指定多个 @CheckCase 注释,例如,具有不同的验证组和消息。虽然可以使用另一个名称,但 Jakarta Bean Validation 规范建议使用 List 名称并将注释作为相应约束类型的内部注释。

6.1.2. 约束验证器

定义了注释后,您需要创建一个约束验证器,该验证器能够验证带有 @CheckCase 注释的元素。为此,请实现 Jakarta Bean Validation 接口 ConstraintValidator,如下所示

示例 6.3:实现约束 @CheckCase 的约束验证器
package org.hibernate.validator.referenceguide.chapter06;

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        if ( caseMode == CaseMode.UPPER ) {
            return object.equals( object.toUpperCase() );
        }
        else {
            return object.equals( object.toLowerCase() );
        }
    }
}

ConstraintValidator 接口定义了两个类型参数,在实现中设置。第一个参数指定要验证的注释类型(CheckCase),第二个参数指定验证器可以处理的元素类型(String)。如果一个约束支持多个数据类型,则必须为每个允许的类型实现一个 ConstraintValidator,并在约束注释中注册,如上所示。

验证器的实现很简单。initialize() 方法允许您访问验证约束的属性值,并允许您将它们存储在验证器中的一个字段中,如示例所示。

isValid() 方法包含实际的验证逻辑。对于 @CheckCase,这将检查给定的字符串是否完全是小写或大写,具体取决于在 initialize() 中检索的大小写模式。请注意,Jakarta Bean Validation 规范建议将空值视为有效值。如果 null 不是元素的有效值,则应使用 @NotNull 明确注释它。

6.1.2.1. ConstraintValidatorContext

示例 6.3,“实现约束 @CheckCase 的约束验证器” 依赖于默认错误消息生成,只需从 isValid() 方法返回 truefalse。使用传递的 ConstraintValidatorContext 对象,可以添加额外的错误消息,也可以完全禁用默认错误消息生成,并仅定义自定义错误消息。ConstraintValidatorContext API 被建模为流畅接口,最好通过示例来演示

示例 6.4:使用 ConstraintValidatorContext 来定义自定义错误消息
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorcontext;

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        boolean isValid;
        if ( caseMode == CaseMode.UPPER ) {
            isValid = object.equals( object.toUpperCase() );
        }
        else {
            isValid = object.equals( object.toLowerCase() );
        }

        if ( !isValid ) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate(
                    "{org.hibernate.validator.referenceguide.chapter06." +
                    "constraintvalidatorcontext.CheckCase.message}"
            )
            .addConstraintViolation();
        }

        return isValid;
    }
}

示例 6.4,“使用 ConstraintValidatorContext 来定义自定义错误消息” 展示了如何禁用默认错误消息生成并使用指定的模板添加自定义错误消息。在本例中,使用 ConstraintValidatorContext 会产生与默认错误消息生成相同的错误消息。

通过调用 addConstraintViolation() 来添加每个配置的约束违反。只有在那之后,才会创建新的约束违反。

默认情况下,在 ConstraintValidatorContext 中创建的自定义违反不会启用表达式语言。

但是,对于一些高级需求,可能需要使用表达式语言。

在这种情况下,您需要解开 HibernateConstraintValidatorContext 并显式启用表达式语言。有关更多信息,请参见 第 12.13.1 节,“HibernateConstraintValidatorContext

请参阅 第 6.2.1 节,“自定义属性路径”,了解如何使用 ConstraintValidatorContext API 来控制类级别约束的约束违反的属性路径。

6.1.2.2. HibernateConstraintValidator 扩展

Hibernate Validator 为 ConstraintValidator 合同提供了一个扩展:HibernateConstraintValidator

此扩展的目的是为 initialize() 方法提供更多上下文信息,因为在当前的 ConstraintValidator 合同中,只有注释作为参数传递。

HibernateConstraintValidatorinitialize() 方法接受两个参数

  • 手头约束的 ConstraintDescriptor。您可以使用 ConstraintDescriptor#getAnnotation() 获取注释。

  • HibernateConstraintValidatorInitializationContext 提供了有用的助手和上下文信息,例如时钟提供程序或时间验证容差。

此扩展被标记为孵化状态,因此可能会发生变化。计划将其标准化,并在将来将其包含在 Jakarta Bean Validation 中。

下面的示例展示了如何将您的验证器基于 HibernateConstraintValidator

示例 6.5:使用 HibernateConstraintValidator 合同
package org.hibernate.validator.referenceguide.chapter06;

public class MyFutureValidator implements HibernateConstraintValidator<MyFuture, Instant> {

    private Clock clock;

    private boolean orPresent;

    @Override
    public void initialize(ConstraintDescriptor<MyFuture> constraintDescriptor,
            HibernateConstraintValidatorInitializationContext initializationContext) {
        this.orPresent = constraintDescriptor.getAnnotation().orPresent();
        this.clock = initializationContext.getClockProvider().getClock();
    }

    @Override
    public boolean isValid(Instant instant, ConstraintValidatorContext constraintContext) {
        //...

        return false;
    }
}

您应该只实现一个 initialize() 方法。请注意,在初始化验证器时,这两个方法都会被调用。

6.1.2.3. 将有效载荷传递给约束验证器

有时,您可能希望根据某些外部参数来设置约束验证器的行为。

例如,您的邮政编码验证器可能会根据您的应用程序实例的区域设置而有所不同,如果您每个国家/地区有一个实例。另一个要求可能是对特定环境有不同的行为:预发布环境可能无法访问某些外部生产资源,而这些资源对于验证器的正常运行是必要的。

约束验证器有效载荷的概念是为了满足所有这些用例而引入的。它是从 Validator 实例通过 HibernateConstraintValidatorContext 传递给每个约束验证器的对象。

下面的示例展示了如何在 ValidatorFactory 初始化期间设置约束验证器有效载荷。除非您覆盖此默认值,否则由此 ValidatorFactory 创建的所有 Validator 都将设置此约束验证器有效载荷值。

示例 6.6:在 ValidatorFactory 初始化期间定义约束验证器有效载荷
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .constraintValidatorPayload( "US" )
        .buildValidatorFactory();

Validator validator = validatorFactory.getValidator();

另一种方法是使用上下文为每个 Validator 设置约束验证器有效载荷

示例 6.7:使用 Validator 上下文定义约束验证器有效载荷
HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
        .configure()
        .buildValidatorFactory()
        .unwrap( HibernateValidatorFactory.class );

Validator validator = hibernateValidatorFactory.usingContext()
        .constraintValidatorPayload( "US" )
        .getValidator();

// [...] US specific validation checks

validator = hibernateValidatorFactory.usingContext()
        .constraintValidatorPayload( "FR" )
        .getValidator();

// [...] France specific validation checks

设置完约束验证器有效载荷后,可以在您的约束验证器中使用它,如下面的示例所示

示例 6.8:在约束验证器中使用约束验证器有效载荷
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload;

public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {

    public String countryCode;

    @Override
    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
        if ( object == null ) {
            return true;
        }

        boolean isValid = false;

        String countryCode = constraintContext
                .unwrap( HibernateConstraintValidatorContext.class )
                .getConstraintValidatorPayload( String.class );

        if ( "US".equals( countryCode ) ) {
            // checks specific to the United States
        }
        else if ( "FR".equals( countryCode ) ) {
            // checks specific to France
        }
        else {
            // ...
        }

        return isValid;
    }
}

HibernateConstraintValidatorContext#getConstraintValidatorPayload() 有一个类型参数,仅当有效载荷为给定类型时才返回有效载荷。

重要的是要注意,约束验证器有效载荷不同于可以在引发的约束违反中包含的动态有效载荷。

此约束验证器有效载荷的全部目的是用于设置约束验证器的行为。它不包含在约束违反中,除非特定的 ConstraintValidator 实现通过使用 约束违反动态有效载荷机制 将有效载荷传递给发出的约束违反。

6.1.3. 错误消息

最后一个缺少的构建块是错误消息,如果 @CheckCase 约束被违反,应该使用它。要定义它,请创建一个名为 ValidationMessages.properties 的文件,其中包含以下内容(另请参见 第 4.1 节,“默认消息插值”

示例 6.9:为 CheckCase 约束定义自定义错误消息
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.

如果发生验证错误,验证运行时将使用您为 @CheckCase 注释的消息属性指定的默认值来在此资源包中查找错误消息。

6.1.4. 使用约束

您现在可以在 第 1 章,“入门” 中的 Car 类中使用此约束来指定 licensePlate 字段应只包含大写字符串

示例 6.10:应用 @CheckCase 约束
package org.hibernate.validator.referenceguide.chapter06;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    @CheckCase(CaseMode.UPPER)
    private String licensePlate;

    @Min(2)
    private int seatCount;

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    //getters and setters ...
}

最后,示例 6.11,“使用 @CheckCase 约束验证对象” 演示了如何验证带有无效车牌的 Car 实例会导致 @CheckCase 约束被违反。

示例 6.11:使用 @CheckCase 约束验证对象
//invalid license plate
Car car = new Car( "Morris", "dd-ab-123", 4 );
Set<ConstraintViolation<Car>> constraintViolations =
        validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals(
        "Case mode must be UPPER.",
        constraintViolations.iterator().next().getMessage()
);

//valid license plate
car = new Car( "Morris", "DD-AB-123", 4 );

constraintViolations = validator.validate( car );

assertEquals( 0, constraintViolations.size() );

6.2. 类级别约束

如前所述,约束也可以应用于类级别,以验证整个对象的状态。类级别约束的定义方式与属性约束相同。 示例 6.12,“实现类级别约束” 展示了约束注释和 @ValidPassengerCount 约束的验证器,您已在 示例 2.9,“类级别约束” 中使用过它。

示例 6.12:实现类级别约束
package org.hibernate.validator.referenceguide.chapter06.classlevel;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { ValidPassengerCountValidator.class })
@Documented
public @interface ValidPassengerCount {

    String message() default "{org.hibernate.validator.referenceguide.chapter06.classlevel." +
            "ValidPassengerCount.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}
package org.hibernate.validator.referenceguide.chapter06.classlevel;

public class ValidPassengerCountValidator
        implements ConstraintValidator<ValidPassengerCount, Car> {

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {
    }

    @Override
    public boolean isValid(Car car, ConstraintValidatorContext context) {
        if ( car == null ) {
            return true;
        }

        return car.getPassengers().size() <= car.getSeatCount();
    }
}

如示例所示,您需要在 @Target 注释中使用元素类型 TYPE。这允许将约束置于类型定义上。示例中约束的验证器在 isValid() 方法中接收一个 Car,并且可以访问完整的对象状态以决定给定的实例是否有效。

6.2.1. 自定义属性路径

默认情况下,类级别约束的约束违反在注释类型的级别报告,例如 Car

但在某些情况下,最好让违规的属性路径引用相关的属性之一。例如,您可能希望针对乘客属性而不是 Car bean 报告 @ValidPassengerCount 约束。

示例 6.13,“使用自定义属性路径添加新的 ConstraintViolation 展示了如何使用传递给 isValid() 的约束验证器上下文来构建具有乘客属性的属性节点的自定义约束违反。请注意,您也可以添加多个属性节点,指向验证 bean 的子实体。

示例 6.13:使用自定义属性路径添加新的 ConstraintViolation
package org.hibernate.validator.referenceguide.chapter06.custompath;

public class ValidPassengerCountValidator
        implements ConstraintValidator<ValidPassengerCount, Car> {

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {
    }

    @Override
    public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) {
        if ( car == null ) {
            return true;
        }

        boolean isValid = car.getPassengers().size() <= car.getSeatCount();

        if ( !isValid ) {
            constraintValidatorContext.disableDefaultConstraintViolation();
            constraintValidatorContext
                    .buildConstraintViolationWithTemplate( "{my.custom.template}" )
                    .addPropertyNode( "passengers" ).addConstraintViolation();
        }

        return isValid;
    }
}

6.3. 交叉参数约束

Jakarta Bean Validation 区分两种不同的约束。

通用约束(到目前为止已经讨论过)应用于注释的元素,例如类型、字段、容器元素、方法参数或返回值等。相反,交叉参数约束应用于方法或构造函数的参数数组,并且可以用来表达取决于多个参数值的验证逻辑。

为了定义交叉参数约束,它的验证器类必须使用 @SupportedValidationTarget(ValidationTarget.PARAMETERS) 注释。ConstraintValidator 接口中的类型参数 T 必须解析为 ObjectObject[],以便在 isValid() 方法中接收方法/构造函数参数数组。

以下示例展示了跨参数约束的定义,它可以用于检查方法的两个Date参数是否按正确顺序排列。

示例 6.14:跨参数约束
package org.hibernate.validator.referenceguide.chapter06.crossparameter;

@Constraint(validatedBy = ConsistentDateParametersValidator.class)
@Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

    String message() default "{org.hibernate.validator.referenceguide.chapter04." +
            "crossparameter.ConsistentDateParameters.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

跨参数约束的定义与定义通用约束没有什么不同,即它必须指定成员message()groups()payload(),并使用@Constraint进行注释。此元注释还指定了相应的验证器,如示例 6.15,“通用和跨参数约束”所示。请注意,除了元素类型METHODCONSTRUCTOR外,还指定了ANNOTATION_TYPE作为注释的目标,以便能够基于@ConsistentDateParameters创建组合约束(参见第 6.4 节,“约束组合”)。

跨参数约束直接在方法或构造函数的声明中指定,返回值约束也是如此。为了提高代码可读性,因此建议选择约束名称(如@ConsistentDateParameters),使约束目标清晰明了。

示例 6.15:通用和跨参数约束
package org.hibernate.validator.referenceguide.chapter06.crossparameter;

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParametersValidator implements
        ConstraintValidator<ConsistentDateParameters, Object[]> {

    @Override
    public void initialize(ConsistentDateParameters constraintAnnotation) {
    }

    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        if ( value.length != 2 ) {
            throw new IllegalArgumentException( "Illegal method signature" );
        }

        //leave null-checking to @NotNull on individual parameters
        if ( value[0] == null || value[1] == null ) {
            return true;
        }

        if ( !( value[0] instanceof Date ) || !( value[1] instanceof Date ) ) {
            throw new IllegalArgumentException(
                    "Illegal method signature, expected two " +
                            "parameters of type Date."
            );
        }

        return ( (Date) value[0] ).before( (Date) value[1] );
    }
}

如上所述,通过使用@SupportedValidationTarget注释,必须为跨参数验证器配置验证目标PARAMETERS。由于跨参数约束可以应用于任何方法或构造函数,因此在验证器实现中检查预期的参数数量和类型被认为是一种最佳实践。

与通用约束一样,null参数应被视为有效参数,并且应在各个参数上使用@NotNull以确保参数不为null

与类级约束类似,在验证跨参数约束时,您可以在单个参数上创建自定义约束违规,而不是所有参数上。只需从传递给isValid()ConstraintValidatorContext中获取节点构建器,然后通过调用addParameterNode()添加参数节点。在示例中,您可以使用此方法在已验证方法的结束日期参数上创建约束违规。

在极少数情况下,约束既是通用约束又是跨参数约束。如果约束的验证器类用@SupportedValidationTarget({ValidationTarget.PARAMETERS, ValidationTarget.ANNOTATED_ELEMENT})进行注释,或者如果它具有通用验证器类和跨参数验证器类,就会出现这种情况。

当在具有参数和返回值的方法上声明这种约束时,无法确定预期的约束目标。因此,同时是通用约束和跨参数约束的约束必须定义一个成员validationAppliesTo(),它允许约束用户指定约束的目标,如示例 6.16,“通用和跨参数约束”所示。

示例 6.16:通用和跨参数约束
package org.hibernate.validator.referenceguide.chapter06.crossparameter;

@Constraint(validatedBy = {
        ScriptAssertObjectValidator.class,
        ScriptAssertParametersValidator.class
})
@Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ScriptAssert {

    String message() default "{org.hibernate.validator.referenceguide.chapter04." +
            "crossparameter.ScriptAssert.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    String script();

    ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
}

@ScriptAssert约束有两个验证器(未显示),一个是通用验证器,另一个是跨参数验证器,因此定义了成员validationAppliesTo()。默认值IMPLICIT允许在可能的情况下(例如,如果约束是在字段上声明的,或者是在具有参数但没有返回值的方法上声明的)自动推导出目标。

如果无法隐式确定目标,则必须由用户将其设置为PARAMETERSRETURN_VALUE,如示例 6.17,“为通用和跨参数约束指定目标”所示。

示例 6.17:为通用和跨参数约束指定目标
@ScriptAssert(script = "arg1.size() <= arg0", validationAppliesTo = ConstraintTarget.PARAMETERS)
public Car buildCar(int seatCount, List<Passenger> passengers) {
    //...
    return null;
}

6.4. 约束组合

查看示例 6.10,“应用@CheckCase约束”Car类的licensePlate字段,您可以看到已经存在三个约束注释。在更复杂的场景中,如果在一个元素上应用了更多约束,这可能会变得有点混乱。此外,如果另一个类中存在一个licensePlate字段,您将不得不将所有约束声明复制到另一个类,从而违反 DRY 原则。

您可以通过创建更高层的约束来解决此类问题,这些约束是由多个基本约束组成的。示例 6.18,“创建组合约束@ValidLicensePlate展示了一个组合约束注释,它包含约束@NotNull@Size@CheckCase

示例 6.18:创建组合约束@ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;

@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
@Documented
public @interface ValidLicensePlate {

    String message() default "{org.hibernate.validator.referenceguide.chapter06." +
            "constraintcomposition.ValidLicensePlate.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

要创建组合约束,只需使用其包含的约束注释约束声明。如果组合约束本身需要验证器,则应在@Constraint注释中指定此验证器。对于不需要额外验证器的组合约束(如@ValidLicensePlate),只需将validatedBy()设置为一个空数组。

licensePlate字段上使用新的组合约束与之前版本完全等效,之前版本是在字段本身直接声明了这三个约束。

示例 6.19:应用组合约束ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;

public class Car {

    @ValidLicensePlate
    private String licensePlate;

    //...
}

在验证Car实例时检索到的ConstraintViolation集合将包含@ValidLicensePlate约束的每个违反组合约束的条目。如果您更希望在任何组合约束违反时只有一个ConstraintViolation,则可以使用@ReportAsSingleViolation元约束,如下所示。

示例 6.20:使用 @ReportAsSingleViolation
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition.reportassingle;

//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {

    String message() default "{org.hibernate.validator.referenceguide.chapter06." +
            "constraintcomposition.reportassingle.ValidLicensePlate.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

7. 值提取

值提取是从容器中提取值的過程,以便可以验证这些值。

7.1. 内置值提取器

Hibernate Validator 附带了针对常用 Java 容器类型的内置值提取器,因此,除非您使用的是自己的自定义容器类型(或外部库(如 GuavaMultimap)的容器类型),否则您不必添加自己的值提取器。

所有以下容器类型都存在内置值提取器

  • java.util.Iterable;

  • java.util.List;

  • java.util.Map:对于键和值;

  • java.util.Optionaljava.util.OptionalIntjava.util.OptionalLongjava.util.OptionalDouble

  • JavaFXObservableValue(有关详细信息,请参见第 7.4 节,“JavaFX 值提取器”)。

可以在 Jakarta Bean Validation 规范中找到所有内置值提取器的完整列表,以及有关它们如何工作的详细信息。

7.2. 实现 ValueExtractor

要从自定义容器中提取值,需要实现一个ValueExtractor

实现ValueExtractor还不够,还需要注册它。有关详细信息,请参见第 7.5 节,“注册ValueExtractor

ValueExtractor 是一个非常简单的 API,因为值提取器的唯一目的是将提取的值提供给ValueReceiver

例如,让我们考虑 Guava 的Optional 的情况。它是一个简单的示例,因为我们可以根据java.util.Optional 的值提取器来塑造它的值提取器。

示例 7.1:Guava 的OptionalValueExtractor
package org.hibernate.validator.referenceguide.chapter07.valueextractor;

public class OptionalValueExtractor
        implements ValueExtractor<Optional<@ExtractedValue ?>> {

    @Override
    public void extractValues(Optional<?> originalValue, ValueReceiver receiver) {
        receiver.value( null, originalValue.orNull() );
    }
}

需要进行一些解释

  • @ExtractedValue 注释标记了正在考虑的类型参数:它将用于解析已验证值的类型;

  • 我们使用接收器的value() 方法,因为Optional 是一个纯包装器类型;

  • 我们不想向约束违规的属性路径添加节点,因为我们希望将违规报告为直接在属性上,因此我们向value() 传递一个null 节点名称。

一个更有趣的例子是 Guava 的Multimap 的情况:我们希望能够验证此容器类型的键和值。

首先让我们考虑值的提取。需要一个提取值的提取器。

示例 7.2:Multimap 值的ValueExtractor
package org.hibernate.validator.referenceguide.chapter07.valueextractor;

public class MultimapValueValueExtractor
        implements ValueExtractor<Multimap<?, @ExtractedValue ?>> {

    @Override
    public void extractValues(Multimap<?, ?> originalValue, ValueReceiver receiver) {
        for ( Entry<?, ?> entry : originalValue.entries() ) {
            receiver.keyedValue( "<multimap value>", entry.getKey(), entry.getValue() );
        }
    }
}

它允许验证Multimap 值的约束。

示例 7.3:Multimap 值上的约束
private Multimap<String, @NotBlank String> map1;

需要另一个值提取器才能对Multimap 的键添加约束。

示例 7.4:Multimap 键的ValueExtractor
package org.hibernate.validator.referenceguide.chapter07.valueextractor;

public class MultimapKeyValueExtractor
        implements ValueExtractor<Multimap<@ExtractedValue ?, ?>> {

    @Override
    public void extractValues(Multimap<?, ?> originalValue, ValueReceiver receiver) {
        for ( Object key : originalValue.keySet() ) {
            receiver.keyedValue( "<multimap key>", key, key );
        }
    }
}

注册这两个值提取器后,您可以在Multimap 的键和值上声明约束。

示例 7.5:Multimap 键和值上的约束
private Multimap<@NotBlank String, @NotBlank String> map2;

乍一看,这两个值提取器之间的区别可能有点微妙,所以让我们来解释一下。

  • @ExtractedValue 注释标记了目标类型参数(在本例中为KV)。

  • 我们使用不同的节点名称(<multimap key> vs. <multimap value>)。

  • 在一种情况下,我们将值传递给接收器(keyedValue() 调用的第三个参数),在另一种情况下,我们将键传递给接收器。

根据您的容器类型,您应该选择最适合的ValueReceiver 方法

value()

对于简单的包装容器 - 它用于Optionals

iterableValue()

对于可迭代容器 - 它用于Sets

indexedValue()

对于包含索引值的容器 - 它用于Lists

keyedValue()

对于包含键值对的容器 - 它用于Maps。它用于键和值。在键的情况下,键也作为已验证的值传递。

对于所有这些方法,您都需要传递一个节点名称:它是添加到约束违规属性路径中的节点的名称。如前所述,如果节点名称为null,则不会将节点添加到属性路径:这对于类似于Optional 的纯包装器类型很有用。

使用的方法的选择很重要,因为它向约束违规的属性路径添加了上下文信息,例如已验证值的索引或键。

7.3. 非泛型容器

您可能已经注意到,到目前为止,我们只为泛型容器实现了值提取器。

Hibernate Validator 也支持对非泛型容器进行值提取。

让我们以java.util.OptionalInt 为例,它将原始int 包装到类似Optional 的容器中。

OptionalInt 的值提取器进行的第一次尝试将如下所示

示例 7.6:OptionalIntValueExtractor
package org.hibernate.validator.referenceguide.chapter07.nongeneric;

public class OptionalIntValueExtractor
        implements ValueExtractor<@ExtractedValue(type = Integer.class) OptionalInt> {

    @Override
    public void extractValues(OptionalInt originalValue, ValueReceiver receiver) {
        receiver.value( null, originalValue.isPresent() ? originalValue.getAsInt() : null );
    }
}

对于非泛型容器,很明显缺少了一些东西:我们没有类型参数。它有两个后果

  • 我们无法使用类型参数确定已验证值的类型;

  • 我们无法在类型参数上添加约束(例如Container<@NotNull String>)。

首先,我们需要一种方法来告诉 Hibernate Validator 从OptionalInt 中提取的值的类型为Integer。如上例所示,@ExtractedValue 注释的type 属性允许将此信息提供给验证引擎。

然后您必须告诉验证引擎您要添加到OptionalInt 属性的Min 约束与包装的值相关,而不是与包装器相关。

Jakarta Bean Validation 为这种情况提供了Unwrapping.Unwrap 负载

示例 7.7:使用Unwrapping.Unwrap 负载
@Min(value = 5, payload = Unwrapping.Unwrap.class)
private OptionalInt optionalInt1;

如果我们退一步思考,大多数(如果不是全部)我们想要添加到OptionalInt属性的约束都将应用于包装的值,因此拥有将其设置为默认值的方法将非常有用。

这正是@UnwrapByDefault注解的用途。

示例 7.8:带有@UnwrapByDefault标记的OptionalIntValueExtractor
package org.hibernate.validator.referenceguide.chapter07.nongeneric;

@UnwrapByDefault
public class UnwrapByDefaultOptionalIntValueExtractor
        implements ValueExtractor<@ExtractedValue(type = Integer.class) OptionalInt> {

    @Override
    public void extractValues(OptionalInt originalValue, ValueReceiver receiver) {
        receiver.value( null, originalValue.isPresent() ? originalValue.getAsInt() : null );
    }
}

在为OptionalInt声明此值提取器时,约束注解默认情况下将应用于包装的值。

示例 7.9:借助@UnwrapByDefault实现隐式解包。
@Min(5)
private OptionalInt optionalInt2;

请注意,您仍然可以通过使用Unwrapping.Skip有效负载为包装器本身声明注解。

示例 7.10:使用Unwrapping.Skip避免隐式解包。
@NotNull(payload = Unwrapping.Skip.class)
@Min(5)
private OptionalInt optionalInt3;

OptionalInt@UnwrapByDefault值提取器是内置值提取器的一部分:无需添加一个。

7.4. JavaFX 值提取器

JavaFX 中的 Bean 属性通常不是Stringint等简单数据类型,而是包装在Property类型中,这使得它们可以被观察到,并用于数据绑定等。

因此,需要值提取才能对包装的值应用约束。

JavaFX ObservableValue值提取器用@UnwrapByDefault标记。因此,默认情况下,容器上的约束会针对包装的值。

因此,您可以像下面这样约束StringProperty

示例 7.11:约束StringProperty
@NotBlank
private StringProperty stringProperty;

LongProperty

示例 7.12:约束LongProperty
@Min(5)
private LongProperty longProperty;

可迭代属性类型(即ReadOnlyListPropertyListProperty及其SetMap对应类型)是泛型,因此可以使用容器元素约束。因此,它们具有不使用@UnwrapByDefault标记的特定值提取器。

ReadOnlyListProperty自然会像List一样被约束。

示例 7.13:约束ReadOnlyListProperty
@Size(min = 1)
private ReadOnlyListProperty<@NotBlank String> listProperty;

7.5. 注册ValueExtractor

Hibernate Validator 不会自动检测类路径中的值提取器,因此必须注册它们。

有几种方法可以注册值提取器(按优先级递增顺序)。

由验证引擎本身提供。

请参阅第 7.1 节,“内置值提取器”

通过 Java 服务加载器机制。

必须提供文件META-INF/services/jakarta.validation.valueextraction.ValueExtractor,其中包含一个或多个值提取器实现的完全限定名称,每个名称在单独的行上。

META-INF/validation.xml文件中。

有关如何在 XML 配置中注册值提取器的更多信息,请参阅第 8.1 节,“在validation.xml中配置验证器工厂”

通过调用Configuration#addValueExtractor(ValueExtractor<?>)

有关更多信息,请参阅第 9.2.6 节,“注册ValueExtractor

通过调用ValidatorContext#addValueExtractor(ValueExtractor<?>)

它仅为此Validator实例声明值提取器。

为给定类型和在较高优先级处指定的类型参数提供的值提取器将覆盖为相同类型和在较低优先级处指定的类型参数提供的任何其他提取器。

7.6. 解析算法

在大多数情况下,您不必担心这个问题,但如果您正在覆盖现有的值提取器,则可以在 Jakarta Bean Validation 规范中找到对值提取器解析算法的详细描述。

要记住的一件重要事情是

  • 对于容器元素约束,使用声明的类型来解析值提取器;

  • 对于级联验证,它是运行时类型。

8. 通过 XML 配置

到目前为止,我们一直在使用 Jakarta Bean Validation 的默认配置源,即注解。但是,还有两种 XML 描述符可以通过 XML 进行配置。第一个描述符描述了常规的 Jakarta Bean Validation 行为,并作为META-INF/validation.xml提供。第二个描述符描述了约束声明,并与通过注解进行约束声明的方法非常相似。让我们看一下这两种文档类型。

XSD 文件可在https://jakarta.ee/xml/ns/validation/页面上找到。

8.1. 在validation.xml中配置验证器工厂

启用 Hibernate Validator 的 XML 配置的关键是文件META-INF/validation.xml。如果此文件存在于类路径上,则在创建ValidatorFactory时将应用其配置。图 1,“验证配置模式”显示了validation.xml必须遵循的 XML 模式模型视图。

validation-configuration-2.0.xsd
图 1. 验证配置模式

示例 8.1,“validation.xml显示了validation.xml的几个配置选项。所有设置都是可选的,并且相同的配置选项也可以通过jakarta.validation.Configuration以编程方式获得。实际上,XML 配置将被通过编程 API 明确指定的 value 覆盖。甚至可以通过Configuration#ignoreXmlConfiguration()完全忽略 XML 配置。另请参阅第 9.2 节,“配置ValidatorFactory

示例 8.1:validation.xml
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
            https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">

    <default-provider>com.acme.ValidationProvider</default-provider>

    <message-interpolator>com.acme.MessageInterpolator</message-interpolator>
    <traversable-resolver>com.acme.TraversableResolver</traversable-resolver>
    <constraint-validator-factory>
        com.acme.ConstraintValidatorFactory
    </constraint-validator-factory>
    <parameter-name-provider>com.acme.ParameterNameProvider</parameter-name-provider>
    <clock-provider>com.acme.ClockProvider</clock-provider>

    <value-extractor>com.acme.ContainerValueExtractor</value-extractor>

    <executable-validation enabled="true">
        <default-validated-executable-types>
            <executable-type>CONSTRUCTORS</executable-type>
            <executable-type>NON_GETTER_METHODS</executable-type>
            <executable-type>GETTER_METHODS</executable-type>
        </default-validated-executable-types>
    </executable-validation>

    <constraint-mapping>META-INF/validation/constraints-car.xml</constraint-mapping>

    <property name="hibernate.validator.fail_fast">false</property>
</validation-config>

类路径上只能有一个名为META-INF/validation.xml的文件。如果找到多个文件,则会抛出异常。

节点default-provider允许选择 Jakarta Bean Validation 提供程序。如果类路径上有多个提供程序,这将很有用。message-interpolatortraversable-resolverconstraint-validator-factoryparameter-name-providerclock-provider允许自定义用于jakarta.validation包中定义的MessageInterpolatorTraversableResolverConstraintValidatorFactoryParameterNameProviderClockProvider接口的实现。有关这些接口的更多信息,请参阅第 9.2 节,“配置ValidatorFactory的子部分。

value-extractor允许声明额外的值提取器,以从自定义容器类型中提取值或覆盖内置值提取器。有关如何实现jakarta.validation.valueextraction.ValueExtractor的更多信息,请参阅第 7 章,值提取

executable-validation及其子节点定义了方法验证的默认值。Jakarta Bean Validation 规范将构造函数和非 getter 方法定义为默认值。enabled 属性充当全局开关,用于打开和关闭方法验证(另请参阅第 3 章,声明和验证方法约束)。

通过constraint-mapping元素,您可以列出任意数量的额外 XML 文件,这些文件包含实际的约束配置。映射文件名必须使用它们在类路径上的完全限定名来指定。有关编写映射文件的详细信息,请参阅下一节。

最后但并非最不重要的一点是,您可以通过property节点指定提供程序特定的属性。在示例中,我们使用了 Hibernate Validator 特定的hibernate.validator.fail_fast属性(请参阅第 12.2 节,“快速失败模式”)。

8.2. 通过constraint-mappings映射约束

可以通过遵循图 2,“验证映射模式”中所示模式的文件在 XML 中表达约束。请注意,仅当通过validation.xml中的 constraint-mapping 列出这些映射文件时,才会处理这些映射文件。

validation-mapping-2.0.xsd
图 2. 验证映射模式
示例 8.2:通过 XML 配置的 Bean 约束
<constraint-mappings
        xmlns="https://jakarta.ee/xml/ns/validation/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/mapping
            https://jakarta.ee/xml/ns/validation/validation-mapping-3.0.xsd"
        version="3.0">

    <default-package>org.hibernate.validator.referenceguide.chapter05</default-package>
    <bean class="Car" ignore-annotations="true">
        <field name="manufacturer">
            <constraint annotation="jakarta.validation.constraints.NotNull"/>
        </field>
        <field name="licensePlate">
            <constraint annotation="jakarta.validation.constraints.NotNull"/>
        </field>
        <field name="seatCount">
            <constraint annotation="jakarta.validation.constraints.Min">
                <element name="value">2</element>
            </constraint>
        </field>
        <field name="driver">
            <valid/>
        </field>
        <field name="partManufacturers">
            <container-element-type type-argument-index="0">
                <valid/>
            </container-element-type>
            <container-element-type type-argument-index="1">
                <container-element-type>
                    <valid/>
                    <constraint annotation="jakarta.validation.constraints.NotNull"/>
                </container-element-type>
            </container-element-type>
        </field>
        <getter name="passedVehicleInspection" ignore-annotations="true">
            <constraint annotation="jakarta.validation.constraints.AssertTrue">
                <message>The car has to pass the vehicle inspection first</message>
                <groups>
                    <value>CarChecks</value>
                </groups>
                <element name="max">10</element>
            </constraint>
        </getter>
    </bean>
    <bean class="RentalCar" ignore-annotations="true">
        <class ignore-annotations="true">
            <group-sequence>
                <value>RentalCar</value>
                <value>CarChecks</value>
            </group-sequence>
        </class>
    </bean>
    <constraint-definition annotation="org.mycompany.CheckCase">
        <validated-by include-existing-validators="false">
            <value>org.mycompany.CheckCaseValidator</value>
        </validated-by>
    </constraint-definition>
</constraint-mappings>
示例 8.3:通过 XML 配置的方法约束
<constraint-mappings
        xmlns="https://jakarta.ee/xml/ns/validation/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/mapping
            https://jakarta.ee/xml/ns/validation/validation-mapping-3.0.xsd"
        version="3.0">

    <default-package>org.hibernate.validator.referenceguide.chapter08</default-package>

    <bean class="RentalStation" ignore-annotations="true">
        <constructor>
            <return-value>
                <constraint annotation="ValidRentalStation"/>
            </return-value>
        </constructor>

        <constructor>
            <parameter type="java.lang.String">
                <constraint annotation="jakarta.validation.constraints.NotNull"/>
            </parameter>
        </constructor>

        <method name="getCustomers">
            <return-value>
                <constraint annotation="jakarta.validation.constraints.NotNull"/>
                <constraint annotation="jakarta.validation.constraints.Size">
                    <element name="min">1</element>
                </constraint>
            </return-value>
        </method>

        <method name="rentCar">
            <parameter type="Customer">
                <constraint annotation="jakarta.validation.constraints.NotNull"/>
            </parameter>
            <parameter type="java.util.Date">
                <constraint annotation="jakarta.validation.constraints.NotNull"/>
                <constraint annotation="jakarta.validation.constraints.Future"/>
            </parameter>
            <parameter type="int">
                <constraint annotation="jakarta.validation.constraints.Min">
                    <element name="value">1</element>
                </constraint>
            </parameter>
        </method>

        <method name="addCars">
            <parameter type="java.util.List">
                <container-element-type>
                    <valid/>
                    <constraint annotation="jakarta.validation.constraints.NotNull"/>
                </container-element-type>
            </parameter>
        </method>
    </bean>

    <bean class="Garage" ignore-annotations="true">
        <method name="buildCar">
            <parameter type="java.util.List"/>
            <cross-parameter>
                <constraint annotation="ELAssert">
                    <element name="expression">...</element>
                    <element name="validationAppliesTo">PARAMETERS</element>
                </constraint>
            </cross-parameter>
        </method>
        <method name="paintCar">
            <parameter type="int"/>
            <return-value>
                <constraint annotation="ELAssert">
                    <element name="expression">...</element>
                    <element name="validationAppliesTo">RETURN_VALUE</element>
                </constraint>
            </return-value>
        </method>
    </bean>

</constraint-mappings>

XML 配置与编程 API 非常相似。因此,只需添加一些注释就足够了。default-package用于所有需要类名的字段。如果未指定所指定的类,则将使用配置的默认包。然后,每个映射文件都可以有多个 bean 节点,每个节点都描述了对具有指定类名的实体的约束。

给定类只能在所有配置文件中配置一次。对于给定约束注解的约束定义也是如此。它只能出现在一个映射文件中。如果违反了这些规则,将抛出ValidationException

ignore-annotations设置为true意味着将忽略放置在配置的 bean 上的约束注解。此值的默认值为 true。ignore-annotations也适用于节点classfieldsgetterconstructormethodparametercross-parameterreturn-value。如果没有在这些级别上明确指定,则将应用配置的 bean 值。

节点classfieldgettercontainer-element-typeconstructormethod(及其子节点 parameter)确定将约束放置在哪个级别。valid节点用于启用级联验证,constraint节点用于在相应级别添加约束。每个约束定义都必须通过annotation属性定义类。Jakarta Bean Validation 规范所需的约束属性(messagegroupspayload)具有专用的节点。所有其他约束特定属性都使用element节点进行配置。

container-element-type允许定义级联验证行为以及容器元素的约束。在上面的示例中,您可以看到对Map值中嵌套的List的嵌套容器元素约束的示例。type-argument-index用于精确说明Map的哪个类型参数受配置影响。如果类型只有一个类型参数(例如我们示例中的List),则可以省略它。

class 节点也允许通过 group-sequence 节点重新配置默认分组序列(参见 第 5.4 节,“重新定义默认分组序列”)。示例中未显示的是使用 convert-group 指定分组转换(参见 第 5.5 节,“分组转换”)。此节点在 fieldgettercontainer-element-typeparameterreturn-value 上可用,并指定 fromto 属性来指定组。

最后但并非最不重要的是,可以通过 constraint-definition 节点更改与给定约束关联的 ConstraintValidator 实例列表。注释属性表示要更改的约束注释。validated-by 元素表示与约束关联的 ConstraintValidator 实现的(有序)列表。如果 include-existing-validator 设置为 false,则忽略约束注释上定义的验证器。如果设置为 true,则 XML 中描述的约束验证器列表将连接到注释中指定的验证器列表。

constraint-definition 的一个用例是更改 @URL 的默认约束定义。从历史上看,Hibernate Validator 用于此约束的默认约束验证器使用 java.net.URL 构造函数来验证 URL 是否有效。但是,也存在一个纯基于正则表达式的版本,可以使用 XML 配置。

使用 XML 注册基于正则表达式的 @URL 约束定义
<constraint-definition annotation="org.hibernate.validator.constraints.URL">
  <validated-by include-existing-validators="false">
    <value>org.hibernate.validator.constraintvalidators.RegexpURLValidator</value>
  </validated-by>
</constraint-definition>

9. 启动

第 2.2.1 节,“获取 Validator 实例” 中,您已经看到了创建 Validator 实例的一种方法 - 通过 Validation#buildDefaultValidatorFactory()。在本章中,您将学习如何使用 jakarta.validation.Validation 中的其他方法来启动特定配置的验证器。

9.1. 获取 ValidatorFactoryValidator

您可以通过从 jakarta.validation.Validation 上的某个静态方法检索 ValidatorFactory,然后在工厂实例上调用 getValidator() 来获取 Validator

示例 9.1,“启动默认 ValidatorFactoryValidator 展示了如何从默认验证器工厂获取验证器

示例 9.1:启动默认 ValidatorFactoryValidator
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

生成的 ValidatorFactoryValidator 实例是线程安全的,可以缓存。由于 Hibernate Validator 使用工厂作为约束元数据缓存的上下文,因此建议在应用程序中使用一个工厂实例。

Jakarta Bean Validation 支持在一个应用程序中使用多个提供者(例如 Hibernate Validator)。如果类路径上存在多个提供者,则无法保证在通过 buildDefaultValidatorFactory() 创建工厂时选择哪个提供者。

在这种情况下,您可以通过 Validation#byProvider() 明确指定要使用的提供者,将提供者的 ValidationProvider 类作为参数传递,如 示例 9.2,“使用特定提供者启动 ValidatorFactoryValidator 所示。

示例 9.2:使用特定提供者启动 ValidatorFactoryValidator
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

请注意,configure() 返回的配置对象允许在调用 buildValidatorFactory() 之前专门自定义工厂。本章稍后将讨论可用的选项。

同样,您可以检索用于配置的默认验证器工厂,这在 示例 9.3,“检索用于配置的默认 ValidatorFactory 中有所展示。

示例 9.3:检索用于配置的默认 ValidatorFactory
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

如果 ValidatorFactory 实例不再使用,则应通过调用 ValidatorFactory#close() 来将其处置。这将释放工厂可能分配的任何资源。

9.1.1. ValidationProviderResolver

默认情况下,使用 Java 服务提供者 机制发现可用的 Jakarta Bean Validation 提供者。

为此,每个提供者都包含文件 META-INF/services/jakarta.validation.spi.ValidationProvider,其中包含其 ValidationProvider 实现的完全限定类名。对于 Hibernate Validator,这是 org.hibernate.validator.HibernateValidator

根据您的环境及其类加载规范,通过 Java 的服务加载器机制发现提供者可能无法正常工作。在这种情况下,您可以插入一个自定义 ValidationProviderResolver 实现,该实现执行提供者检索。一个示例是 OSGi,您可以在其中实现一个提供者解析器,该解析器使用 OSGi 服务进行提供者发现。

要使用自定义提供者解析器,请通过 providerResolver() 传递它,如 示例 9.4,“使用自定义 ValidationProviderResolver 所示。

示例 9.4:使用自定义 ValidationProviderResolver
package org.hibernate.validator.referenceguide.chapter09;

public class OsgiServiceDiscoverer implements ValidationProviderResolver {

    @Override
    public List<ValidationProvider<?>> getValidationProviders() {
        //...
        return null;
    }
}
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .providerResolver( new OsgiServiceDiscoverer() )
        .configure()
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

9.2. 配置 ValidatorFactory

默认情况下,从 Validation 检索到的验证器工厂及其创建的任何验证器都根据 XML 描述符 META-INF/validation.xml(参见 第 8 章,通过 XML 配置)进行配置(如果存在)。

如果您想禁用基于 XML 的配置,可以通过调用 Configuration#ignoreXmlConfiguration() 来实现。

可以通过 Configuration#getBootstrapConfiguration() 访问 XML 配置的不同值。例如,这在您想要将 Jakarta Bean Validation 集成到托管环境中并想要创建通过 XML 配置的对象的托管实例时非常有用。

使用流畅的配置 API,您可以在启动工厂时覆盖一个或多个设置。以下各节将展示如何利用不同的选项。请注意,Configuration 类公开了不同扩展点的默认实现,如果您想将这些实现用作自定义实现的委托,这将非常有用。

9.2.1. MessageInterpolator

消息插值器由验证引擎用于从约束消息描述符创建用户可读的错误消息。

如果 第 4 章,插值约束错误消息 中描述的默认消息插值算法不满足您的需求,您可以通过 Configuration#messageInterpolator() 传入 MessageInterpolator 接口的自定义实现,如 示例 9.5,“使用自定义 MessageInterpolator 所示。

示例 9.5:使用自定义 MessageInterpolator
package org.hibernate.validator.referenceguide.chapter09;

public class MyMessageInterpolator implements MessageInterpolator {

    @Override
    public String interpolate(String messageTemplate, Context context) {
        //...
        return null;
    }

    @Override
    public String interpolate(String messageTemplate, Context context, Locale locale) {
        //...
        return null;
    }
}
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator( new MyMessageInterpolator() )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

9.2.2. TraversableResolver

在某些情况下,验证引擎不应访问 bean 属性的状态。最明显的例子是 JPA 实体的延迟加载属性或关联。验证此延迟加载属性或关联将意味着必须访问其状态,从而触发数据库中的加载。

哪些属性可以访问,哪些属性不能访问,这由查询 TraversableResolver 接口来控制。 示例 9.6,“使用自定义 TraversableResolver 展示了如何使用自定义可遍历解析器实现。

示例 9.6:使用自定义 TraversableResolver
package org.hibernate.validator.referenceguide.chapter09;

public class MyTraversableResolver implements TraversableResolver {

    @Override
    public boolean isReachable(
            Object traversableObject,
            Node traversableProperty,
            Class<?> rootBeanType,
            Path pathToTraversableObject,
            ElementType elementType) {
        //...
        return false;
    }

    @Override
    public boolean isCascadable(
            Object traversableObject,
            Node traversableProperty,
            Class<?> rootBeanType,
            Path pathToTraversableObject,
            ElementType elementType) {
        //...
        return false;
    }
}
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .traversableResolver( new MyTraversableResolver() )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

如果没有配置特定的可遍历解析器,则默认行为是将所有属性视为可访问和可级联的。当将 Hibernate Validator 与 JPA 2 提供者(例如 Hibernate ORM)一起使用时,只有那些已经由持久性提供者加载的属性才会被视为可访问的,并且所有属性都会被视为可级联的。

默认情况下,可遍历解析器调用在每次验证调用时都会被缓存。这在 JPA 环境中尤其重要,因为调用 isReachable() 会产生很大的成本。

这种缓存会增加一些开销。如果您的自定义可遍历解析器非常快,您可能最好考虑关闭缓存。

您可以通过 XML 配置禁用缓存

示例 9.7:通过 XML 配置禁用 TraversableResolver 结果缓存
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">
    <default-provider>org.hibernate.validator.HibernateValidator</default-provider>

    <property name="hibernate.validator.enable_traversable_resolver_result_cache">false</property>
</validation-config>

或者通过编程 API 禁用缓存

示例 9.8:通过编程 API 禁用 TraversableResolver 结果缓存
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .traversableResolver( new MyFastTraversableResolver() )
        .enableTraversableResolverResultCache( false )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

9.2.3. ConstraintValidatorFactory

ConstraintValidatorFactory 是用于自定义如何实例化和释放约束验证器的扩展点。

Hibernate Validator 提供的默认 ConstraintValidatorFactory 需要一个公共无参构造函数来实例化 ConstraintValidator 实例(参见 第 6.1.2 节,“约束验证器”)。使用自定义 ConstraintValidatorFactory 例如提供了一种在约束验证器实现中使用依赖项注入的可能性。

要配置自定义约束验证器工厂,请调用 Configuration#constraintValidatorFactory()(参见 示例 9.9,“使用自定义 ConstraintValidatorFactory)。

示例 9.9:使用自定义 ConstraintValidatorFactory
package org.hibernate.validator.referenceguide.chapter09;

public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {

    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        //...
        return null;
    }

    @Override
    public void releaseInstance(ConstraintValidator<?, ?> instance) {
        //...
    }
}
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .constraintValidatorFactory( new MyConstraintValidatorFactory() )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

任何依赖于特定于实现的 ConstraintValidatorFactory 行为(依赖项注入、无无参构造函数等等)的约束实现不被视为可移植的。

ConstraintValidatorFactory 实现不应缓存验证器实例,因为每个实例的状态都可以在 initialize() 方法中更改。

9.2.4. ParameterNameProvider

如果方法或构造函数参数约束被违反,则使用 ParameterNameProvider 接口来检索参数名称,并将其通过约束违反的属性路径提供给用户。

默认实现返回通过 Java 反射 API 获得的参数名称。如果您使用 -parameters 编译器标志编译源代码,则将返回源代码中的实际参数名称。否则将使用 arg0arg1 等等形式的合成名称。

要使用自定义参数名称提供者,请在启动时传递提供者的实例,如 示例 9.10,“使用自定义 ParameterNameProvider 所示,或者在 META-INF/validation.xml 文件中将提供者的完全限定类名作为 <parameter-name-provider> 元素的值指定(参见 第 8.1 节,“在 validation.xml 中配置验证器工厂”)。这在 示例 9.10,“使用自定义 ParameterNameProvider 中有说明。

示例 9.10:使用自定义 ParameterNameProvider
package org.hibernate.validator.referenceguide.chapter09;

public class MyParameterNameProvider implements ParameterNameProvider {

    @Override
    public List<String> getParameterNames(Constructor<?> constructor) {
        //...
        return null;
    }

    @Override
    public List<String> getParameterNames(Method method) {
        //...
        return null;
    }
}
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .parameterNameProvider( new MyParameterNameProvider() )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

Hibernate Validator 带有一个基于 ParaNamer 库的自定义 ParameterNameProvider 实现,该实现提供了多种在运行时获取参数名称的方法。请参阅 第 12.14 节,“基于 Paranamer 的 ParameterNameProvider 了解有关此特定实现的更多信息。

9.2.5. ClockProvider 和时间验证容差

对于时间相关的验证(例如 @Past@Future 约束),定义什么是 now 可能很有用。

当您想要以可靠的方式测试约束时,这一点尤其重要。

参考时间由 ClockProvider 合同定义。ClockProvider 的职责是提供一个 java.time.Clock,该时钟定义了时间相关验证器的 now

示例 9.11:使用自定义 ClockProvider
package org.hibernate.validator.referenceguide.chapter09;

public class FixedClockProvider implements ClockProvider {

    private Clock clock;

    public FixedClockProvider(ZonedDateTime dateTime) {
        clock = Clock.fixed( dateTime.toInstant(), dateTime.getZone() );
    }

    @Override
    public Clock getClock() {
        return clock;
    }

}
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .clockProvider( new FixedClockProvider( ZonedDateTime.of( 2016, 6, 15, 0, 0, 0, 0, ZoneId.of( "Europe/Paris" ) ) ) )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

或者,您可以在通过 META-INF/validation.xml 配置默认验证器工厂时,使用 <clock-provider> 元素指定 ClockProvider 实现的完全限定类名(参见 第 8 章,通过 XML 配置)。

在验证 @Future@Past 约束时,您可能想要获取当前时间。

您可以在验证器中通过调用 ConstraintValidatorContext#getClockProvider() 方法获取 ClockProvider

例如,如果您想用更明确的消息替换 @Future 约束的默认消息,这可能很有用。

在处理分布式架构时,在应用时间约束(如 @Past@Future)时可能需要一些容忍度。

您可以通过如下方式引导您的 ValidatorFactory 来设置时间验证容忍度。

示例 9.12:使用时间验证容忍度
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .temporalValidationTolerance( Duration.ofMillis( 10 ) )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

或者,您可以在 XML 配置中通过在您的 META-INF/validation.xml 中设置 hibernate.validator.temporal_validation_tolerance 属性来定义它。

此属性的值必须为一个 long,定义以毫秒为单位的容忍度。

在实现您自己的时间约束时,您可能需要访问时间验证容忍度。

可以通过调用 HibernateConstraintValidatorInitializationContext#getTemporalValidationTolerance() 方法获取它。

请注意,要获取初始化时的访问权限,您的约束验证器必须实现 HibernateConstraintValidator 契约(参见 第 6.1.2.2 节,“HibernateConstraintValidator 扩展”)。此契约目前标记为孵化状态:它可能在将来发生更改。

9.2.6. 注册 ValueExtractor

第 7 章,值提取 中所述,可以在引导期间注册额外的值提取器(参见 第 7.5 节,“注册 ValueExtractor,了解注册值提取器的其他方法)。

示例 9.13,“注册额外的值提取器” 展示了如何注册我们之前创建的值提取器来提取 Guava 的 Multimap 的键和值。

示例 9.13:注册额外的值提取器
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .addValueExtractor( new MultimapKeyValueExtractor() )
        .addValueExtractor( new MultimapValueValueExtractor() )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

9.2.7. 添加映射流

如前所述,您可以使用基于 XML 的约束映射来配置应用于 Java Bean 的约束。

除了在 META-INF/validation.xml 中指定的映射文件外,您还可以通过 Configuration#addMapping() 添加进一步的映射(参见 示例 9.14,“添加约束映射流”)。请注意,传递的输入流必须符合 第 8.2 节,“通过 constraint-mappings 映射约束” 中介绍的约束映射的 XML 模式。

示例 9.14:添加约束映射流
InputStream constraintMapping1 = null;
InputStream constraintMapping2 = null;
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
        .configure()
        .addMapping( constraintMapping1 )
        .addMapping( constraintMapping2 )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

在创建验证器工厂后,您应该关闭任何传递的输入流。

9.2.8. 提供商特定的设置

通过 Validation#byProvider() 返回的配置对象,可以配置提供商特定的选项。

在 Hibernate Validator 的情况下,这例如允许您启用快速失败模式并传递一个或多个程序化约束映射,如 示例 9.15,“设置 Hibernate Validator 特定的选项” 中所示。

示例 9.15:设置 Hibernate Validator 特定的选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .addMapping( (ConstraintMapping) null )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

或者,可以通过 Configuration#addProperty() 传递提供商特定的选项。Hibernate Validator 也支持通过这种方式启用快速失败模式。

示例 9.16:通过 addProperty() 启用 Hibernate Validator 特定的选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .addProperty( "hibernate.validator.fail_fast", "true" )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

请参阅 第 12.2 节,“快速失败模式”第 12.4 节,“程序化约束定义和声明”,了解有关快速失败模式和约束声明 API 的更多信息。

9.2.9. 配置 ScriptEvaluatorFactory

对于像 @ScriptAssert@ParameterScriptAssert 这样的约束,配置脚本引擎的初始化方式和脚本评估器的构建方式可能很有用。这可以通过设置 ScriptEvaluatorFactory 的自定义实现来完成。

特别是对于模块化环境(例如 OSGi),用户可能会遇到模块化类加载和 JSR 223 的问题。它还允许使用任何自定义脚本引擎,而不一定基于 JSR 223(例如 Spring 表达式语言)。

9.2.9.1. XML 配置

要通过 XML 指定 ScriptEvaluatorFactory,您需要定义 hibernate.validator.script_evaluator_factory 属性。

示例 9.17:通过 XML 定义 ScriptEvaluatorFactory
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
            https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">

    <property name="hibernate.validator.script_evaluator_factory">
        org.hibernate.validator.referenceguide.chapter09.CustomScriptEvaluatorFactory
    </property>

</validation-config>

在这种情况下,指定的 ScriptEvaluatorFactory 必须具有一个无参数构造函数。

9.2.9.2. 程序化配置

要以编程方式配置它,您需要将 ScriptEvaluatorFactory 的实例传递给 ValidatorFactory。这在 ScriptEvaluatorFactory 的配置中提供了更大的灵活性。示例 9.18,“以编程方式定义 ScriptEvaluatorFactory 展示了如何完成此操作。

示例 9.18:以编程方式定义 ScriptEvaluatorFactory
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .scriptEvaluatorFactory( new CustomScriptEvaluatorFactory() )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
9.2.9.3. 自定义 ScriptEvaluatorFactory 实现示例

本节展示了一些可以在模块化环境中使用的自定义 ScriptEvaluatorFactory 实现,以及一个使用 Spring 表达式语言 来编写约束脚本的实现。

模块化环境和 JSR 223 的问题来自类加载。脚本引擎可用的类加载器可能与 Hibernate Validator 的类加载器不同。因此,脚本引擎将无法使用默认策略找到。

为了解决这个问题,可以在下面引入 MultiClassLoaderScriptEvaluatorFactory

/*
 * Hibernate Validator, declare and validate application constraints
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <https://apache.ac.cn/licenses/LICENSE-2.0>.
 */
package org.hibernate.validator.osgi.scripting;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

import org.hibernate.validator.spi.scripting.AbstractCachingScriptEvaluatorFactory;
import org.hibernate.validator.spi.scripting.ScriptEngineScriptEvaluator;
import org.hibernate.validator.spi.scripting.ScriptEvaluationException;
import org.hibernate.validator.spi.scripting.ScriptEvaluator;
import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory;

/**
 * {@link ScriptEvaluatorFactory} that allows you to pass multiple {@link ClassLoader}s that will be used
 * to search for {@link ScriptEngine}s. Useful in environments similar to OSGi, where script engines can be
 * found only in {@link ClassLoader}s different from default one.
 *
 * @author Marko Bekhta
 */
public class MultiClassLoaderScriptEvaluatorFactory extends AbstractCachingScriptEvaluatorFactory {

    private final ClassLoader[] classLoaders;

    public MultiClassLoaderScriptEvaluatorFactory(ClassLoader... classLoaders) {
        if ( classLoaders.length == 0 ) {
            throw new IllegalArgumentException( "No class loaders were passed" );
        }
        this.classLoaders = classLoaders;
    }

    @Override
    protected ScriptEvaluator createNewScriptEvaluator(String languageName) {
        for ( ClassLoader classLoader : classLoaders ) {
            ScriptEngine engine = new ScriptEngineManager( classLoader ).getEngineByName( languageName );
            if ( engine != null ) {
                return new ScriptEngineScriptEvaluator( engine );
            }
        }
        throw new ScriptEvaluationException( "No JSR 223 script engine found for language " + languageName );
    }
}

然后声明为

Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .scriptEvaluatorFactory(
                new MultiClassLoaderScriptEvaluatorFactory( GroovyScriptEngineFactory.class.getClassLoader() )
        )
        .buildValidatorFactory()
        .getValidator();

这样,就可以传递多个 ClassLoader 实例:通常是想要 ScriptEngine 的类加载器。

OSGi 环境的另一种方法是使用下面定义的 OsgiScriptEvaluatorFactory

/*
 * Hibernate Validator, declare and validate application constraints
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <https://apache.ac.cn/licenses/LICENSE-2.0>.
 */
package org.hibernate.validator.osgi.scripting;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import jakarta.validation.ValidationException;

import org.hibernate.validator.spi.scripting.AbstractCachingScriptEvaluatorFactory;
import org.hibernate.validator.spi.scripting.ScriptEngineScriptEvaluator;
import org.hibernate.validator.spi.scripting.ScriptEvaluator;
import org.hibernate.validator.spi.scripting.ScriptEvaluatorFactory;
import org.hibernate.validator.spi.scripting.ScriptEvaluatorNotFoundException;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

/**
 * {@link ScriptEvaluatorFactory} suitable for OSGi environments. It is created
 * based on the {@code BundleContext} which is used to iterate through {@code Bundle}s and find all {@link ScriptEngineFactory}
 * candidates.
 *
 * @author Marko Bekhta
 */
public class OsgiScriptEvaluatorFactory extends AbstractCachingScriptEvaluatorFactory {

    private final List<ScriptEngineManager> scriptEngineManagers;

    public OsgiScriptEvaluatorFactory(BundleContext context) {
        this.scriptEngineManagers = Collections.unmodifiableList( findManagers( context ) );
    }

    @Override
    protected ScriptEvaluator createNewScriptEvaluator(String languageName) throws ScriptEvaluatorNotFoundException {
        return scriptEngineManagers.stream()
                .map( manager -> manager.getEngineByName( languageName ) )
                .filter( Objects::nonNull )
                .map( engine -> new ScriptEngineScriptEvaluator( engine ) )
                .findFirst()
                .orElseThrow( () -> new ValidationException( String.format( "Unable to find script evaluator for '%s'.", languageName ) ) );
    }

    private List<ScriptEngineManager> findManagers(BundleContext context) {
        return findFactoryCandidates( context ).stream()
                .map( className -> {
                    try {
                        return new ScriptEngineManager( Class.forName( className ).getClassLoader() );
                    }
                    catch (ClassNotFoundException e) {
                        throw new ValidationException( "Unable to instantiate '" + className + "' based engine factory manager.", e );
                    }
                } ).collect( Collectors.toList() );
    }

    /**
     * Iterates through all bundles to get the available {@link ScriptEngineFactory} classes
     *
     * @return the names of the available ScriptEngineFactory classes
     *
     * @throws IOException
     */
    private List<String> findFactoryCandidates(BundleContext context) {
        return Arrays.stream( context.getBundles() )
                .filter( Objects::nonNull )
                .filter( bundle -> !"system.bundle".equals( bundle.getSymbolicName() ) )
                .flatMap( this::toStreamOfResourcesURL )
                .filter( Objects::nonNull )
                .flatMap( url -> toListOfFactoryCandidates( url ).stream() )
                .collect( Collectors.toList() );
    }

    private Stream<URL> toStreamOfResourcesURL(Bundle bundle) {
        Enumeration<URL> entries = bundle.findEntries(
                "META-INF/services",
                "javax.script.ScriptEngineFactory",
                false
        );
        return entries != null ? Collections.list( entries ).stream() : Stream.empty();
    }

    private List<String> toListOfFactoryCandidates(URL url) {
        try ( BufferedReader reader = new BufferedReader( new InputStreamReader( url.openStream(), "UTF-8" ) ) ) {
            return reader.lines()
                    .map( String::trim )
                    .filter( line -> !line.isEmpty() )
                    .filter( line -> !line.startsWith( "#" ) )
                    .collect( Collectors.toList() );
        }
        catch (IOException e) {
            throw new ValidationException( "Unable to read the ScriptEngineFactory resource file", e );
        }
    }
}

然后声明为

Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .scriptEvaluatorFactory(
                new OsgiScriptEvaluatorFactory( FrameworkUtil.getBundle( this.getClass() ).getBundleContext() )
        )
        .buildValidatorFactory()
        .getValidator();

它专为 OSGi 环境设计,允许您将 BundleContext 作为参数传递,该参数将用于搜索 ScriptEngineFactory

如前所述,您还可以使用不基于 JSR 223 的脚本引擎。

例如,要使用 Spring 表达式语言,您可以定义一个 SpringELScriptEvaluatorFactory 如下

package org.hibernate.validator.referenceguide.chapter09;

public class SpringELScriptEvaluatorFactory extends AbstractCachingScriptEvaluatorFactory {

    @Override
    public ScriptEvaluator createNewScriptEvaluator(String languageName) {
        if ( !"spring".equalsIgnoreCase( languageName ) ) {
            throw new IllegalStateException( "Only Spring EL is supported" );
        }

        return new SpringELScriptEvaluator();
    }

    private static class SpringELScriptEvaluator implements ScriptEvaluator {

        private final ExpressionParser expressionParser = new SpelExpressionParser();

        @Override
        public Object evaluate(String script, Map<String, Object> bindings) throws ScriptEvaluationException {
            try {
                Expression expression = expressionParser.parseExpression( script );
                EvaluationContext context = new StandardEvaluationContext( bindings.values().iterator().next() );
                for ( Entry<String, Object> binding : bindings.entrySet() ) {
                    context.setVariable( binding.getKey(), binding.getValue() );
                }
                return expression.getValue( context );
            }
            catch (ParseException | EvaluationException e) {
                throw new ScriptEvaluationException( "Unable to evaluate SpEL script", e );
            }
        }
    }
}

此工厂允许在 ScriptAssertParameterScriptAssert 约束中使用 Spring 表达式语言

@ScriptAssert(script = "value > 0", lang = "spring")
public class Foo {

    private final int value;

    private Foo(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

9.2.10. 对正在验证的值进行日志记录

在某些情况下,检查 Hibernate Validator 生成的日志可能很有用。当日志级别设置为 TRACE 时,验证器将生成,除其他外,包含正在评估的约束描述符的日志条目。默认情况下,正在验证的值将不会在这些消息中可见,以防止敏感数据泄露。如果需要,Hibernate Validator 可以配置为也打印这些值。像往常一样,有几种方法可以做到这一点

示例 9.25:程序化配置
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .showValidatedValuesInTraceLogs( true )
        .buildValidatorFactory()
        .getValidator();
示例 9.26:通过属性进行程序化配置
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .addProperty( "hibernate.validator.show_validated_value_in_trace_logs", "true" )
        .buildValidatorFactory()
        .getValidator();
示例 9.27:通过属性进行 XML 配置
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">
    <default-provider>org.hibernate.validator.HibernateValidator</default-provider>

    <property name="hibernate.validator.show_validated_value_in_trace_logs">true</property>
</validation-config>

8.0 之前的 Hibernate Validator 版本,如果日志级别设置为 TRACE,则会记录值和约束描述符。

9.3. 配置验证器

在使用配置的验证器工厂时,有时可能需要对单个 Validator 实例应用不同的配置。示例 9.28,“通过 usingContext() 配置 Validator 实例” 展示了如何通过调用 ValidatorFactory#usingContext() 来实现此操作。

示例 9.28:通过 usingContext() 配置 Validator 实例
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();

Validator validator = validatorFactory.usingContext()
        .messageInterpolator( new MyMessageInterpolator() )
        .traversableResolver( new MyTraversableResolver() )
        .getValidator();

10. 使用约束元数据

Jakarta Bean Validation 规范不仅提供了一个验证引擎,还提供了一个 API,用于以统一的方式检索约束元数据,无论约束是使用注释还是通过 XML 映射声明。阅读本章以了解有关此 API 及其功能的更多信息。您可以在 jakarta.validation.metadata 包中找到所有元数据 API 类型。

本章中介绍的示例基于 示例 10.1,“示例类” 中显示的类和约束声明。

示例 10.1:示例类
package org.hibernate.validator.referenceguide.chapter10;

public class Person {

    public interface Basic {
    }

    @NotNull
    private String name;

    //getters and setters ...
}
package org.hibernate.validator.referenceguide.chapter10;

public interface Vehicle {

    public interface Basic {
    }

    @NotNull(groups = Vehicle.Basic.class)
    String getManufacturer();
}
package org.hibernate.validator.referenceguide.chapter10;

@ValidCar
public class Car implements Vehicle {

    public interface SeverityInfo extends Payload {
    }

    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

    private Person driver;

    private String modelName;

    public Car() {
    }

    public Car(
            @NotNull String manufacturer,
            String licencePlate,
            Person driver,
            String modelName) {

        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.driver = driver;
        this.modelName = modelName;
    }

    public void driveAway(@Max(75) int speed) {
        //...
    }

    @LuggageCountMatchesPassengerCount(
            piecesOfLuggagePerPassenger = 2,
            validationAppliesTo = ConstraintTarget.PARAMETERS,
            payload = SeverityInfo.class,
            message = "There must not be more than {piecesOfLuggagePerPassenger} pieces " +
                    "of luggage per passenger."
    )
    public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
        //...
    }

    @Override
    @Size(min = 3)
    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    @Valid
    @ConvertGroup(from = Default.class, to = Person.Basic.class)
    public Person getDriver() {
        return driver;
    }

    //further getters and setters...
}
package org.hibernate.validator.referenceguide.chapter10;

public class Library {

    @NotNull
    private String name;

    private List<@NotNull @Valid Book> books;

    //getters and setters ...
}
package org.hibernate.validator.referenceguide.chapter10;

public class Book {

    @NotEmpty
    private String title;

    @NotEmpty
    private String author;

    //getters and setters ...
}

10.1. BeanDescriptor

元数据 API 的入口点是 Validator#getConstraintsForClass() 方法,该方法返回 BeanDescriptor 接口的实例。使用此描述符,您可以获取直接在 bean 本身上声明的约束的元数据(类级或属性级),还可以检索表示单个属性、方法和构造函数的元数据描述符。

示例 10.2,“使用 BeanDescriptor 演示了如何为 Car 类检索 BeanDescriptor,以及如何在断言的形式中使用此描述符。

如果请求的类托管的约束声明无效,则会抛出 ValidationException

示例 10.2:使用 BeanDescriptor
BeanDescriptor carDescriptor = validator.getConstraintsForClass( Car.class );

assertTrue( carDescriptor.isBeanConstrained() );

//one class-level constraint
assertEquals( 1, carDescriptor.getConstraintDescriptors().size() );

//manufacturer, licensePlate, driver
assertEquals( 3, carDescriptor.getConstrainedProperties().size() );

//property has constraint
assertNotNull( carDescriptor.getConstraintsForProperty( "licensePlate" ) );

//property is marked with @Valid
assertNotNull( carDescriptor.getConstraintsForProperty( "driver" ) );

//constraints from getter method in interface and implementation class are returned
assertEquals(
        2,
        carDescriptor.getConstraintsForProperty( "manufacturer" )
                .getConstraintDescriptors()
                .size()
);

//property is not constrained
assertNull( carDescriptor.getConstraintsForProperty( "modelName" ) );

//driveAway(int), load(List<Person>, List<PieceOfLuggage>)
assertEquals( 2, carDescriptor.getConstrainedMethods( MethodType.NON_GETTER ).size() );

//driveAway(int), getManufacturer(), getDriver(), load(List<Person>, List<PieceOfLuggage>)
assertEquals(
        4,
        carDescriptor.getConstrainedMethods( MethodType.NON_GETTER, MethodType.GETTER )
                .size()
);

//driveAway(int)
assertNotNull( carDescriptor.getConstraintsForMethod( "driveAway", int.class ) );

//getManufacturer()
assertNotNull( carDescriptor.getConstraintsForMethod( "getManufacturer" ) );

//setManufacturer() is not constrained
assertNull( carDescriptor.getConstraintsForMethod( "setManufacturer", String.class ) );

//Car(String, String, Person, String)
assertEquals( 1, carDescriptor.getConstrainedConstructors().size() );

//Car(String, String, Person, String)
assertNotNull(
        carDescriptor.getConstraintsForConstructor(
                String.class,
                String.class,
                Person.class,
                String.class
        )
);

您可以通过 isBeanConstrained() 确定指定类是否托管任何类级或属性级约束。isBeanConstrained() 不会考虑方法或构造函数约束。

getConstraintDescriptors() 方法对于从 ElementDescriptor 派生的所有描述符来说是通用的(参见 第 10.4 节,“ElementDescriptor),它返回表示直接在给定元素上声明的约束的描述符集。在 BeanDescriptor 的情况下,将返回 bean 的类级约束。有关 ConstraintDescriptor 的更多详细信息,请参见 第 10.7 节,“ConstraintDescriptor

通过 getConstraintsForProperty()getConstraintsForMethod()getConstraintsForConstructor(),您可以获取表示一个给定属性或可执行元素的描述符,该元素由其名称标识,并且在方法和构造函数的情况下,还由参数类型标识。这些方法返回的不同描述符类型将在以下部分中介绍。

请注意,这些方法会根据 第 2.1.5 节,“约束继承” 中介绍的约束继承规则,考虑在超类型上声明的约束。一个例子是 manufacturer 属性的描述符,它提供对 Vehicle#getManufacturer() 和实现方法 Car#getManufacturer() 上定义的所有约束的访问权限。如果指定的元素不存在或没有约束,则返回 null

getConstrainedProperties()getConstrainedMethods()getConstrainedConstructors() 方法分别返回(可能为空的)集合,其中包含所有受约束的属性、方法和构造函数。如果一个元素至少有一个约束或被标记为级联验证,则它被认为是受约束的。当调用 getConstrainedMethods() 时,您可以指定要返回的方法类型(getter、非 getter 或两者)。

10.2. PropertyDescriptor

PropertyDescriptor 接口表示类的某个给定属性。无论约束是在字段上还是在属性 getter 上声明的,只要尊重 JavaBeans 命名约定,它都是透明的。示例 10.3,“使用 PropertyDescriptor 展示了如何使用 PropertyDescriptor 接口。

示例 10.3:使用 PropertyDescriptor
PropertyDescriptor licensePlateDescriptor = carDescriptor.getConstraintsForProperty(
        "licensePlate"
);

//"licensePlate" has two constraints, is not marked with @Valid and defines no group conversions
assertEquals( "licensePlate", licensePlateDescriptor.getPropertyName() );
assertEquals( 2, licensePlateDescriptor.getConstraintDescriptors().size() );
assertTrue( licensePlateDescriptor.hasConstraints() );
assertFalse( licensePlateDescriptor.isCascaded() );
assertTrue( licensePlateDescriptor.getGroupConversions().isEmpty() );

PropertyDescriptor driverDescriptor = carDescriptor.getConstraintsForProperty( "driver" );

//"driver" has no constraints, is marked with @Valid and defines one group conversion
assertEquals( "driver", driverDescriptor.getPropertyName() );
assertTrue( driverDescriptor.getConstraintDescriptors().isEmpty() );
assertFalse( driverDescriptor.hasConstraints() );
assertTrue( driverDescriptor.isCascaded() );
assertEquals( 1, driverDescriptor.getGroupConversions().size() );

使用getConstraintDescriptors(),您可以检索一组ConstraintDescriptors,以提供有关给定属性的各个约束的更多信息。方法isCascaded()如果属性被标记为级联验证(使用@Valid注释或通过 XML),则返回true,否则返回false。任何配置的组转换都将由getGroupConversions()返回。有关GroupConversionDescriptor的更多详细信息,请参见第 10.6 节,“GroupConversionDescriptor

10.3. MethodDescriptorConstructorDescriptor

受约束的方法和构造函数分别由接口MethodDescriptor ConstructorDescriptor表示。 示例 10.4,“使用MethodDescriptorConstructorDescriptor 演示了如何使用这些描述符。

示例 10.4:使用MethodDescriptorConstructorDescriptor
//driveAway(int) has a constrained parameter and an unconstrained return value
MethodDescriptor driveAwayDescriptor = carDescriptor.getConstraintsForMethod(
        "driveAway",
        int.class
);
assertEquals( "driveAway", driveAwayDescriptor.getName() );
assertTrue( driveAwayDescriptor.hasConstrainedParameters() );
assertFalse( driveAwayDescriptor.hasConstrainedReturnValue() );

//always returns an empty set; constraints are retrievable by navigating to
//one of the sub-descriptors, e.g. for the return value
assertTrue( driveAwayDescriptor.getConstraintDescriptors().isEmpty() );

ParameterDescriptor speedDescriptor = driveAwayDescriptor.getParameterDescriptors()
        .get( 0 );

//The "speed" parameter is located at index 0, has one constraint and is not cascaded
//nor does it define group conversions
assertEquals( "speed", speedDescriptor.getName() );
assertEquals( 0, speedDescriptor.getIndex() );
assertEquals( 1, speedDescriptor.getConstraintDescriptors().size() );
assertFalse( speedDescriptor.isCascaded() );
assert speedDescriptor.getGroupConversions().isEmpty();

//getDriver() has no constrained parameters but its return value is marked for cascaded
//validation and declares one group conversion
MethodDescriptor getDriverDescriptor = carDescriptor.getConstraintsForMethod(
        "getDriver"
);
assertFalse( getDriverDescriptor.hasConstrainedParameters() );
assertTrue( getDriverDescriptor.hasConstrainedReturnValue() );

ReturnValueDescriptor returnValueDescriptor = getDriverDescriptor.getReturnValueDescriptor();
assertTrue( returnValueDescriptor.getConstraintDescriptors().isEmpty() );
assertTrue( returnValueDescriptor.isCascaded() );
assertEquals( 1, returnValueDescriptor.getGroupConversions().size() );

//load(List<Person>, List<PieceOfLuggage>) has one cross-parameter constraint
MethodDescriptor loadDescriptor = carDescriptor.getConstraintsForMethod(
        "load",
        List.class,
        List.class
);
assertTrue( loadDescriptor.hasConstrainedParameters() );
assertFalse( loadDescriptor.hasConstrainedReturnValue() );
assertEquals(
        1,
        loadDescriptor.getCrossParameterDescriptor().getConstraintDescriptors().size()
);

//Car(String, String, Person, String) has one constrained parameter
ConstructorDescriptor constructorDescriptor = carDescriptor.getConstraintsForConstructor(
        String.class,
        String.class,
        Person.class,
        String.class
);

assertEquals( "Car", constructorDescriptor.getName() );
assertFalse( constructorDescriptor.hasConstrainedReturnValue() );
assertTrue( constructorDescriptor.hasConstrainedParameters() );
assertEquals(
        1,
        constructorDescriptor.getParameterDescriptors()
                .get( 0 )
                .getConstraintDescriptors()
                .size()
);

getName()返回给定方法或构造函数的名称。方法hasConstrainedParameters()hasConstrainedReturnValue()可用于快速检查可执行元素是否具有任何参数约束(单个参数上的约束或跨参数约束)或返回值约束。

请注意,约束不会直接在MethodDescriptorConstructorDescriptor上公开,而是公开在表示可执行文件参数、返回值和跨参数约束的专用描述符上。要获取这些描述符之一,请分别调用getParameterDescriptors()getReturnValueDescriptor()getCrossParameterDescriptor()

这些描述符提供了对元素约束(getConstraintDescriptors())的访问,并且在参数和返回值的情况下,对级联验证的配置(isValid()getGroupConversions())提供了访问。对于参数,您还可以通过getName()getIndex()分别检索索引和名称,这些索引和名称由当前使用的参数名称提供者返回(参见第 9.2.4 节,“ParameterNameProvider)。

遵循 JavaBeans 命名约定的 Getter 方法被视为 Bean 属性,但也被视为受约束的方法。

这意味着您可以通过获取PropertyDescriptor(例如BeanDescriptor.getConstraintsForProperty("foo"))或检查 Getter 的MethodDescriptor的返回值描述符(例如BeanDescriptor.getConstraintsForMethod("getFoo").getReturnValueDescriptor())来检索相关元数据。

10.4. ElementDescriptor

ElementDescriptor接口是各个描述符类型(如BeanDescriptorPropertyDescriptor等)的通用基类。除了getConstraintDescriptors()之外,它还提供了一些所有描述符共有的方法。

hasConstraints()允许快速检查元素是否具有任何直接约束(例如,在BeanDescriptor的情况下,类级约束)。

getElementClass()返回给定描述符所表示的元素的 Java 类型。更具体地说,该方法返回

  • BeanDescriptor上调用时,对象的类型

  • 在分别调用PropertyDescriptorParameterDescriptor时,属性或参数的类型

  • CrossParameterDescriptor上调用时,Object[].class

  • 在调用ConstructorDescriptorMethodDescriptorReturnValueDescriptor时,返回值类型。对于没有返回值的方法,将返回void.class

示例 10.5,“使用ElementDescriptor 方法” 展示了如何使用这些方法。

示例 10.5:使用ElementDescriptor 方法
PropertyDescriptor manufacturerDescriptor = carDescriptor.getConstraintsForProperty(
        "manufacturer"
);

assertTrue( manufacturerDescriptor.hasConstraints() );
assertEquals( String.class, manufacturerDescriptor.getElementClass() );

CrossParameterDescriptor loadCrossParameterDescriptor = carDescriptor.getConstraintsForMethod(
        "load",
        List.class,
        List.class
).getCrossParameterDescriptor();

assertTrue( loadCrossParameterDescriptor.hasConstraints() );
assertEquals( Object[].class, loadCrossParameterDescriptor.getElementClass() );

最后,ElementDescriptor提供了对ConstraintFinder API 的访问,该 API 允许您以细粒度的方式查询约束元数据。 示例 10.6,“ConstraintFinder 的用法” 展示了如何通过findConstraints()检索ConstraintFinder实例并使用该 API 查询约束元数据。

示例 10.6:ConstraintFinder 的用法
PropertyDescriptor manufacturerDescriptor = carDescriptor.getConstraintsForProperty(
        "manufacturer"
);

//"manufacturer" constraints are declared on the getter, not the field
assertTrue(
        manufacturerDescriptor.findConstraints()
                .declaredOn( ElementType.FIELD )
                .getConstraintDescriptors()
                .isEmpty()
);

//@NotNull on Vehicle#getManufacturer() is part of another group
assertEquals(
        1,
        manufacturerDescriptor.findConstraints()
                .unorderedAndMatchingGroups( Default.class )
                .getConstraintDescriptors()
                .size()
);

//@Size on Car#getManufacturer()
assertEquals(
        1,
        manufacturerDescriptor.findConstraints()
                .lookingAt( Scope.LOCAL_ELEMENT )
                .getConstraintDescriptors()
                .size()
);

//@Size on Car#getManufacturer() and @NotNull on Vehicle#getManufacturer()
assertEquals(
        2,
        manufacturerDescriptor.findConstraints()
                .lookingAt( Scope.HIERARCHY )
                .getConstraintDescriptors()
                .size()
);

//Combining several filter options
assertEquals(
        1,
        manufacturerDescriptor.findConstraints()
                .declaredOn( ElementType.METHOD )
                .lookingAt( Scope.HIERARCHY )
                .unorderedAndMatchingGroups( Vehicle.Basic.class )
                .getConstraintDescriptors()
                .size()
);

通过declaredOn(),您可以搜索在特定元素类型上声明的ConstraintDescriptors。这对于查找在字段或 Getter 方法上声明的属性约束很有用。

unorderedAndMatchingGroups()将结果约束限制为与给定验证组匹配的约束。

lookingAt()允许区分直接在元素上指定的约束(Scope.LOCAL_ELEMENT)或属于元素但位于类层次结构中的任何位置的约束(Scope.HIERARCHY)。

您还可以组合不同的选项,如最后一个示例所示。

顺序不受unorderedAndMatchingGroups()影响,但组继承和通过序列继承则受影响。

10.5. ContainerDescriptorContainerElementTypeDescriptor

ContainerDescriptor接口是所有支持容器元素约束和级联验证的元素(PropertyDescriptorParameterDescriptorReturnValueDescriptor)的通用接口。

它有一个名为getConstrainedContainerElementTypes()的方法,它返回一个ContainerElementTypeDescriptor集。

ContainerElementTypeDescriptor扩展ContainerDescriptor以支持嵌套容器元素约束。

ContainerElementTypeDescriptor包含有关容器、约束和级联验证的信息。

示例 10.7,“使用ContainerElementTypeDescriptor 展示了如何使用getConstrainedContainerElementTypes()检索ContainerElementTypeDescriptor集。

示例 10.7:使用ContainerElementTypeDescriptor
PropertyDescriptor booksDescriptor = libraryDescriptor.getConstraintsForProperty(
        "books"
);

Set<ContainerElementTypeDescriptor> booksContainerElementTypeDescriptors =
        booksDescriptor.getConstrainedContainerElementTypes();
ContainerElementTypeDescriptor booksContainerElementTypeDescriptor =
        booksContainerElementTypeDescriptors.iterator().next();

assertTrue( booksContainerElementTypeDescriptor.hasConstraints() );
assertTrue( booksContainerElementTypeDescriptor.isCascaded() );
assertEquals(
        0,
        booksContainerElementTypeDescriptor.getTypeArgumentIndex().intValue()
);
assertEquals(
        List.class,
        booksContainerElementTypeDescriptor.getContainerClass()
);

Set<ConstraintDescriptor<?>> constraintDescriptors =
        booksContainerElementTypeDescriptor.getConstraintDescriptors();
ConstraintDescriptor<?> constraintDescriptor =
        constraintDescriptors.iterator().next();

assertEquals(
        NotNull.class,
        constraintDescriptor.getAnnotation().annotationType()
);

10.6. GroupConversionDescriptor

所有那些表示可以进行级联验证的元素的描述符类型(即PropertyDescriptorParameterDescriptorReturnValueDescriptor)都通过getGroupConversions()提供对元素组转换的访问。返回的集合包含每个配置的转换的GroupConversionDescriptor,允许检索转换的源组和目标组。 示例 10.8,“使用GroupConversionDescriptor 展示了一个示例。

示例 10.8:使用GroupConversionDescriptor
PropertyDescriptor driverDescriptor = carDescriptor.getConstraintsForProperty( "driver" );

Set<GroupConversionDescriptor> groupConversions = driverDescriptor.getGroupConversions();
assertEquals( 1, groupConversions.size() );

GroupConversionDescriptor groupConversionDescriptor = groupConversions.iterator()
        .next();
assertEquals( Default.class, groupConversionDescriptor.getFrom() );
assertEquals( Person.Basic.class, groupConversionDescriptor.getTo() );

10.7. ConstraintDescriptor

最后但并非最不重要的是,ConstraintDescriptor接口描述了单个约束及其组成的约束。通过该接口的实例,您可以访问约束注释及其参数。

示例 10.9,“使用ConstraintDescriptor 展示了如何从ConstraintDescriptor中检索默认约束属性(如消息模板、组等)以及自定义约束属性(piecesOfLuggagePerPassenger)和其它元数据,如约束的注释类型及其验证器。

示例 10.9:使用ConstraintDescriptor
//descriptor for the @LuggageCountMatchesPassengerCount constraint on the
//load(List<Person>, List<PieceOfLuggage>) method
ConstraintDescriptor<?> constraintDescriptor = carDescriptor.getConstraintsForMethod(
        "load",
        List.class,
        List.class
).getCrossParameterDescriptor().getConstraintDescriptors().iterator().next();

//constraint type
assertEquals(
        LuggageCountMatchesPassengerCount.class,
        constraintDescriptor.getAnnotation().annotationType()
);

//standard constraint attributes
assertEquals( SeverityInfo.class, constraintDescriptor.getPayload().iterator().next() );
assertEquals(
        ConstraintTarget.PARAMETERS,
        constraintDescriptor.getValidationAppliesTo()
);
assertEquals( Default.class, constraintDescriptor.getGroups().iterator().next() );
assertEquals(
        "There must not be more than {piecesOfLuggagePerPassenger} pieces of luggage per " +
        "passenger.",
        constraintDescriptor.getMessageTemplate()
);

//custom constraint attribute
assertEquals(
        2,
        constraintDescriptor.getAttributes().get( "piecesOfLuggagePerPassenger" )
);

//no composing constraints
assertTrue( constraintDescriptor.getComposingConstraints().isEmpty() );

//validator class
assertEquals(
        Arrays.<Class<?>>asList( LuggageCountMatchesPassengerCount.Validator.class ),
        constraintDescriptor.getConstraintValidatorClasses()
);

11. 与其它框架集成

Hibernate Validator 旨在用于实现多层数据验证,其中约束在单个位置(带注释的域模型)中表达,并在应用程序的多个不同层中检查。因此,它与其它技术的多个集成点。

11.1. ORM 集成

Hibernate Validator 与 Hibernate ORM 和所有纯 Java 持久性提供程序集成。

当应验证延迟加载的关联时,建议将约束放在关联的 Getter 上。Hibernate ORM 将延迟加载的关联替换为代理实例,这些实例在通过 Getter 请求时被初始化/加载。如果在这种情况下将约束放在字段级别,则将使用实际的代理实例,这将导致验证错误。

11.1.1. 数据库模式级验证

Hibernate ORM 开箱即用,将您为实体定义的约束转换为映射元数据。例如,如果实体的属性使用@NotNull进行注释,则其列将在 Hibernate ORM 生成的 DDL 模式中声明为not null

如果由于某种原因需要禁用此功能,请将hibernate.validator.apply_to_ddl设置为false。另请参见第 2.3.1 节,“Jakarta Bean Validation 约束”第 2.3.2 节,“附加约束”

您还可以通过设置属性org.hibernate.validator.group.ddl将 DDL 约束生成限制为定义的约束的子集。该属性指定约束必须属于的组的逗号分隔、完全限定的类名,以便被视为 DDL 模式生成的目标。

11.1.2. Hibernate ORM 基于事件的验证

Hibernate Validator 具有内置的 Hibernate 事件监听器 - org.hibernate.cfg.beanvalidation.BeanValidationEventListener - 它是 Hibernate ORM 的一部分。每当发生PreInsertEventPreUpdateEventPreDeleteEvent时,监听器都会验证实体实例的所有约束,并在任何约束违反时抛出异常。默认情况下,对象将在 Hibernate ORM 执行任何插入或更新之前进行检查。默认情况下,预删除事件不会触发验证。您可以使用属性jakarta.persistence.validation.group.pre-persistjakarta.persistence.validation.group.pre-updatejakarta.persistence.validation.group.pre-remove配置每个事件类型要验证的组。这些属性的值是逗号分隔的完全限定的组类名。 示例 11.1,“BeanValidationEvenListener 的手动配置” 展示了这些属性的默认值。在这种情况下,它们也可以省略。

在约束违反时,该事件将引发运行时ConstraintViolationException,该异常包含一组ConstraintViolation实例,描述了每个错误。

如果类路径中存在 Hibernate Validator,Hibernate ORM 将透明地使用它。若要避免验证(即使类路径中存在 Hibernate Validator),请将jakarta.persistence.validation.mode设置为 none。

如果 Bean 未使用验证注释进行注释,则不会产生运行时性能成本。

如果您需要手动设置 Hibernate ORM 的事件监听器,请在hibernate.cfg.xml中使用以下配置

示例 11.1:BeanValidationEvenListener 的手动配置
<hibernate-configuration>
    <session-factory>
        ...
        <property name="jakarta.persistence.validation.group.pre-persist">
            jakarta.validation.groups.Default
        </property>
        <property name="jakarta.persistence.validation.group.pre-update">
            jakarta.validation.groups.Default
        </property>
        <property name="jakarta.persistence.validation.group.pre-remove"></property>
        ...
        <event type="pre-update">
            <listener class="org.hibernate.cfg.beanvalidation.BeanValidationEventListener"/>
        </event>
        <event type="pre-insert">
            <listener class="org.hibernate.cfg.beanvalidation.BeanValidationEventListener"/>
        </event>
        <event type="pre-delete">
            <listener class="org.hibernate.cfg.beanvalidation.BeanValidationEventListener"/>
        </event>
    </session-factory>
</hibernate-configuration>

11.1.3. JPA

如果您使用的是 JPA 2 且类路径中存在 Hibernate Validator,JPA2 规范要求启用 Jakarta Bean Validation。属性jakarta.persistence.validation.group.pre-persistjakarta.persistence.validation.group.pre-updatejakarta.persistence.validation.group.pre-remove(如第 11.1.2 节,“Hibernate ORM 基于事件的验证”中所述)在这种情况下可以在persistence.xml中配置。persistence.xml还定义了一个名为 validation-mode 的节点,它可以设置为AUTOCALLBACKNONE。默认值为AUTO

11.2. JSF & Seam

当使用 JSF2 或 JBoss Seam 并且运行时环境中存在 Hibernate Validator(Jakarta Bean Validation)时,将针对应用程序中的每个字段触发验证。示例 11.2,“在 JSF2 中使用 Jakarta Bean Validation” 显示了 JSF 页面中 f:validateBean 标签的示例。validationGroups 属性是可选的,可用于指定用逗号分隔的验证组列表。默认值为 jakarta.validation.groups.Default。有关更多信息,请参阅 Seam 文档或 JSF 2 规范。

示例 11.2:在 JSF2 中使用 Jakarta Bean Validation
<h:form>

  <f:validateBean validationGroups="jakarta.validation.groups.Default">

    <h:inputText value=#{model.property}/>
    <h:selectOneRadio value=#{model.radioProperty}> ... </h:selectOneRadio>
    <!-- other input components here -->

  </f:validateBean>

</h:form>

JSF 2 和 Jakarta Bean Validation 之间的集成在 JSR-314 的“Jakarta Bean Validation 集成”一章中进行了描述。有趣的是,JSF 2 实现了一个自定义的 MessageInterpolator 以确保正确的本地化。为了鼓励使用 Jakarta Bean Validation 消息机制,默认情况下,JSF 2 仅显示生成的 Bean Validation 消息。但是,可以通过应用程序资源束提供以下配置来配置此行为({0} 将替换为 Jakarta Bean Validation 消息,而 {1} 将替换为 JSF 组件标签)

jakarta.faces.validator.BeanValidator.MESSAGE={1}: {0}

默认值为

jakarta.faces.validator.BeanValidator.MESSAGE={0}

11.3. CDI

从 1.1 版开始,Bean Validation(因此也包括 Jakarta Bean Validation)已与 CDI(Jakarta EE 的上下文和依赖项注入)集成。

此集成提供了 ValidatorValidatorFactory 的 CDI 管理 Bean,并在约束验证器、自定义消息插值器、可遍历解析器、约束验证器工厂、参数名称提供程序、时钟提供程序和值提取器中启用了依赖项注入。

此外,CDI 管理 Bean 的方法和构造函数上的参数和返回值约束将在调用时自动验证。

当您的应用程序在 Jakarta EE 容器上运行时,此集成默认启用。当在 Servlet 容器或纯 Java SE 环境中使用 CDI 时,可以使用 Hibernate Validator 提供的 CDI 可移植扩展。为此,请将可移植扩展添加到您的类路径中,如 第 1.1.2 节,“CDI” 中所述。

11.3.1. 依赖项注入

CDI 的依赖项注入机制使获取 ValidatorFactoryValidator 实例并在您的管理 Bean 中使用它们变得非常容易。只需在您的 Bean 的实例字段上添加 @jakarta.inject.Inject 注解,如 示例 11.3,“通过 @Inject 获取验证器工厂和验证器” 中所示。

示例 11.3:通过 @Inject 获取验证器工厂和验证器
package org.hibernate.validator.referenceguide.chapter11.cdi.validator;

@ApplicationScoped
public class RentalStation {

    @Inject
    private ValidatorFactory validatorFactory;

    @Inject
    private Validator validator;

    //...
}

注入的 Bean 是默认的验证器工厂和验证器实例。为了配置它们(例如,使用自定义消息插值器),可以使用 Jakarta Bean Validation XML 描述符,如 第 8 章,“通过 XML 配置” 中所述。

如果您正在使用多个 Jakarta Bean Validation 提供程序,您可以通过在注入点添加 @HibernateValidator 限定符来确保注入来自 Hibernate Validator 的工厂和验证器,如 示例 11.4,“使用 @HibernateValidator 限定符注解” 中所示。

示例 11.4:使用 @HibernateValidator 限定符注解
package org.hibernate.validator.referenceguide.chapter11.cdi.validator.qualifier;

@ApplicationScoped
public class RentalStation {

    @Inject
    @HibernateValidator
    private ValidatorFactory validatorFactory;

    @Inject
    @HibernateValidator
    private Validator validator;

    //...
}

限定符注解的完全限定名称为 org.hibernate.validator.cdi.HibernateValidator。请确保不要导入 org.hibernate.validator.HibernateValidator,它是在使用引导 API 时用于选择 Hibernate Validator 的 ValidationProvider 实现(请参阅 第 9.1 节,“获取 ValidatorFactoryValidator)。

通过 @Inject,您还可以将依赖项注入约束验证器和其他 Jakarta Bean Validation 对象,例如 MessageInterpolator 实现等。

示例 11.5,“具有注入 Bean 的约束验证器” 演示了如何在 ConstraintValidator 实现中使用注入的 CDI Bean 来确定给定的约束是否有效。如示例所示,您还可以使用 @PostConstruct@PreDestroy 回调来实现任何必需的构造和销毁逻辑。

示例 11.5:具有注入 Bean 的约束验证器
package org.hibernate.validator.referenceguide.chapter11.cdi.injection;

public class ValidLicensePlateValidator
        implements ConstraintValidator<ValidLicensePlate, String> {

    @Inject
    private VehicleRegistry vehicleRegistry;

    @PostConstruct
    public void postConstruct() {
        //do initialization logic...
    }

    @PreDestroy
    public void preDestroy() {
        //do destruction logic...
    }

    @Override
    public void initialize(ValidLicensePlate constraintAnnotation) {
    }

    @Override
    public boolean isValid(String licensePlate, ConstraintValidatorContext constraintContext) {
        return vehicleRegistry.isValidLicensePlate( licensePlate );
    }
}

11.3.2. 方法验证

CDI 的方法拦截机制允许与 Jakarta Bean Validation 的方法验证功能紧密集成。只需将约束注解放在 CDI Bean 的可执行文件的参数和返回值上,它们将在调用方法或构造函数之前(参数约束)和之后(返回值约束)自动验证。

请注意,不需要显式拦截器绑定,而是将为所有具有约束方法和构造函数的管理 Bean 自动注册必需的方法验证拦截器。

拦截器 org.hibernate.validator.cdi.internal.interceptor.ValidationInterceptororg.hibernate.validator.cdi.internal.ValidationExtension 注册。这在 Jakarta EE 运行时环境中隐式发生,或者通过添加 hibernate-validator-cdi 工件显式发生 - 请参阅 第 1.1.2 节,“CDI”

您可以在 示例 11.6,“具有方法级约束的 CDI 管理 Bean” 中看到一个示例。

示例 11.6:具有方法级约束的 CDI 管理 Bean
package org.hibernate.validator.referenceguide.chapter11.cdi.methodvalidation;

@ApplicationScoped
public class RentalStation {

    @Valid
    public RentalStation() {
        //...
    }

    @NotNull
    @Valid
    public Car rentCar(
            @NotNull Customer customer,
            @NotNull @Future Date startDate,
            @Min(1) int durationInDays) {
        //...
        return null;
    }

    @NotNull
    List<Car> getAvailableCars() {
        //...
        return null;
    }
}
package org.hibernate.validator.referenceguide.chapter11.cdi.methodvalidation;

@RequestScoped
public class RentCarRequest {

    @Inject
    private RentalStation rentalStation;

    public void rentCar(String customerId, Date startDate, int duration) {
        //causes ConstraintViolationException
        rentalStation.rentCar( null, null, -1 );
    }
}

这里,RentalStation Bean 托管多个方法约束。当从另一个 Bean(例如 RentCarRequest)调用 RentalStation 的方法时,将自动验证调用方法的约束。如果传递任何非法参数值(如示例所示),方法拦截器将抛出 ConstraintViolationException,提供有关违反约束的详细信息。如果方法的返回值违反了任何返回值约束,情况也是如此。

类似地,构造函数约束在调用时会自动验证。在示例中,由于构造函数返回值用 @Valid 标记,因此构造函数返回的 RentalStation 对象将被验证。

11.3.2.1. 验证的可执行类型

Jakarta Bean Validation 允许对自动验证的可执行类型进行细粒度控制。默认情况下,构造函数和非 getter 方法上的约束将被验证。因此,当调用该方法时,示例 11.6,“具有方法级约束的 CDI 管理 Bean” 中的 RentalStation#getAvailableCars() 方法上的 @NotNull 约束不会被验证。

您可以使用以下选项来配置调用时验证哪些类型的可执行文件

如果为给定的可执行文件指定了多个配置源,则可执行文件级别的 @ValidateOnExecution 优先于类型级别的 @ValidateOnExecution,并且 @ValidateOnExecution 通常优先于 META- INF/validation.xml 中全局配置的类型。

示例 11.7,“使用 @ValidateOnExecution 显示了如何使用 @ValidateOnExecution 注解

示例 11.7:使用 @ValidateOnExecution
package org.hibernate.validator.referenceguide.chapter11.cdi.methodvalidation.configuration;

@ApplicationScoped
@ValidateOnExecution(type = ExecutableType.ALL)
public class RentalStation {

    @Valid
    public RentalStation() {
        //...
    }

    @NotNull
    @Valid
    @ValidateOnExecution(type = ExecutableType.NONE)
    public Car rentCar(
            @NotNull Customer customer,
            @NotNull @Future Date startDate,
            @Min(1) int durationInDays) {
        //...
        return null;
    }

    @NotNull
    public List<Car> getAvailableCars() {
        //...
        return null;
    }
}

这里,方法 rentCar() 在调用时不会被验证,因为它用 @ValidateOnExecution(type = ExecutableType.NONE) 注解。相反,构造函数和方法 getAvailableCars() 会被验证,因为在类型级别上指定了 @ValidateOnExecution(type = ExecutableType.ALL)ExecutableType.ALL 是明确指定所有类型 CONSTRUCTORSGETTER_METHODSNON_GETTER_METHODS 的更简洁形式。

可以通过在 META-INF/validation.xml 中指定 <executable-validation enabled="false"/> 在全局范围内关闭可执行文件验证。在这种情况下,将忽略所有 @ValidateOnExecution 注解。

请注意,当方法覆盖或实现超类型方法时,将从覆盖或实现的该方法(如通过方法本身或超类型上的 @ValidateOnExecution 给定)获取配置。这可以保护超类型方法的客户端免受配置的意外更改,例如,在子类型中禁用覆盖可执行文件的验证。

如果 CDI 管理 Bean 覆盖或实现超类型方法,并且此超类型方法托管任何约束,则验证拦截器可能无法与 Bean 正确注册,导致 Bean 的方法在调用时不被验证。在这种情况下,您可以像 示例 11.8,“使用 ExecutableType.IMPLICIT 中所示那样,在子类上指定可执行类型 IMPLICIT,这将确保发现所有必需的元数据,并且当调用 ExpressRentalStation 上的方法时,验证拦截器将启动。

示例 11.8:使用 ExecutableType.IMPLICIT
package org.hibernate.validator.referenceguide.chapter11.cdi.methodvalidation.implicit;

@ValidateOnExecution(type = ExecutableType.ALL)
public interface RentalStation {

    @NotNull
    @Valid
    Car rentCar(
            @NotNull Customer customer,
            @NotNull @Future Date startDate,
            @Min(1) int durationInDays);

    @NotNull
    List<Car> getAvailableCars();
}
package org.hibernate.validator.referenceguide.chapter11.cdi.methodvalidation.implicit;

@ApplicationScoped
@ValidateOnExecution(type = ExecutableType.IMPLICIT)
public class ExpressRentalStation implements RentalStation {

    @Override
    public Car rentCar(Customer customer, Date startDate, @Min(1) int durationInDays) {
        //...
        return null;
    }

    @Override
    public List<Car> getAvailableCars() {
        //...
        return null;
    }
}

11.4. Jakarta EE

当您的应用程序在 Jakarta EE 应用程序服务器(例如 WildFly)上运行时,您还可以通过在管理对象(例如 EJB 等)中使用 @Resource 注解进行注入来获取 ValidatorValidatorFactory 实例,如 示例 11.9,“通过 @Resource 注解注入获取 ValidatorValidatorFactory 中所示。

示例 11.9:通过 @Resource 注解注入获取 ValidatorValidatorFactory
package org.hibernate.validator.referenceguide.chapter11.javaee;

public class RentalStationBean {

    @Resource
    private ValidatorFactory validatorFactory;

    @Resource
    private Validator validator;

    //...
}

或者,您可以分别从 JNDI 中以名称“java:comp/Validator”和“java:comp/ValidatorFactory”获取验证器和验证器工厂。

与通过 @Inject 进行的基于 CDI 的注入类似,这些对象代表默认的验证器和验证器工厂,因此可以使用 XML 描述符 META-INF/validation.xml 进行配置(请参阅 第 8 章,“通过 XML 配置”)。

当您的应用程序启用 CDI 时,注入的对象也是 CDI 感知的,例如,它们支持约束验证器中的依赖项注入。

11.5. JavaFX

Hibernate Validator 还提供对 JavaFX 属性解包的支持。如果 JavaFX 存在于类路径中,则会自动注册 JavaFX 属性的 ValueExtractor。请参阅 第 7.4 节,“JavaFX 值提取器” 以获取示例和进一步的讨论。

12. Hibernate Validator 特定内容

在本章中,您将学习如何在使用 Jakarta Bean Validation 规范定义的功能之外,使用 Hibernate Validator 提供的几个功能。这包括快速失败模式、用于编程约束配置的 API 以及约束的布尔组合。

只要新的 API 或 SPI 处于开发阶段,就会用 org.hibernate.validator.Incubating 注解标记。这意味着此类元素(例如,包、类型、方法、常量等)可能在后续版本中发生不兼容的更改(或删除)。鼓励使用孵化 API/SPI 成员(以便开发团队可以获得有关这些新功能的反馈),但您应准备好根据需要在升级到 Hibernate Validator 的新版本时更新使用它们的代码。

使用以下部分中描述的功能可能会导致应用程序代码在 Jakarta Bean Validation 提供程序之间不可移植。

12.1. 公共 API

但是,让我们先看一下 Hibernate Validator 的公共 API。您可以在下面找到属于此 API 的所有包及其用途的列表。请注意,当一个包是公共 API 的一部分时,这并不一定适用于它的子包。

org.hibernate.validator

Jakarta Bean Validation 引导机制使用的类(例如,验证提供程序、配置类);有关更多详细信息,请参阅 第 9 章,“引导”

org.hibernate.validator.cfgorg.hibernate.validator.cfg.contextorg.hibernate.validator.cfg.defsorg.hibernate.validator.spi.cfg

Hibernate Validator 的约束声明流畅 API;在 org.hibernate.validator.cfg 中,您将找到 ConstraintMapping 接口,在 org.hibernate.validator.cfg.defs 中找到所有约束定义,以及在 org.hibernate.validator.spi.cfg 中找到用于使用 API 配置默认验证器工厂的回调。有关详细信息,请参阅 第 12.4 节,“程序化约束定义和声明”

org.hibernate.validator.constraintsorg.hibernate.validator.constraints.brorg.hibernate.validator.constraints.pl

除了 Jakarta Bean Validation 规范定义的内置约束之外,Hibernate Validator 还提供了一些有用的自定义约束;这些约束在 第 2.3.2 节,“其他约束” 中详细说明。

org.hibernate.validator.constraintvalidation

扩展的约束验证器上下文,允许为消息插值设置自定义属性。 第 12.13.1 节,“HibernateConstraintValidatorContext 描述了如何使用此功能。

org.hibernate.validator.grouporg.hibernate.validator.spi.group

组序列提供程序功能,允许您根据验证对象的 state 定义动态默认组序列;具体内容可以在 第 5.4 节,“重新定义默认组序列” 中找到。

org.hibernate.validator.messageinterpolationorg.hibernate.validator.resourceloadingorg.hibernate.validator.spi.resourceloading

与约束消息插值相关的类;第一个包包含 Hibernate Validator 的默认消息插值器 ResourceBundleMessageInterpolator。后两个包为资源捆绑包的加载提供了 ResourceBundleLocator SPI(请参阅 第 4.2.1 节,“ResourceBundleLocator)及其默认实现。

org.hibernate.validator.parameternameprovider

基于 Paranamer 库的 ParameterNameProvider,请参阅 第 12.14 节,“基于 Paranamer 的 ParameterNameProvider

org.hibernate.validator.propertypath

jakarta.validation.Path API 的扩展,请参阅 第 12.7 节,“Path API 的扩展”

org.hibernate.validator.spi.constraintdefinition

用于以编程方式注册其他约束验证器的 SPI,请参阅 第 12.15 节,“提供约束定义”

org.hibernate.validator.spi.messageinterpolation

可以用来调整插值约束违反消息时区域设置解析的 SPI。请参阅 第 12.12 节,“自定义区域设置解析”

org.hibernate.validator.spi.nodenameprovider

可以用来更改在构造属性路径时如何解析属性名称的 SPI。请参阅 第 12.18 节,“自定义约束违规的属性名称解析”

Hibernate Validator 的公共包分为两类:实际的 API 部分旨在由客户端调用使用(例如,用于程序化约束声明的 API 或自定义约束),而 SPI(服务提供程序接口)包包含旨在由客户端实现的接口(例如,ResourceBundleLocator)。

表中未列出的任何包都是 Hibernate Validator 的内部包,不打算被客户端访问。这些内部包的内容可能会在不同版本之间发生变化,恕不另行通知,因此可能会破坏任何依赖它的客户端代码。

12.2. 快速失败模式

使用快速失败模式,Hibernate Validator 允许在第一个约束违反发生时从当前验证返回。这对于大型对象图的验证非常有用,在这些验证中,您只对快速检查是否存在任何约束违反感兴趣。

示例 12.1,“使用快速失败验证模式” 显示了如何引导和使用启用快速失败的验证器。

示例 12.1:使用快速失败验证模式
package org.hibernate.validator.referenceguide.chapter12.failfast;

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...
}
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .buildValidatorFactory()
        .getValidator();

Car car = new Car( null, false );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

这里,被验证的对象实际上无法满足在 Car 类上声明的两个约束,但验证调用只产生一个 ConstraintViolation,因为启用了快速失败模式。

无法保证约束的评估顺序,即无法确定返回的违规是来自 @NotNull 约束还是 @AssertTrue 约束。如果需要,可以使用组序列强制执行确定性的评估顺序,如 第 5.3 节,“定义组序列” 中所述。

请参阅 第 9.2.8 节,“提供程序特定的设置”,了解在引导验证器时启用快速失败模式的不同方法。

12.3. 类层次结构中方法验证需求的放松

Jakarta Bean Validation 规范定义了一组在类层次结构中定义方法约束时适用的先决条件。这些先决条件在 Jakarta Bean Validation 规范的 第 5.6.5 节 中定义。另请参阅本指南中的 第 3.1.4 节,“继承层次结构中的方法约束”

根据规范,Jakarta Bean Validation 提供程序可以放松这些先决条件。使用 Hibernate Validator,您可以通过以下两种方式之一执行此操作。

首先,您可以使用 validation.xml 中的配置属性 hibernate.validator.allow_parameter_constraint_overridehibernate.validator.allow_multiple_cascaded_validation_on_resulthibernate.validator.allow_parallel_method_parameter_constraint。请参阅示例 示例 12.2,“通过属性配置类层次结构中的方法验证行为”

示例 12.2:通过属性配置类层次结构中的方法验证行为
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">
    <default-provider>org.hibernate.validator.HibernateValidator</default-provider>

    <property name="hibernate.validator.allow_parameter_constraint_override">true</property>
    <property name="hibernate.validator.allow_multiple_cascaded_validation_on_result">true</property>
    <property name="hibernate.validator.allow_parallel_method_parameter_constraint">true</property>
</validation-config>

或者,这些设置可以在程序化引导过程中应用。

示例 12.3:配置类层次结构中的方法验证行为
HibernateValidatorConfiguration configuration = Validation.byProvider( HibernateValidator.class ).configure();

configuration.allowMultipleCascadedValidationOnReturnValues( true )
        .allowOverridingMethodAlterParameterConstraint( true )
        .allowParallelMethodsDefineParameterConstraints( true );

默认情况下,所有这些属性都为 false,实现了 Jakarta Bean Validation 规范中定义的默认行为。

更改方法验证的默认行为将导致非符合规范且不可移植的应用程序。确保您了解自己在做什么,并且您的用例确实需要更改默认行为。

12.4. 程序化约束定义和声明

根据 Jakarta Bean Validation 规范,您可以使用 Java 注释和基于 XML 的约束映射来定义和声明约束。

此外,Hibernate Validator 提供了一个流畅的 API,允许程序化配置约束。用例包括根据某些应用程序 state 动态添加约束,或者在测试中,您需要在不同场景中使用具有不同约束的实体,但不想为每个测试用例实现实际的 Java 类。

默认情况下,通过流畅的 API 添加的约束是对通过标准配置功能配置的约束的补充。但也可以在需要时忽略通过注释和 XML 配置的约束。

该 API 以 ConstraintMapping 接口为中心。您可以通过 HibernateValidatorConfiguration#createConstraintMapping() 获取一个新的映射,然后以流畅的方式对其进行配置,如 示例 12.4,“程序化约束声明” 中所示。

示例 12.4:程序化约束声明
HibernateValidatorConfiguration configuration = Validation
        .byProvider( HibernateValidator.class )
        .configure();

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "manufacturer" )
            .constraint( new NotNullDef() )
        .field( "licensePlate" )
            .ignoreAnnotations( true )
            .constraint( new NotNullDef() )
            .constraint( new SizeDef().min( 2 ).max( 14 ) )
    .type( RentalCar.class )
        .getter( "rentalStation" )
            .constraint( new NotNullDef() );

Validator validator = configuration.addMapping( constraintMapping )
        .buildValidatorFactory()
        .getValidator();

可以使用方法链在多个类和属性上配置约束。约束定义类 NotNullDefSizeDef 是辅助类,允许以类型安全的方式配置约束参数。所有内置约束的定义类都存在于 org.hibernate.validator.cfg.defs 包中。通过调用 ignoreAnnotations(),任何通过注释或 XML 配置的约束都会被忽略,对于给定的元素而言。

每个元素(类型、属性、方法等)只能在一个约束映射中配置一次,用于设置一个验证器工厂。否则将引发 ValidationException

不支持通过配置子类型来向未覆盖的超类型属性和方法添加约束。在这种情况下,您需要配置超类型。

配置完映射后,您必须将其添加回配置对象,然后从中获取验证器工厂。

对于自定义约束,您可以创建自己的扩展 ConstraintDef 的定义类,也可以使用 GenericConstraintDef,如 示例 12.5,“自定义约束的程序化声明” 中所示。

示例 12.5:自定义约束的程序化声明
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "licensePlate" )
            .constraint( new GenericConstraintDef<>( CheckCase.class )
                .param( "value", CaseMode.UPPER )
            );

程序化 API 支持容器元素约束,使用 containerElementType()

示例 12.6,“嵌套容器元素约束的程序化声明” 显示了一个在嵌套容器元素上声明约束的示例。

示例 12.6:嵌套容器元素约束的程序化声明
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "manufacturer" )
            .constraint( new NotNullDef() )
        .field( "licensePlate" )
            .ignoreAnnotations( true )
            .constraint( new NotNullDef() )
            .constraint( new SizeDef().min( 2 ).max( 14 ) )
        .field( "partManufacturers" )
            .containerElementType( 0 )
                .constraint( new NotNullDef() )
            .containerElementType( 1, 0 )
                .constraint( new NotNullDef() )
    .type( RentalCar.class )
        .getter( "rentalStation" )
            .constraint( new NotNullDef() );

如示例所示,传递给 containerElementType() 的参数是用于获取所需嵌套容器元素类型的类型参数索引的路径。

通过调用 valid(),您可以将成员标记为级联验证,这等效于使用 @Valid 注释它。使用 convertGroup() 方法(相当于 @ConvertGroup)配置在级联验证期间要应用的任何组转换。示例可以在 示例 12.7,“将属性标记为级联验证” 中看到。

示例 12.7:将属性标记为级联验证
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "driver" )
            .constraint( new NotNullDef() )
            .valid()
            .convertGroup( Default.class ).to( PersonDefault.class )
        .field( "partManufacturers" )
            .containerElementType( 0 )
                .valid()
            .containerElementType( 1, 0 )
                .valid()
    .type( Person.class )
        .field( "name" )
            .constraint( new NotNullDef().groups( PersonDefault.class ) );

您不仅可以使用流畅的 API 配置 Bean 约束,还可以配置方法和构造函数约束。如 示例 12.8,“方法和构造函数约束的程序化声明” 中所示,构造函数通过其参数类型识别,方法通过其名称和参数类型识别。选择了一个方法或构造函数后,您可以标记其参数和/或返回值以进行级联验证,并添加约束以及跨参数约束。

如示例所示,valid() 也可以在容器元素类型上调用。

示例 12.8:方法和构造函数约束的程序化声明
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .constructor( String.class )
            .parameter( 0 )
                .constraint( new SizeDef().min( 3 ).max( 50 ) )
            .returnValue()
                .valid()
        .method( "drive", int.class )
            .parameter( 0 )
                .constraint( new MaxDef().value( 75 ) )
        .method( "load", List.class, List.class )
            .crossParameter()
                .constraint( new GenericConstraintDef<>(
                        LuggageCountMatchesPassengerCount.class ).param(
                            "piecesOfLuggagePerPassenger", 2
                        )
                )
        .method( "getDriver" )
            .returnValue()
                .constraint( new NotNullDef() )
                .valid();

最后但并非最不重要的是,您可以配置类型的默认组序列或默认组序列提供程序,如以下示例所示。

示例 12.9:默认组序列和默认组序列提供程序的配置
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .defaultGroupSequence( Car.class, CarChecks.class )
    .type( RentalCar.class )
        .defaultGroupSequenceProviderClass( RentalCarGroupSequenceProvider.class );

12.5. 将程序化约束声明应用于默认验证器工厂

如果您没有手动引导验证器工厂,而是使用通过 META-INF/validation.xml 配置的默认工厂(请参阅 第 8 章,通过 XML 配置),您可以通过创建一到多个约束映射贡献者来添加一到多个约束映射。为此,请实现 ConstraintMappingContributor 合同

示例 12.10:自定义 ConstraintMappingContributor 实现
package org.hibernate.validator.referenceguide.chapter12.constraintapi;

public class MyConstraintMappingContributor implements ConstraintMappingContributor {

    @Override
    public void createConstraintMappings(ConstraintMappingBuilder builder) {
        builder.addConstraintMapping()
            .type( Marathon.class )
                .getter( "name" )
                    .constraint( new NotNullDef() )
                .field( "numberOfHelpers" )
                    .constraint( new MinDef().value( 1 ) );

        builder.addConstraintMapping()
            .type( Runner.class )
                .field( "paidEntryFee" )
                    .constraint( new AssertTrueDef() );
    }
}

然后,您需要在 META-INF/validation.xml 中使用属性键 hibernate.validator.constraint_mapping_contributors 指定贡献者实现的完全限定类名。您可以通过用逗号分隔它们来指定多个贡献者。

12.6. 高级约束组合功能

12.6.1. 纯粹组合约束的验证目标规范

如果您在方法声明中指定一个纯粹组合的约束 - 即一个本身没有验证器,而是完全由其他组合约束组成的约束 - 验证引擎无法确定该约束是作为返回值约束还是作为跨参数约束来应用。

Hibernate Validator 允许通过在组合约束类型的声明上指定 @SupportedValidationTarget 注解来解决此类歧义,如 示例 12.11,“指定纯粹组合约束的验证目标” 所示。@ValidInvoiceAmount 不声明任何验证器,但它完全由 @Min@NotNull 约束组成。@SupportedValidationTarget 确保在方法声明中给出时,约束应用于方法返回值。

示例 12.11:指定纯粹组合约束的验证目标
package org.hibernate.validator.referenceguide.chapter12.purelycomposed;

@Min(value = 0)
@NotNull
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
@ReportAsSingleViolation
public @interface ValidInvoiceAmount {

    String message() default "{org.hibernate.validator.referenceguide.chapter11.purelycomposed."
            + "ValidInvoiceAmount.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @OverridesAttribute(constraint = Min.class, name = "value")
    long value();
}

12.6.2. 约束的布尔组合

Jakarta Bean Validation 规定,组合约束(见 第 6.4 节,“约束组合”)的约束都是通过逻辑 AND 组合在一起的。这意味着所有组成约束都需要返回 true 才能获得整体成功的验证。

Hibernate Validator 对此进行了扩展,允许您通过逻辑 ORNOT 组合约束。为此,您必须使用 ConstraintComposition 注解和枚举 CompositionType 及其值 ANDORALL_FALSE

示例 12.12,“约束的 OR 组合” 显示了如何构建一个组合约束 @PatternOrSize,其中只需要一个组成约束有效即可通过验证。被验证的字符串要么全部是小写字母,要么长度在 2 到 3 个字符之间。

示例 12.12:约束的 OR 组合
package org.hibernate.validator.referenceguide.chapter12.booleancomposition;

@ConstraintComposition(OR)
@Pattern(regexp = "[a-z]")
@Size(min = 2, max = 3)
@ReportAsSingleViolation
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
public @interface PatternOrSize {
    String message() default "{org.hibernate.validator.referenceguide.chapter11." +
            "booleancomposition.PatternOrSize.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

使用 ALL_FALSE 作为组合类型隐式地强制在约束组合验证失败的情况下只报告单个违规。

12.7. Path API 的扩展

Hibernate Validator 为 jakarta.validation.Path API 提供了扩展。对于 ElementKind.PROPERTYElementKind.CONTAINER_ELEMENT 的节点,它允许获取表示的属性的值。为此,请使用 Node#as() 将给定节点缩小到 org.hibernate.validator.path.PropertyNodeorg.hibernate.validator.path.ContainerElementNode 类型,如以下示例所示

示例 12.13:从属性节点获取值
Building building = new Building();

// Assume the name of the person violates a @Size constraint
Person bob = new Person( "Bob" );
Apartment bobsApartment = new Apartment( bob );
building.getApartments().add( bobsApartment );

Set<ConstraintViolation<Building>> constraintViolations = validator.validate( building );

Path path = constraintViolations.iterator().next().getPropertyPath();
Iterator<Path.Node> nodeIterator = path.iterator();

Path.Node node = nodeIterator.next();
assertEquals( node.getName(), "apartments" );
assertSame( node.as( PropertyNode.class ).getValue(), bobsApartment );

node = nodeIterator.next();
assertEquals( node.getName(), "resident" );
assertSame( node.as( PropertyNode.class ).getValue(), bob );

node = nodeIterator.next();
assertEquals( node.getName(), "name" );
assertEquals( node.as( PropertyNode.class ).getValue(), "Bob" );

这对于在属性路径上获取 Set 属性的元素(例如,示例中的 apartments)也非常有用,否则无法识别这些元素(与 MapList 不同,在这种情况下没有键或索引)。

12.8. ConstraintViolation 中的动态负载

在某些情况下,如果约束违规提供额外的数据 - 一个所谓的动态负载,则可以帮助自动处理违规。例如,此动态负载可能包含有关如何解决违规的提示。

可以在 自定义约束 中使用 HibernateConstraintValidatorContext 设置动态负载。这在示例 示例 12.14,“ConstraintValidator 实现设置动态负载” 中展示,其中 jakarta.validation.ConstraintValidatorContext 被解包到 HibernateConstraintValidatorContext 以便调用 withDynamicPayload

示例 12.14:ConstraintValidator 实现设置动态负载
package org.hibernate.validator.referenceguide.chapter12.dynamicpayload;

import static org.hibernate.validator.internal.util.CollectionHelper.newHashMap;

public class ValidPassengerCountValidator implements ConstraintValidator<ValidPassengerCount, Car> {

    private static final Map<Integer, String> suggestedCars = newHashMap();

    static {
        suggestedCars.put( 2, "Chevrolet Corvette" );
        suggestedCars.put( 3, "Toyota Volta" );
        suggestedCars.put( 4, "Maserati GranCabrio" );
        suggestedCars.put( 5, " Mercedes-Benz E-Class" );
    }

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {
    }

    @Override
    public boolean isValid(Car car, ConstraintValidatorContext context) {
        if ( car == null ) {
            return true;
        }

        int passengerCount = car.getPassengers().size();
        if ( car.getSeatCount() >= passengerCount ) {
            return true;
        }
        else {

            if ( suggestedCars.containsKey( passengerCount ) ) {
                HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                        HibernateConstraintValidatorContext.class
                );
                hibernateContext.withDynamicPayload( suggestedCars.get( passengerCount ) );
            }
            return false;
        }
    }
}

在约束违规处理方面,可以将 jakarta.validation.ConstraintViolation 解包到 HibernateConstraintViolation 以便检索动态负载以供进一步处理。

示例 12.15:检索 ConstraintViolation 的动态负载
Car car = new Car( 2 );
car.addPassenger( new Person() );
car.addPassenger( new Person() );
car.addPassenger( new Person() );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation = constraintViolations.iterator().next();
@SuppressWarnings("unchecked")
HibernateConstraintViolation<Car> hibernateConstraintViolation = constraintViolation.unwrap(
        HibernateConstraintViolation.class
);
String suggestedCar = hibernateConstraintViolation.getDynamicPayload( String.class );
assertEquals( "Toyota Volta", suggestedCar );

12.9. 启用表达式语言功能

Hibernate Validator 限制了默认情况下公开的表达式语言功能。

为此,我们在 ExpressionLanguageFeatureLevel 中定义了几个功能级别

  • NONE:完全禁用表达式语言插值。

  • VARIABLES:允许插值通过addExpressionVariable()、资源捆绑包注入的变量,以及使用formatter对象。

  • BEAN_PROPERTIES:允许VARIABLES允许的所有内容,以及 bean 属性的插值。

  • BEAN_METHODS:还允许执行 Bean 方法。如果处理不当,这可能会导致严重的安全问题,包括任意代码执行。

根据上下文,我们公开的功能会有所不同

  • 对于约束,默认级别为 BEAN_PROPERTIES。对于所有内置约束消息的正确插值,您至少需要 VARIABLES 级别。

  • 对于通过 ConstraintValidatorContext 创建的自定义违规,表达式语言默认情况下处于禁用状态。您可以为特定自定义违规启用它,并且在启用时,它将默认使用 VARIABLES

Hibernate Validator 提供了在引导 ValidatorFactory 时覆盖这些默认值的方法。

要更改约束的表达式语言功能级别,请使用以下方法

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .constraintExpressionLanguageFeatureLevel( ExpressionLanguageFeatureLevel.VARIABLES )
        .buildValidatorFactory();

要更改自定义违规的表达式语言功能级别,请使用以下方法

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .customViolationExpressionLanguageFeatureLevel( ExpressionLanguageFeatureLevel.VARIABLES )
        .buildValidatorFactory();

这样做将自动为应用程序中的所有自定义违规启用表达式语言。

它只能用于兼容性,并简化从旧的 Hibernate Validator 版本迁移。

这些级别也可以使用以下属性定义

  • hibernate.validator.constraint_expression_language_feature_level

  • hibernate.validator.custom_violation_expression_language_feature_level

这些属性的接受值为:nonevariablesbean-propertiesbean-methods

12.10. ParameterMessageInterpolator

默认情况下,Hibernate Validator 需要一个 Unified EL 的实现(见 第 1.1.1 节,“Unified EL”)。这是为了允许使用 EL 表达式插值约束错误消息,如 Jakarta Bean Validation 规范所定义。

对于无法或不想提供 EL 实现的环境,Hibernate Validator 提供了一个非 EL 基消息插值器 - org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator

请参阅 第 4.2 节,“自定义消息插值”,了解如何插入自定义消息插值器实现。

包含 EL 表达式的约束消息将由 org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator 返回未插值的。这也影响使用 EL 表达式的内置默认约束消息。目前,DecimalMinDecimalMax 会受到影响。

12.11. ResourceBundleLocator

通过 ResourceBundleLocator,Hibernate Validator 提供了一个额外的 SPI,它允许从除 ValidationMessages 之外的其他资源包中检索错误消息,同时仍然使用规范中定义的实际插值算法。请参阅 第 4.2.1 节,“ResourceBundleLocator,了解如何使用该 SPI。

12.12. 自定义语言环境解析

这些契约被标记为 @Incubating,因此它们将来可能会发生变化。

Hibernate Validator 提供了几个扩展点来构建自定义语言环境解析策略。在插值约束违规消息时使用解析的语言环境。

Hibernate Validator 的默认行为是始终使用系统默认语言环境(通过 Locale.getDefault() 获取)。如果您通常将系统语言环境设置为 en-US 但希望应用程序提供法语消息,这可能不是理想的行为。

以下示例显示了如何将 Hibernate Validator 默认语言环境设置为 fr-FR

示例 12.16:配置默认语言环境
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .defaultLocale( Locale.FRANCE )
        .buildValidatorFactory()
        .getValidator();

Set<ConstraintViolation<Bean>> violations = validator.validate( new Bean() );
assertEquals( "doit avoir la valeur vrai", violations.iterator().next().getMessage() );

虽然这已经是一个不错的改进,但在完全国际化的应用程序中,这还不够:您需要 Hibernate Validator 根据用户上下文选择语言环境。

Hibernate Validator 提供了 org.hibernate.validator.spi.messageinterpolation.LocaleResolver SPI,它允许微调语言环境的解析。通常,在 JAX-RS 环境中,您可以从 Accept-Language HTTP 标头中解析要使用的语言环境。

在以下示例中,我们使用硬编码值,但例如,在 RESTEasy 应用程序的情况下,您可以从 ResteasyContext 中提取标头。

示例 12.17:通过 LocaleResolver 微调用于插值消息的语言环境
LocaleResolver localeResolver = new LocaleResolver() {

    @Override
    public Locale resolve(LocaleResolverContext context) {
        // get the locales supported by the client from the Accept-Language header
        String acceptLanguageHeader = "it-IT;q=0.9,en-US;q=0.7";

        List<LanguageRange> acceptedLanguages = LanguageRange.parse( acceptLanguageHeader );
        List<Locale> resolvedLocales = Locale.filter( acceptedLanguages, context.getSupportedLocales() );

        if ( resolvedLocales.size() > 0 ) {
            return resolvedLocales.get( 0 );
        }

        return context.getDefaultLocale();
    }
};

Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .defaultLocale( Locale.FRANCE )
        .locales( Locale.FRANCE, Locale.ITALY, Locale.US )
        .localeResolver( localeResolver )
        .buildValidatorFactory()
        .getValidator();

Set<ConstraintViolation<Bean>> violations = validator.validate( new Bean() );
assertEquals( "deve essere true", violations.iterator().next().getMessage() );

使用 LocaleResolver 时,您必须通过 locales() 方法定义支持的语言环境列表。

12.13. 自定义上下文

Jakarta Bean Validation 规范在其 API 中的几个点提供了将给定接口解包到特定于实现者的子类型的可能性。在 ConstraintValidator 实现中的约束违规创建以及 MessageInterpolator 实例中的消息插值的情况下,存在用于提供的上下文实例 - ConstraintValidatorContextMessageInterpolatorContext - 的 unwrap() 方法。Hibernate Validator 为这两个接口提供了自定义扩展。

12.13.1. HibernateConstraintValidatorContext

HibernateConstraintValidatorContextConstraintValidatorContext 的子类型,它允许您

  • 为特定自定义违规启用表达式语言插值 - 见下文

  • 使用 HibernateConstraintValidatorContext#addExpressionVariable(String, Object)HibernateConstraintValidatorContext#addMessageParameter(String, Object) 设置用于通过表达式语言消息插值工具插值的任意参数。

    示例 155. 自定义 @Future 验证器注入表达式变量
    package org.hibernate.validator.referenceguide.chapter12.context;
    
    public class MyFutureValidator implements ConstraintValidator<Future, Instant> {
    
        @Override
        public void initialize(Future constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(Instant value, ConstraintValidatorContext context) {
            if ( value == null ) {
                return true;
            }
    
            HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                    HibernateConstraintValidatorContext.class
            );
    
            Instant now = Instant.now( context.getClockProvider().getClock() );
    
            if ( !value.isAfter( now ) ) {
                hibernateContext.disableDefaultConstraintViolation();
                hibernateContext
                        .addExpressionVariable( "now", now )
                        .buildConstraintViolationWithTemplate( "Must be after ${now}" )
                        .addConstraintViolation();
    
                return false;
            }
    
            return true;
        }
    }
    示例 156. 自定义 @Future 验证器注入消息参数
    package org.hibernate.validator.referenceguide.chapter12.context;
    
    public class MyFutureValidatorMessageParameter implements ConstraintValidator<Future, Instant> {
    
        @Override
        public void initialize(Future constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(Instant value, ConstraintValidatorContext context) {
            if ( value == null ) {
                return true;
            }
    
            HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                    HibernateConstraintValidatorContext.class
            );
    
            Instant now = Instant.now( context.getClockProvider().getClock() );
    
            if ( !value.isAfter( now ) ) {
                hibernateContext.disableDefaultConstraintViolation();
                hibernateContext
                        .addMessageParameter( "now", now )
                        .buildConstraintViolationWithTemplate( "Must be after {now}" )
                        .addConstraintViolation();
    
                return false;
            }
    
            return true;
        }
    }

    除了语法之外,消息参数和表达式变量之间的主要区别在于,消息参数只是插值,而表达式变量是使用表达式语言引擎解释的。在实践中,如果您不需要表达式语言的高级功能,请使用消息参数。

    请注意,通过 addExpressionVariable(String, Object)addMessageParameter(String, Object) 指定的参数是全局的,并应用于此 isValid() 调用创建的所有约束违规。这包括默认约束违规,但也包括 ConstraintViolationBuilder 创建的所有违规。但是,您可以在调用 ConstraintViolationBuilder#addConstraintViolation() 之间更新参数。

  • 设置任意动态负载 - 见 第 12.8 节,“ConstraintViolation 中的动态负载”

默认情况下,表达式语言插值为禁用,这是为了避免在消息模板从不正确转义的用户输入构建时发生任意代码执行或敏感数据泄露。

可以使用 enableExpressionLanguage() 为给定自定义违规启用表达式语言,如以下示例所示

public class SafeValidator implements ConstraintValidator<ZipCode, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }

        HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                HibernateConstraintValidatorContext.class );
        hibernateContext.disableDefaultConstraintViolation();

        if ( isInvalid( value ) ) {
            hibernateContext
                    .addExpressionVariable( "validatedValue", value )
                    .buildConstraintViolationWithTemplate( "${validatedValue} is not a valid ZIP code" )
                    .enableExpressionLanguage()
                    .addConstraintViolation();

            return false;
        }

        return true;
    }

    private boolean isInvalid(String value) {
        // ...
        return false;
    }
}

在这种情况下,消息模板将由表达式语言引擎插值。

默认情况下,启用表达式语言时只启用变量插值。

您可以使用 HibernateConstraintViolationBuilder#enableExpressionLanguage(ExpressionLanguageFeatureLevel level) 启用更多功能。

我们为表达式语言插值定义了几个功能级别

  • NONE:表达式语言插值完全禁用 - 这是自定义违规的默认值。

  • VARIABLES:允许插值通过addExpressionVariable()、资源捆绑包注入的变量,以及使用formatter对象。

  • BEAN_PROPERTIES:允许VARIABLES允许的所有内容,以及 bean 属性的插值。

  • BEAN_METHODS:还允许执行 Bean 方法。如果处理不当,这可能会导致严重的安全问题,包括任意代码执行。

使用 `addExpressionVariable()` 是将变量注入表达式中唯一安全的方法,如果您使用 `BEAN_PROPERTIES` 或 `BEAN_METHODS` 特性级别,则这一点尤其重要。

如果您通过简单地将用户输入与消息串联来注入用户输入,您将允许潜在的任意代码执行和敏感数据泄露:如果用户输入包含有效的表达式,它们将由表达式语言引擎执行。

以下是一个您绝对不应该做的事情的示例

public class UnsafeValidator implements ConstraintValidator<ZipCode, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }

        context.disableDefaultConstraintViolation();

        HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                HibernateConstraintValidatorContext.class );
        hibernateContext.disableDefaultConstraintViolation();

        if ( isInvalid( value ) ) {
            hibernateContext
                    // THIS IS UNSAFE, DO NOT COPY THIS EXAMPLE
                    .buildConstraintViolationWithTemplate( value + " is not a valid ZIP code" )
                    .enableExpressionLanguage()
                    .addConstraintViolation();

            return false;
        }

        return true;
    }

    private boolean isInvalid(String value) {
        // ...
        return false;
    }
}

在上面的示例中,如果 `value`(可能是用户输入)包含有效的表达式,它将由表达式语言引擎进行插值,这可能导致不安全的行为。

12.13.2. `HibernateMessageInterpolatorContext`

Hibernate Validator 还提供了一个 `MessageInterpolatorContext` 的自定义扩展,即 `HibernateMessageInterpolatorContext`(参见 示例 12.18,`HibernateMessageInterpolatorContext`)。此子类型是为了允许 Hibernate Validator 更好地集成到 Glassfish 中而引入的。在这种情况下,需要根 bean 类型来确定消息资源包的正确类加载器。如果您还有其他用例,请告诉我们。

示例 12.18:`HibernateMessageInterpolatorContext`
public interface HibernateMessageInterpolatorContext extends MessageInterpolator.Context {

    /**
     * Returns the currently validated root bean type.
     *
     * @return The currently validated root bean type.
     */
    Class<?> getRootBeanType();

    /**
     * @return the message parameters added to this context for interpolation
     *
     * @since 5.4.1
     */
    Map<String, Object> getMessageParameters();

    /**
     * @return the expression variables added to this context for EL interpolation
     *
     * @since 5.4.1
     */
    Map<String, Object> getExpressionVariables();

    /**
     * @return the path to the validated constraint starting from the root bean
     *
     * @since 6.1
     */
    Path getPropertyPath();

    /**
     * @return the level of features enabled for the Expression Language engine
     *
     * @since 6.2
     */
    ExpressionLanguageFeatureLevel getExpressionLanguageFeatureLevel();
}

12.14. 基于 Paranamer 的 `ParameterNameProvider`

Hibernate Validator 附带一个 `ParameterNameProvider` 实现,它利用了 Paranamer 库。

该库提供了多种方法来在运行时获取参数名称,例如基于 Java 编译器创建的调试符号、在编译后步骤中将参数名称常量编织到字节码中或使用 JSR 330 的 `@Named` 注释等。

为了使用 `ParanamerParameterNameProvider`,您可以在引导验证器时传递一个实例,如 示例 9.10,“使用自定义的 `ParameterNameProvider`” 中所示,或者在 META-INF/validation.xml 文件中将 `org.hibernate.validator.parameternameprovider.ParanamerParameterNameProvider` 指定为 `<parameter-name-provider>` 元素的值。

使用此参数名称提供程序时,您需要将 Paranamer 库添加到您的类路径中。它在 Maven Central 存储库中可用,组 ID 为 `com.thoughtworks.paranamer`,工件 ID 为 `paranamer`。

默认情况下,`ParanamerParameterNameProvider` 从构建时添加到字节码的常量中获取参数名称(通过 `DefaultParanamer`)和调试符号(通过 `BytecodeReadingParanamer`)。或者,您可以在创建 `ParanamerParameterNameProvider` 实例时指定您选择的 `Paranamer` 实现。

12.15. 提供约束定义

Jakarta Bean Validation 允许通过其约束映射文件中的 XML 来(重新)定义约束定义。有关更多信息,请参见 第 8.2 节,“通过 `constraint-mappings` 映射约束”,并参见 示例 8.2,“通过 XML 配置的 Bean 约束” 以获取示例。虽然这种方法对于许多用例来说已经足够了,但在其他情况下它也存在缺点。例如,想象一个约束库想要为自定义类型贡献约束定义。该库可以提供一个包含其库的映射文件,但该文件仍然需要由库的用户引用。幸运的是,还有更好的方法。

目前,以下概念被认为是实验性的。请告诉我们您是否发现它们有用,以及它们是否满足您的需求。

12.15.1. 通过 `ServiceLoader` 获取约束定义

Hibernate Validator 允许利用 Java 的 ServiceLoader 机制来注册额外的约束定义。您所要做的就是将文件 jakarta.validation.ConstraintValidator 添加到 META-INF/services 中。在此服务文件中,您列出约束验证器类的完全限定类名(每行一个)。Hibernate Validator 将自动推断它们适用的约束类型。有关示例,请参见 通过服务文件获取约束定义

示例 12.19:META-INF/services/jakarta.validation.ConstraintValidator
# Assuming a custom constraint annotation @org.mycompany.CheckCase
org.mycompany.CheckCaseValidator

要为您的自定义约束贡献默认消息,请将文件 ContributorValidationMessages.properties 及其特定于语言环境的专用化放在 JAR 的根目录下。除了 ValidationMessages.properties 中给出的那些之外,Hibernate Validator 还将考虑在类路径上找到的所有具有此名称的捆绑包中的条目。

此机制在创建大型多模块应用程序时也很有用:您可以为每个模块创建一个资源包,其中只包含该模块的消息,而不是将所有约束消息都放在一个单一的包中。

我们强烈建议阅读 Marko Bekhta 的这篇博文,它将逐步指导您完成创建包含自定义约束的独立 JAR 并通过 `ServiceLoader` 声明它们的步骤。

12.15.2. 以编程方式添加约束定义

虽然服务加载器方法适用于许多场景,但并不适用于所有场景(例如,考虑 OSGi,其中服务文件不可见),因此还有另一种方法可以贡献约束定义。您可以使用以编程方式声明约束的 API - 请参见 示例 12.20,“通过编程 API 添加约束定义”

示例 12.20:通过编程 API 添加约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
        .constraintDefinition( ValidPassengerCount.class )
        .validatedBy( ValidPassengerCountValidator.class );

如果您的验证器实现比较简单(即,不需要从注释进行初始化,并且不使用 `ConstraintValidatorContext`),您还可以使用此备用 API 来使用 Lambda 表达式或方法引用来指定约束逻辑

示例 12.21:使用 Lambda 表达式添加约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
        .constraintDefinition( ValidPassengerCount.class )
            .validateType( Bus.class )
                .with( b -> b.getSeatCount() >= b.getPassengers().size() );

您可以使用 `ConstraintMappingContributor` 代替直接将约束映射添加到配置对象中,如 第 12.5 节,“将以编程方式声明的约束应用于默认验证器工厂” 中所述。当使用 META-INF/validation.xml 配置默认验证器工厂时,这可能很有用(请参见 第 8 章,通过 XML 配置)。

通过编程 API 注册约束定义的一个用例是能够为 `@URL` 约束指定备用约束验证器。从历史上看,Hibernate Validator 用于此约束的默认约束验证器使用 `java.net.URL` 构造函数来验证 URL。但是,还有一个基于纯正则表达式的版本可用,它可以使用 `ConstraintDefinitionContributor` 进行配置

使用以编程方式声明约束的 API 为 `@URL` 注册基于正则表达式的约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
        .constraintDefinition( URL.class )
        .includeExistingValidators( false )
        .validatedBy( RegexpURLValidator.class );

12.16. 自定义类加载

Hibernate Validator 需要根据名称加载资源或类的情况有很多

  • XML 描述符(META-INF/validation.xml 以及 XML 约束映射)

  • 在 XML 描述符中按名称指定的类(例如,自定义消息插值器等)

  • ValidationMessages 资源包

  • 用于基于表达式的消息插值的 `ExpressionFactory` 实现

默认情况下,Hibernate Validator 会尝试通过当前线程上下文类加载器来加载这些资源。如果失败,则会尝试使用 Hibernate Validator 自己的类加载器作为后备。

对于此策略不适用(例如,模块化环境,如 OSGi)的情况,您可以在引导验证器工厂时为加载这些资源提供特定的类加载器

示例 12.22:提供用于加载外部资源和类的类加载器
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .externalClassLoader( classLoader )
        .buildValidatorFactory()
        .getValidator();

在 OSGi 的情况下,您可以例如传递引导 Hibernate Validator 的捆绑包中的类的加载器,或传递将委托给 `Bundle#loadClass()` 等的自定义类加载器实现。

如果不再需要给定的验证器工厂实例,请调用 `ValidatorFactory#close()`。如果这样做,则可能会导致类加载器泄漏,在这种情况下,应用程序/捆绑包将重新部署,而未关闭的验证器工厂仍然被应用程序代码引用。

12.17. 自定义 getter 属性选择策略

当 Hibernate Validator 验证 bean 时,它会验证其属性。属性可以是字段或 getter。默认情况下,Hibernate Validator 遵守 JavaBeans 规范,只要以下条件之一为真,就会将方法视为 getter

  • 方法名以 `get` 开头,它具有非 void 返回类型,并且没有参数;

  • 方法名以 `is` 开头,它具有 `boolean` 返回类型,并且没有参数;

  • 方法名以 `has` 开头,它具有 `boolean` 返回类型,并且没有参数(此规则特定于 Hibernate Validator,并且不是 JavaBeans 规范所强制的)

虽然这些规则在遵循经典 JavaBeans 约定时通常是合适的,但在某些情况下,尤其是使用代码生成器时,JavaBeans 命名约定可能不会被遵循,并且 getter 的名称可能遵循不同的约定。

在这种情况下,应该重新定义用于检测 getter 的策略,以便完全验证对象。

此要求的一个典型示例是,当类遵循流畅的命名约定时,如 示例 12.23,“使用非标准 getter 的类” 中所示。

示例 12.23:使用非标准 getter 的类
package org.hibernate.validator.referenceguide.chapter12.getterselectionstrategy;

public class User {

    private String firstName;
    private String lastName;
    private String email;

    // [...]

    @NotEmpty
    public String firstName() {
        return firstName;
    }

    @NotEmpty
    public String lastName() {
        return lastName;
    }

    @Email
    public String email() {
        return email;
    }
}

如果验证了这样的对象,则不会对 getter 执行任何验证,因为标准策略无法检测到它们。

示例 12.24:使用默认 getter 属性选择策略验证使用非标准 getter 的类
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .buildValidatorFactory()
        .getValidator();

User user = new User( "", "", "not an email" );

Set<ConstraintViolation<User>> constraintViolations = validator.validate( user );

// as User has non-standard getters no violations are triggered
assertEquals( 0, constraintViolations.size() );

要使 Hibernate Validator 将这些方法视为属性,应该配置自定义的 `GetterPropertySelectionStrategy`。在这种特殊情况下,该策略的可能实现将是

示例 12.25:自定义 `GetterPropertySelectionStrategy` 实现
package org.hibernate.validator.referenceguide.chapter12.getterselectionstrategy;

public class FluentGetterPropertySelectionStrategy implements GetterPropertySelectionStrategy {

    private final Set<String> methodNamesToIgnore;

    public FluentGetterPropertySelectionStrategy() {
        // we will ignore all the method names coming from Object
        this.methodNamesToIgnore = Arrays.stream( Object.class.getDeclaredMethods() )
                .map( Method::getName )
                .collect( Collectors.toSet() );
    }

    @Override
    public Optional<String> getProperty(ConstrainableExecutable executable) {
        if ( methodNamesToIgnore.contains( executable.getName() )
                || executable.getReturnType() == void.class
                || executable.getParameterTypes().length > 0 ) {
            return Optional.empty();
        }

        return Optional.of( executable.getName() );
    }

    @Override
    public List<String> getGetterMethodNameCandidates(String propertyName) {
        // As method name == property name, there always is just one possible name for a method
        return Collections.singletonList( propertyName );
    }
}

有几种方法可以配置 Hibernate Validator 以使用此策略。它可以以编程方式完成(请参见 示例 12.26,“以编程方式配置自定义 `GetterPropertySelectionStrategy`”)或使用 XML 配置中的 `hibernate.validator.getter_property_selection_strategy` 属性完成(请参见 示例 12.27,“使用 XML 属性配置自定义 `GetterPropertySelectionStrategy`”)。

示例 12.26:以编程方式配置自定义 `GetterPropertySelectionStrategy`
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        // Setting a custom getter property selection strategy
        .getterPropertySelectionStrategy( new FluentGetterPropertySelectionStrategy() )
        .buildValidatorFactory()
        .getValidator();

User user = new User( "", "", "not an email" );

Set<ConstraintViolation<User>> constraintViolations = validator.validate( user );

assertEquals( 3, constraintViolations.size() );
示例 12.27:使用 XML 属性配置自定义 `GetterPropertySelectionStrategy`
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
            https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">

    <property name="hibernate.validator.getter_property_selection_strategy">
        org.hibernate.validator.referenceguide.chapter12.getterselectionstrategy.NoPrefixGetterPropertySelectionStrategy
    </property>

</validation-config>

重要的是要注意,在使用 `HibernateValidatorConfiguration#addMapping(ConstraintMapping)` 以编程方式添加约束的情况下,应该始终在配置所需的 getter 属性选择策略之后添加映射。否则,将使用默认策略来处理在定义策略之前添加的映射。

12.18. 自定义约束违规的属性名称解析

假设我们有一个简单的數據類,它對某些字段有 `@NotNull` 约束

示例 12.28:Person 数据类
public class Person {
    @NotNull
    @JsonProperty("first_name")
    private final String firstName;

    @JsonProperty("last_name")
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

可以使用 Jackson 库将此类序列化为 JSON

示例 12.29:将 Person 对象序列化为 JSON
public class PersonSerializationTest {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void personIsSerialized() throws JsonProcessingException {
        Person person = new Person( "Clark", "Kent" );

        String serializedPerson = objectMapper.writeValueAsString( person );

        assertEquals( "{\"first_name\":\"Clark\",\"last_name\":\"Kent\"}", serializedPerson );
    }
}

正如我们所看到的,该对象被序列化为

示例 12.30:Person 作为 json
{
  "first_name": "Clark",
  "last_name": "Kent"
}

请注意属性名称的差异。在 Java 对象中,我们有 firstNamelastName,而在 JSON 输出中,我们有 first_namelast_name。我们通过 @JsonProperty 注解定制了这种行为。

现在假设我们在 REST 环境中使用这个类,用户可以在请求主体中发送 一个 Person 实例作为 JSON。当验证失败时,如果能指示验证失败的字段名称是 JSON 请求中使用的名称 first_name,而不是我们在 Java 代码中内部使用的名称 firstName,将会很方便。

org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider 接口允许我们做到这一点。通过实现它,我们可以定义在验证过程中如何解析属性的名称。在我们的例子中,我们想从 Jackson 配置中读取值。

一个实现方法是利用 Jackson API

示例 12.31:JacksonPropertyNodeNameProvider 实现
import org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty;
import org.hibernate.validator.spi.nodenameprovider.Property;
import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;

public class JacksonPropertyNodeNameProvider implements PropertyNodeNameProvider {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String getName(Property property) {
        if ( property instanceof JavaBeanProperty ) {
            return getJavaBeanPropertyName( (JavaBeanProperty) property );
        }

        return getDefaultName( property );
    }

    private String getJavaBeanPropertyName(JavaBeanProperty property) {
        JavaType type = objectMapper.constructType( property.getDeclaringClass() );
        BeanDescription desc = objectMapper.getSerializationConfig().introspect( type );

        return desc.findProperties()
                .stream()
                .filter( prop -> prop.getInternalName().equals( property.getName() ) )
                .map( BeanPropertyDefinition::getName )
                .findFirst()
                .orElse( property.getName() );
    }

    private String getDefaultName(Property property) {
        return property.getName();
    }
}

在执行验证时

示例 12.32:JacksonPropertyNodeNameProvider 用法
public class JacksonPropertyNodeNameProviderTest {
    @Test
    public void nameIsReadFromJacksonAnnotationOnField() {
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .propertyNodeNameProvider( new JacksonPropertyNodeNameProvider() )
                .buildValidatorFactory();

        Validator validator = validatorFactory.getValidator();

        Person clarkKent = new Person( null, "Kent" );

        Set<ConstraintViolation<Person>> violations = validator.validate( clarkKent );
        ConstraintViolation<Person> violation = violations.iterator().next();

        assertEquals( violation.getPropertyPath().toString(), "first_name" );
    }

我们可以看到,属性路径现在返回 first_name

请注意,这在注解位于 getter 方法上时也有效

示例 12.33:注解位于 getter 方法上
@Test
public void nameIsReadFromJacksonAnnotationOnGetter() {
    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
            .configure()
            .propertyNodeNameProvider( new JacksonPropertyNodeNameProvider() )
            .buildValidatorFactory();

    Validator validator = validatorFactory.getValidator();

    Person clarkKent = new Person( null, "Kent" );

    Set<ConstraintViolation<Person>> violations = validator.validate( clarkKent );
    ConstraintViolation<Person> violation = violations.iterator().next();

    assertEquals( violation.getPropertyPath().toString(), "first_name" );
}

public class Person {
    private final String firstName;

    @JsonProperty("last_name")
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @NotNull
    @JsonProperty("first_name")
    public String getFirstName() {
        return firstName;
    }
}

这只是我们想要更改属性名称解析方式的一个用例。

org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider 可以被实现,以任何你认为合适的方式提供属性名称(例如从注解中读取)。

还有两个值得一提的接口

  • org.hibernate.validator.spi.nodenameprovider.Property 是一个基本接口,它包含有关属性的元数据。它有一个 String getName() 方法,可以用来获取属性的“原始”名称。此接口应该被用作解析名称的默认方式(参见 示例 12.31,“JacksonPropertyNodeNameProvider 实现” 中的用法)。

  • org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty 是一个接口,它包含有关 Bean 属性的元数据。它扩展了 org.hibernate.validator.spi.nodenameprovider.Property 并提供一些额外的的方法,例如 Class<?> getDeclaringClass(),它返回拥有该属性的类。

13. 注解处理器

你是否曾经无意中做了以下事情?

  • 在不支持的数据类型上指定约束注解(例如,用 @Past 注解一个 String)。

  • 注解 JavaBeans 属性的 setter 方法(而不是 getter 方法)。

  • 用约束注解注解静态字段或方法(这是不支持的)。

那么 Hibernate Validator 注解处理器就是你的不二之选。它通过插入构建过程并在约束注解使用不当时引发编译错误,来帮助你避免这些错误。

你可以在 Sourceforge 上的发布包中或 Maven 中央仓库等常见 Maven 仓库中找到 Hibernate Validator 注解处理器,其 GAV 为 org.hibernate.validator:hibernate-validator-annotation-processor:8.0.1.Final

13.1. 先决条件

Hibernate Validator 注解处理器基于 Java 平台中定义的 JSR 269 所定义的“可插拔注解处理 API”。

13.2. 特性

从 Hibernate Validator 8.0.1.Final 开始,Hibernate Validator 注解处理器检查以下内容:

  • 约束注解是否允许用于所注解元素的类型。

  • 只有非静态字段或方法才被约束注解注解。

  • 只有非基本类型字段或方法被 @Valid 注解。

  • 只有那些被约束注解注解的方法才是有效的 JavaBeans getter 方法(可选,见下文)。

  • 只有那些被约束注解注解的注解类型本身才是约束注解。

  • @GroupSequenceProvider 定义的动态默认组序列是否有效。

  • 注解参数值是否有意义且有效。

  • 继承层次结构中的方法参数约束是否符合继承规则。

  • 继承层次结构中的方法返回值约束是否符合继承规则。

13.3. 选项

可以使用以下 处理器选项 控制 Hibernate Validator 注解处理器的行为。

diagnosticKind

控制如何报告约束问题。必须是枚举 javax.tools.Diagnostic.Kind 中的值的字符串表示形式,例如 WARNING。如果值为 ERROR,则只要 AP 检测到约束问题,编译就会停止。默认值为 ERROR

methodConstraintsSupported

控制是否允许在任何类型的方法上使用约束。在使用 Hibernate Validator 支持的方法级约束时,必须设置为 true。可以设置为 false,以只允许在 Jakarta Bean Validation API 定义的 JavaBeans getter 方法上使用约束。默认值为 true

verbose

控制是否显示详细的处理信息,这对于调试很有用。必须为 truefalse。默认值为 false

13.4. 使用注解处理器

本节将详细介绍如何将 Hibernate Validator 注解处理器集成到命令行构建(Maven、Ant、javac)和基于 IDE 的构建(Eclipse、IntelliJ IDEA、NetBeans)中。

13.4.1. 命令行构建

13.4.1.1. Maven

要使用 Maven 将 Hibernate Validator 注解处理器,请通过 annotationProcessorPaths 选项进行设置,如下所示:

示例 13.1:在 Maven 中使用 HV 注解处理器
<project>
    [...]
    <build>
        [...]
        <plugins>
            [...]
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.hibernate.validator</groupId>
                            <artifactId>hibernate-validator-annotation-processor</artifactId>
                            <version>8.0.1.Final</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            [...]
        </plugins>
        [...]
    </build>
    [...]
</project>
13.4.1.2. Gradle

在使用 Gradle 时,只需将注解处理器作为 annotationProcessor 依赖项引用即可。

示例 13.2:在 Gradle 中使用注解处理器
dependencies {
    annotationProcessor group: 'org.hibernate.validator', name: 'hibernate-validator-annotation-processor', version: '8.0.1.Final'

    // any other dependencies ...
}
13.4.1.3. Apache Ant

与直接使用 javac 类似,在为 Apache Ant 调用 javac 任务 时,可以将注解处理器添加为编译器参数。

示例 13.3:在 Ant 中使用注解处理器
<javac srcdir="src/main"
       destdir="build/classes"
       classpath="/path/to/validation-api-3.0.2.jar">
       <compilerarg value="-processorpath" />
       <compilerarg value="/path/to/hibernate-validator-annotation-processor-8.0.1.Final.jar"/>
</javac>
13.4.1.4. javac

在使用命令行使用 javac 编译时,使用“processorpath”选项指定 JAR 文件 hibernate-validator-annotation-processor-8.0.1.Final.jar,如以下列表所示。处理器将被编译器自动检测并会在编译期间被调用。

示例 13.4:在 javac 中使用注解处理器
javac src/main/java/org/hibernate/validator/ap/demo/Car.java \
   -cp /path/to/validation-api-3.0.2.jar \
   -processorpath /path/to/hibernate-validator-annotation-processor-8.0.1.Final.jar

13.4.2. IDE 构建

13.4.2.1. Eclipse

只要你安装了 M2E Eclipse 插件,注解处理器就会自动为上面描述的 Maven 项目进行设置。

对于普通的 Eclipse 项目,请按照以下步骤设置注解处理器:

  • 右键单击你的项目,选择“属性”。

  • 转到“Java 编译器”,并确保“编译器合规性级别”设置为“1.8”。否则处理器将不会被激活。

  • 转到“Java 编译器 - 注解处理”,并选择“启用注解处理”。

  • 转到“Java 编译器 - 注解处理 - 工厂路径”,并添加 JAR 文件 hibernate-validator-annotation-processor-8.0.1.Final.jar。

  • 确认工作区重建。

现在你应该在编辑器中和“问题”视图中看到任何注解问题,它们将作为常规的错误标记显示。

annotation processor eclipse
13.4.2.2. IntelliJ IDEA

要使用 IntelliJ IDEA(版本 9 及更高版本)中的注解处理器,必须按照以下步骤操作:

  • 转到“文件”,然后是“设置”。

  • 展开节点“编译器”,然后是“注解处理器”。

  • 选择“启用注解处理”,并在“处理器路径”中输入以下内容:/path/to/hibernate-validator-annotation-processor-8.0.1.Final.jar

  • 将处理器的完全限定名称 org.hibernate.validator.ap.ConstraintValidationProcessor 添加到“注解处理器”列表中。

  • 如果适用,将你的模块添加到“已处理模块”列表中。

重建项目后,应该会显示任何错误的约束注解。

annotation processor intellij
13.4.2.3. NetBeans

NetBeans IDE 支持在 IDE 构建中使用注解处理器。为此,请执行以下操作:

  • 右键单击你的项目,选择“属性”。

  • 转到“库”选项卡“处理器”,并添加 JAR 文件 hibernate-validator-annotation-processor-8.0.1.Final.jar。

  • 转到“构建 - 编译”,选择“启用注解处理”和“在编辑器中启用注解处理”。通过指定其完全限定名称 org.hibernate.validator.ap.ConstraintValidationProcessor 来添加注解处理器。

任何约束注解问题都会直接在编辑器中标记出来。

annotation processor netbeans

13.5. 已知问题

截至 2017 年 7 月,存在以下已知问题:

  • 目前不支持容器元素约束。

  • 应用于容器但实际上应用于容器元素(无论是通过 Unwrapping.Unwrap 负载还是通过标记有 @UnwrapByDefault 的值提取器)的约束不支持。

  • HV-308:使用 XML 注册 到约束的额外验证器不会被注解处理器评估。

  • 有时在 Eclipse 中使用处理器时,自定义约束无法 正确评估。在这种情况下,清理项目会有所帮助。这似乎是 Eclipse JSR 269 API 实现中的一个问题,但需要进一步调查。

  • 在 Eclipse 中使用处理器时,动态默认组序列定义的检查不起作用。经过进一步调查,这似乎是 Eclipse JSR 269 API 实现中的一个问题。

14. 进一步阅读

最后,这里有一些指向更多信息的链接。

一个很好的示例来源是 Jakarta Bean Validation TCK,它可以在 GitHub 上匿名访问。尤其是 TCK 的 测试 可能很有趣。 Jakarta Bean Validation 规范本身也是加深你对 Jakarta Bean Validation 和 Hibernate Validator 理解的好方法。

如果你对 Hibernate Validator 有任何其他问题,或者想分享你的用例,请查看 Hibernate Validator WikiHibernate Validator 论坛Stack Overflow 上的 Hibernate Validator 标签

如果你想报告错误,请使用 Hibernate 的 Jira 实例。欢迎反馈!