前言

数据校验是一项常见任务,从表示层到持久层的所有应用都会用到。通常,在每个层中都各自实现其验证逻辑,这既耗时又容易出错。为了避免重复进行校验操作,开发人员通常将校验逻辑直接捆绑到域模型中,从而使域类与校验相关代码杂乱无章,而校验相关代码实际上是有关类本身的元数据。

application layers

Jakarta Bean Validation 2.0-定义了用于实体和方法校验的元数据模型和API。默认的元数据源是注解,也能够通过使用XML方式来覆盖或扩展元数据。该API不受特定应用程序层或编程模型的束缚。它不与Web层或持久层绑定,并且可被用于服务器端应用程序编程以及客户端Swing应用程序开发。

application layers2

Hibernate Validator是Jakarta Bean Validation的参考实现。该实现本身以及Jakarta Bean验证API和TCK均在 Apache Software License 2.0 下提供和分发。

Hibernate Validator 6 和 Jakarta Bean Validation 2.0 均需要Java 8及以上版本。

1. 快速开始

本章将向您展示如何开始使用Hibernate Validator,它是Jakarta Bean Validation的参考实现(reference implementation RI)。您需要准备:

  • JDK 8

  • Apache Maven

  • 互联网 (Maven需要下载相关依赖库)

1.1. 创建项目

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

Example 1. Hibernate Validator Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.1.Final</version>
</dependency>

这样也会同时引入依赖 Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.0)。

1.1.1. 统一的EL表达式

Hibernate Validator 需要一个 Jakarta Expression Language 的实现,能够使用表达式动态计算校验结果 ( Section 4.1, “默认消息插值”)。当您的应用程序在 Java EE 容器(如 JOSS AS )中运行时,相关容器已经提供了EL实现。 但是,在Java SE环境中,您必须将一个实现作为依赖项添加到您的POM文件。例如,您可以添加以下依赖项来使用 Jakarta EL 参考实现:

Example 2. Maven引入EL的实现
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>4.0.0</version>
</dependency>

对于那些不能提供EL的环境,Hibernate Validator 提供了一个Section 12.10, “ParameterMessageInterpolator。但是,使用这个插值器不符合 Jakarta Bean Validation规范。

1.1.2. CDI

Jakarta Bean Validation 定义了与 CDI ( Contexts and Dependency Injection for Jakarta EE)的集成点。 如果你的应用运行在一个没有提供这种集成的环境中,你可以使用 Hibernate Validator CDI 可移植扩展,方法是在你的 POM 中添加下面的 Maven 依赖项:

Example 3. Hibernate Validator CDI 可移植扩展 Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-cdi</artifactId>
    <version>7.0.1.Final</version>
</dependency>

注意,在 Java EE 应用程序服务器上运行的应用程序通常不需要添加此依赖项。您可以在Section 11.3, “CDI”中了解关于 Jakarta Bean Validation和 CDI 集成的更多信息。

1.1.3. 与安全管理器一起运行

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

Example 4. 使用 Hibernate Validator 和安全管理器的策略文件
grant codeBase "file:path/to/hibernate-validator-7.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.0.jar" {
    permission java.io.FilePermission "path/to/hibernate-validator-7.0.1.Final.jar", "read";
};

grant codeBase "file:path/to/jboss-logging-3.4.1.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 校验器

WildFly 应用程序服务器包含开箱即用的 Hibernate Validator。为了将 Jakarta Bean Validation API 和 Hibernate Validator 服务模块更新为最新和最稳定的模块,可以使用 WildFly 的补丁机制。

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

Example 5. WildFly 22.0.0.Final 补丁文件的 Maven 依赖项
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-modules</artifactId>
    <version>7.0.1.Final</version>
    <classifier>wildfly-22.0.0.Final-patch</classifier>
    <type>zip</type>
</dependency>

我们还为 WildFly 提供了一个补丁:

Example 6. Maven 对 WildFly 补丁文件的依赖关系
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-modules</artifactId>
    <version>7.0.1.Final</version>
    <classifier>wildfly-patch</classifier>
    <type>zip</type>
</dependency>

下载了补丁文件后,你可以通过运行以下命令将它应用到 WildFly:

Example 7. 应用 WildFly 补丁
$JBOSS_HOME/bin/jboss-cli.sh patch apply hibernate-validator-modules-7.0.1.Final-wildfly-22.0.0.Final-patch.zip

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

Example 8. 回滚 WildFly 补丁
$JBOSS_HOME/bin/jboss-cli.sh patch rollback --reset-configuration=true

您可以在 这里这里了解有关 WildFly 补丁的基础信息。

1.1.5. 在 Java 9上运行

从 Hibernate Validator 7.0.1.Final 开始便支持 Java 9。但是对 Java 9的模块化特性(JPMS)的支持是实验性的。目前还没有提供 JPMS 模块描述符,但 Hibernate Validator 目前可以用作自动模块。

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

  • Jakarta Bean Validation API: java.validation

  • Hibernate Validator core: org.hibernate.validator

  • Hibernate Validator CDI extension: org.hibernate.validator.cdi

  • Hibernate Validator test utilities: org.hibernate.validator.testutils

  • Hibernate Validator annotation processor: org.hibernate.validator.annotationprocessor

这些模块名称是暂时的,在将来提供真正的模块描述符的版本中可能会改变。

1.2. 使用约束

让我们直接深入到一个示例中,看看如何使用约束注解。

Example 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 的单元测试:

Example 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 实例是线程安全的,可以多次重用。因此,可以将 Validator 做为一个静态变量安全地存储,并用于测试方法,以校验不同的 Car 实例。

validate() 方法返回 ConstraintViolation 对象的 Set 集合, 您可以对其进行迭代操作,以查看发生了哪些校验错误。前三个测试方法显示了一些预期的违反约束的情况:

  • manufacturerIsNull() 方法中,违反了 manufacturer 字段的 @NotNull 约束

  • licensePlateTooShort() 方法中,违反了 licensePlate 字段的 @Size 约束

  • seatCountTooLow() 方法中,违反了 seatCount 字段的 @Min 约束

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

注意,只使用 Bean Validation API中 jakarta.validation 里面的类。没有直接引用来自 Hibernate Validator 的类,可以增强代码的可移植性。

1.4. 接下来的是?

以上是对 Hibernate Validator 和 Jakarta Bean Validation 的5分钟快速入门。继续探索代码示例或者进一步阅读 Chapter 14, 进一步阅读

要了解关于 bean 和属性校验的更多信息,请继续阅读 Chapter 2, 声明和校验 bean 约束。如果您对使用 Jakarta Bean Validation 来校验方法前置和后置条件感兴趣,请参阅Chapter 3, 声明和校验 method 约束 。如果您的应用程序有特定的校验要求,请参阅Chapter 6, 自定义约束

2. 声明和校验 bean 约束

在本章中,您将学习如何声明 (参见 Section 2.1, “声明 bean 约束”) 以及 校验 (参见 Section 2.2, “校验 bean 约束”) bean 约束。 Section 2.3, “内置约束” 概述了 Hibernate Validator提供的所有内置约束。

如果您对方法参数和返回值的约束校验感兴趣,请参阅 Chapter 3, 声明和校验 method 约束

2.1. 声明 bean 约束

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

  • 字段约束

  • 属性约束

  • 集合元素约束

  • 类约束

并非所有的约束都可以放在所有这些级别上。事实上,Jakarta Bean Validation 定义的约束都不能放在类级别上。约束注解的元注解 java.lang.annotation.Target 表明该注解可以在哪些类型上使用。 请参见 Chapter 6, 自定义约束 获取更多信息。

2.1.1. 字段级约束

约束可以通过在字段上添加注解来表示。 Example 11, “字段级约束” 显示了一个字段级别的配置例子:

Example 11. 字段级约束
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...
}

当使用字段级约束时,就是使用字段的值作为校验的对象。这意味着校验引擎将直接访问实例中的变量,而不是通过调用属性访问器方法,即使存在属性访问器(Getter方法)。

字段约束可以应用于任何访问类型(公共、私有等)的字段。但是,不支持对静态字段的约束。

当校验字节码增强对象时,应该使用属性级约束,因为字节码增强库不能通过反射确定要访问的字段。

2.1.2. 属性级约束

如果您的类遵循 JavaBeans 标准, 那么也可以对 bean 的属性而不是字段进行注解。 Example 12, “属性级约束” 使用与 Example 11, “字段级约束” 相同的实体,但是使用了属性级别的约束。

Example 12. 属性级约束
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 的实现 (例如: Lists, Sets),

  • java.util.Map 的实现, 支持keys和values方法,

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

  • JavaFX 中的 javafx.beans.observable.ObservableValue 各类实现

它还支持自定义集合类型上使用集合元素约束(参见 Chapter 7, 值提取 )。

在6之前的版本中,支持集合元素约束的子集。但在集合级别需要 @valid 注解来启用它们。从 Hibernate Validator 6开始就不再需要这个了。

我们将在下面提供两个示例,展示在不同 Java 集合类型上使用集合元素约束。

在这些示例中, @ValidPart 是设置了 TYPE_USE 的自定义约束。

2.1.3.1. Iterable

在对 Iterable 类型参数应用约束时,Hibernate Validator 将校验其中的每个元素。 Example 13, “集合元素约束 Set 展示了对有一个元素的 Set 进行约束的示例。

Example 13. 集合元素约束 Set
package org.hibernate.validator.referenceguide.chapter02.containerelement.set;

import java.util.HashSet;
import java.util.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 将校验每个元素。 Example 14, “集合元素约束 List 展示了对有一个元素的 List 进行约束的示例。

Example 14. 集合元素约束 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

集合元素约束也可以在map类型的键和值上进行校验。 Example 15, “映射键和值上的集合元素约束” 展示了一个 Map 的例子,对 key 和 value都进行校验。

Example 15. 映射键和值上的集合元素约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.map;

import java.util.HashMap;
import java.util.Map;

import jakarta.validation.constraints.NotNull;

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

校验产生的错误信息:

  • 无效的元素 (在第二个示例中, key为 null)。

  • 在第一个示例中, <map value> 不符合约束, 在第二个示例中,<map key> 不符合约束。

  • 在第二个示例中,您可能已经注意到类型参数 <k> 的出现,稍后将详细讨论。

2.1.3.4. java.util.Optional

当对 Optional 参数应用约束时, Hibernate Validator 将自动打开类型并校验内部值。Example 16, “集合元素约束 Optional” 展示了一个对 Optional 进行约束的例子。

Example 16. 集合元素约束 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 ,以允许检索要校验的值 (参见 Chapter 7, 值提取 以获得关于如何实现自己的 ValueExtractor 以及如何注册它的更多信息)。

Example 17, “自定义集合类型上的集合元素约束” 展示了一个具有类型参数约束的自定义参数化类型的示例。

Example 17. 自定义集合类型上的集合元素约束
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. 嵌套集合元素

嵌套集合元素也支持约束。

当校验 Example 18, “嵌套集合元素的约束” 中的 Car 类, 集合中的 PartManufacturer 都会执行 @NotNull 的校验。

Example 18. 嵌套集合元素的约束
package org.hibernate.validator.referenceguide.chapter02.containerelement.nested;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.validation.constraints.NotNull;

public class Car {

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

	//...
}

2.1.4. 类层面的约束

最后但并非最不重要的一点是,还可以在类级别上放置约束。在这种情况下,校验的对象不是单个属性,而是完整的对象。如果校验依赖于对象的几个属性之间的相关性,则类级别约束非常有用。

Example 19, “类级别约束” 中的 Car 具有两个属性 seatCountpassengers 的约束,应确保乘客名单中的登记项不超过可用座位。为此,在类级别上添加 @ValidPassengerCount 约束。该约束的校验器可以访问完整的 Car 对象,允许比较座位和乘客的数量。

请参阅 Section 6.2, “类级别的约束” ,详细了解如何实现这个自定义约束。

Example 19. 类级别约束
package org.hibernate.validator.referenceguide.chapter02.classlevel;

@ValidPassengerCount
public class Car {

	private int seatCount;

	private List<Person> passengers;

	//...
}

2.1.5. 约束继承

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

Example 20. 约束继承
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 对表示对另一个对象的引用的字段或属性进行校验,如Example 21, “级联校验”

Example 21. 级联校验
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 注解,这将导致在校验父对象时校验每个包含的元素。

嵌套集合元素也支持级联校验。

Example 22. 集合级联校验
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;

	//...
}

当校验 Example 22, “集合级联校验” 中的 Car 类的实例时 , 以下情况 ConstraintViolation 将被创建:

  • 如果 passengers 链表中的任何 Person 对象的name字段为 null ;

  • 如果 map keys 中的 Part 对象具有 null 的 name字段;

  • 如果 map values 中的 Manufacturer 对象具有 null 的 name字段;

在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() :

Example 23. Validation#buildDefaultValidatorFactory()
		ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
		validator = factory.getValidator();

这将使用默认配置启动校验程序。请参阅 Chapter 9, Bootstrapping 以了解更多关于不同的启动方法以及如何获得特定配置的 Validator 实例。

2.2.2. Validator 方法

Validator 接口定义了三个方法,可用于校验整个实体或仅校验实体的单个属性。

这三个方法都返回一个 Set<ConstraintViolation>。如果校验成功,集合是空的。否则,将为每个违反的约束添加一个 ConstraintViolation 实例。

所有的校验方法都有一个可变参数,可以用来指定在执行校验时应该考虑哪些校验组。如果未指定参数,则默认校验组( jakarta.validation.groups.Default) 。 分组将在 Chapter 5, 分组约束 中详细讨论 。

2.2.2.1. Validator#validate()

使用 validate() 方法对给定 bean 的所有约束执行校验。 Example 24, “使用 Validator#validate() 中的 Car 类来自 Example 12, “属性级约束” 该实例未能满足 manufacturer 字段上的 @NotNull 约束。因此,校验调用返回一个 ConstraintViolation 对象。

Example 24. 使用 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() 您可以校验给定对象的单个属性。

Example 25. 使用 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() 方法,您可以检查某值对给定类的单个属性是否可以成功校验:

Example 26. 使用 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中(参见 Section 11.2, “JSF & Seam”) ,以便对输入到表单中的值执行校验,然后将它们传播到模型中。

2.2.3. ConstraintViolation

2.2.3.1. ConstraintViolation 的方法

现在是时候仔细研究一下什么是 ConstraintViolation 了。使用 ConstraintViolation 中不同的方法可以获取校验失败原因的许多有用信息。以下是这些方法的概述。 列下的案例值引用了 Example 24, “使用 Validator#validate().

getMessage()

插值后产生的错误信息

示例

"must not be null"

getMessageTemplate()

未插值的模版信息

示例

"{…​ NotNull.message}"

getRootBean()

被校验的根bean

示例

car

getRootBeanClass()

被校验的根bean的类

示例

Car.class

getLeafBean()

如果是bean约束,则将获取到应用约束到bean实例;如果是属性约束,则将获取到托管该约束的属性的bean实例

示例

car

getPropertyPath()

根bean的校验值的属性路径

示例

约束的一个节点是 PROPERTY 类型,属性名为 "manufacturer"

getInvalidValue()

导致校验失败的值

示例

null

getConstraintDescriptor()

导致校验失败的约束的元信息

示例

@NotNull 约束的 descriptor

2.2.3.2. 利用属性路径

要确定触发冲突的原因,需要利用 getPropertyPath() 方法的返回值。

返回的 Path 由描述元素路径的 Nodes 组成

关于 Path 的结构和各种类型的 Nodes 的更多信息可以在 Jakarta Bean Validation 规范中的 ConstraintViolation 章节找到.

2.3. 内置约束

Hibernate Validator 包含一组常用的约束。这些都是 Jakarta Bean Validation 规范定义 (参见 Section 2.3.1, “Jakarta Bean Validation 约束”)。 此外,Hibernate Validator 还提供了在某些场景十分有用的自定义约束 (参见 Section 2.3.2, “附加约束”)。

2.3.1. Jakarta Bean Validation 约束

下面是 Jakarta Bean Validation API 中指定的所有约束的列表。 所有这些约束都适用于字段/属性级别,在 Jakarta Bean Validation规范中没有定义类级别约束。如果您使用 Hibernate ORM , 那么在为您的实体类创建 DDL 语句时会考虑到一些约束 (在下面的列表的 "Hibernate 元数据影响")。

Hibernate Validator 允许将一些约束应用于更多的数据类型,而不是 Jakarta Bean Validation 规范所要求的数据类型 (例如 @Max 可以应用于字符串)。但是,如果你在程序中依赖此特性,可能会影响应用程序在 Jakarta Bean Validation 提供程序之间的可移植性。

@AssertFalse

检查带注解的元素是否为false

支持的数据类型

Booleanboolean

Hibernate 元数据影响

None

@AssertTrue

检查带注解的元素是否为true

支持的数据类型

Booleanboolean

Hibernate 元数据影响

None

@DecimalMax(value=, inclusive=)

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

支持的数据类型

BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的基本类型包装器; Hibernate Validator还支持 JSR 354 API 中的 Numberjavax.money.MonetaryAmount 的实现。

Hibernate 元数据影响

None

@DecimalMin(value=, inclusive=)

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

支持的数据类型

BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的基本类型包装器; Hibernate Validator还支持 JSR 354 API 中的 Numberjavax.money.MonetaryAmount 的实现。

Hibernate 元数据影响

None

@Digits(integer=, fraction=)

检查被注解的值是否最多有 integer 位整数和 fraction 位小数

支持的数据类型

BigDecimal, BigIntegerCharSequencebyteshortintlong a以及各自的基本类型包装器; Hibernate Validator还支持 JSR 354 API 中的 Numberjavax.money.MonetaryAmount 的实现。

Hibernate 元数据影响

定义数据的精度和规模

@Email

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

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@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; Hibernate Validator还支持 Joda Time 中的 ReadablePartialReadableInstant 实现

Hibernate 元数据影响

None

@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; Hibernate Validator还支持 Joda Time 中的 ReadablePartialReadableInstant 实现

Hibernate 元数据影响

None

@Max(value=)

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

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的各个包装器; Hibernate Validator还支持 CharSequence 的子类 (字符序列表示的数值被求值),以及 Numberjavax.money.MonetaryAmount 的子类。

Hibernate 元数据影响

在列上添加检查约束

@Min(value=)

检查注解的值是否高于或等于指定的最小值

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 以及原始类型的各个包装器; Hibernate Validator还支持 CharSequence 的子类 (字符序列表示的数值被求值),以及 Numberjavax.money.MonetaryAmount 的子类。

Hibernate 元数据影响

在列上添加检查约束

@NotBlank

检查带注解的字符序列是否为空,去除首尾的空格后的长度是否大于0。 与 @NotEmpty 的不同之处在于,这个约束只能应用于字符序列,并且尾随的空格被忽略。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@NotEmpty

检查带注解的元素是否为 null 或 ""

支持的数据类型

CharSequenceCollectionMap and arrays

Hibernate 元数据影响

None

@NotNull

检查注解的值是否为 null

支持的数据类型

Any type

Hibernate 元数据影响

Column(s) are not nullable

@Negative

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

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 和各自的基本类型包装器;Hibernate Validator还支持 CharSequence 的子类 (字符序列表示的数值被求值),以及 Numberjavax.money.MonetaryAmount 的子类。

Hibernate 元数据影响

None

@NegativeOrZero

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

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 和各自的基本类型包装器;Hibernate Validator还支持 CharSequence 的子类 (字符序列表示的数值被求值),以及 Numberjavax.money.MonetaryAmount 的子类。

Hibernate 元数据影响

None

@Null

检查注解的值是否为 null

支持的数据类型

Any type

Hibernate 元数据影响

None

@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; Hibernate Validator还支持 Joda Time 中的 ReadablePartialReadableInstant 实现

Hibernate 元数据影响

None

@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; Hibernate Validator还支持 Joda Time 中的 ReadablePartialReadableInstant 实现

Hibernate 元数据影响

None

@Pattern(regex=, flags=)

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

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@Positive

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

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 和各自的基本类型包装器;Hibernate Validator还支持 CharSequence 的子类 (字符序列表示的数值被求值),以及 Numberjavax.money.MonetaryAmount 的子类。

Hibernate 元数据影响

None

@PositiveOrZero

检查元素是正数或者零。

支持的数据类型

BigDecimalBigIntegerbyteshortintlong 和各自的基本类型包装器;Hibernate Validator还支持 CharSequence 的子类 (字符序列表示的数值被求值),以及 Numberjavax.money.MonetaryAmount 的子类。

Hibernate 元数据影响

None

@Size(min=, max=)

检查带注解的元素的大小是否介于最小和最大(包括)之间

支持的数据类型

CharSequenceCollectionMap and arrays

Hibernate 元数据影响

Column length will be set to max

在上面列出的参数之上,每个约束都有参数消息、组和有效负载。这是 Jakarta Bean Validation 规范的一个要求。

2.3.2. 附加约束

除了 Jakarta Bean Validation API 定义的约束之外,Hibernate Validator 还提供了下面列出的几个有用的自定义约束。除了一个 @ScriptAssert 例外可以用于类级别的约束,这些约束只适用于字段/属性级别。

@CreditCardNumber(ignoreNonDigitCharacters=)

检查带注解的字符序列是否通过 Luhn 校验和测试。注意,此校验旨在检查用户错误,而不是信用卡的有效性!参见 Anatomy of a credit card number. ignoreNonDigitCharacters 允许忽略非数字字符。默认值为 false

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@Currency(value=)

检查带注解的 javax.money.MonetaryAmount 的货币单位是否是指定货币单位的一部分。

支持的数据类型

javax.money.MonetaryAmount JSR 354 API 的实现类

Hibernate 元数据影响

None

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

注解了 java.time.Duration 元素不大于由注解参数构造的元素。如果将 inclusive 标志设置为 true,则允许相等。

支持的数据类型

java.time.Duration

Hibernate 元数据影响

None

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

注解了 java.time.Duration 元素不小于由注解参数构造的元素。如果将 inclusive 标志设置为 true,则允许相等。

支持的数据类型

java.time.Duration

Hibernate 元数据影响

None

@EAN

检查带注解的字符序列是否为有效的 EAN 条形码。类型决定了条形码的类型。默认值是 EAN-13。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@ISBN

检查带注解的字符序列是否为有效的 ISBN 条形码。类型决定了条形码的类型。默认值是 ISBN-13。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@Length(min=, max=)

校验带注解的字符序列长度是否在 minmax 之间

支持的数据类型

CharSequence

Hibernate 元数据影响

Column length will be set to max

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

校验带注解的字符序列的代码点长度是否在最小值和最大值之间。如果设置了 normalizationStrategy ,则校验规范化值。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

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

检查带注解的字符序列中的数字是否通过 Luhn 校验和算法 (参见 Luhn algorithm). startIndexendIndex 只允许在指定的子字符串上运行算法。 checkDigitIndex 允许使用字符序列中的任意数字作为校验数字。如果未指定,则假定检查数字是指定范围的一部分。最后但并非最不重要的是,ignoreNonDigitCharacters 允许忽略非数字字符。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

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

检查带注解的字符序列中的数字是否通过了通用的 mod 10校验和算法。 multiplier 决定奇数的乘数(缺省为3) ,加权为偶数(缺省为1)。 startIndexendIndex 只允许在指定的子字符串上运行算法。 checkDigitIndex 允许使用字符序列中的任意数字作为校验数字。如果未指定,则假定检查数字是指定范围的一部分。最后但并非最不重要的是,ignoreNonDigitCharacters 允许忽略非数字字符。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

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

C检查带注解的字符序列中的数字是否通过 mod 11校验和算法。 threshold`指定 mod11乘数增长的阈值; 如果没有指定值,乘数将无限增长。 `treatCheck10AstreatCheck11As 分别指定当 mod 11校验和等于10或11时使用的校验数字。默认值分别为 x 和0。 startIndexendIndex checkDigitIndex and ignoreNonDigitCharacters 的语义与 `@Mod10Check`中的相同。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@Normalized(form=)

校验注解的字符序列是否根据给定的 form 进行了规范化。

支持的数据类型

CharSequence

Hibernate 元数据影响

None

@Range(min=, max=)

检查注解值是否位于指定的最小值和最大值之间

支持的数据类型

BigDecimalBigIntegerCharSequencebyteshortintlong 以及基元类型的相应包装器

Hibernate 元数据影响

None

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

检查是否可以根据带注解的元素成功地计算给定的脚本。为了使用这个约束,JSR 223(“ Java TM 平台的脚本编程”)定义的 Java 脚本 API 的实现必须是类路径的一部分。要求值的表达式可以使用任何脚本或表达式语言编写,在类路径中可以找到与 JSR 223兼容的引擎。即使这是一个类级别的约束,也可以使用 reportOn 属性报告特定属性(而不是整个对象)上的约束冲突。

支持的数据类型

Any type

Hibernate 元数据影响

None

@UniqueElements

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

支持的数据类型

Collection

Hibernate 元数据影响

None

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

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

支持的数据类型

CharSequence

Hibernate 元数据影响

None

2.3.2.1. 特殊国家约束

Hibernate Validator 还提供了一些国家特有的约束,例如用于校验社会安全号码。

如果您必须实现一个国家特定的约束,可以考虑将其作为 Hibernate Validator 的一个贡献!

@CNPJ

检查带注解的字符序列是否代表巴西公司纳税人注册号(Cadastro de Pessoa Jurídica)

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Brazil

@CPF

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

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Brazil

@TituloEleitoral

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

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Brazil

@NIP

检查带注解的字符序列是否表示波兰 VAT 标识号 (NIP)

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Poland

@PESEL

检查带注解的字符序列是否表示波兰国家标识号 (PESEL)

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Poland

@REGON

检查带注解的字符序列是否表示波兰纳税人识别号 (REGON). 。可同时应用于9位和14位数字版本的 REGON

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Poland

@INN

检查带注解的字符序列是否表示一个俄罗斯纳税人识别号 (INN). 可以适用于国际旅馆的个人和法律版本

支持的数据类型

CharSequence

Hibernate 元数据影响

None

Country

Russia

在某些情况下,Jakarta Bean Validation 约束和 Hibernate Validator提供的自定义约束都不能满足您的需求。在这种情况下,您可以很容易地编写自己的约束。你可以 Chapter 6, 自定义约束 中找到更多信息。

3. 声明和校验 method 约束

从 Bean Validation 1.1 开始,约束不仅应用于 JavaBeans 中的变量及其 Getter 方法 ,还可以应用于 Java 方法的返回值和构造函数的参数。同样也是使用 Jakarta Bean Validation 约束来指定。

  • 在调用方法或构造函数之前(对可执行方法的输入参数施加约束),可以保证调用者必须满足的前提条件

  • 在方法或构造函数调用返回后(通过将约束应用于可执行方法的返回值),可以保证调用方可以保证的后置条件

对于本参考指南,method constraint(方法约束) 既指方法约束,也指构造函数约束,如果没有另行说明的话。有时,在引用方法和构造函数时会使用术语 executable(可执行方法)

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

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

  • 可执行方法的输入、返回约束不必在其文档中重新表示,因为约束注解将自动包含在生成的 JavaDoc 中。这样可以避免冗余,并减少实现和文档之间不一致的可能性

为了使注解显示在被注解元素的 JavaDoc 中,注解类型本身必须使用元注解 @Documented 来注解。所有内置约束都标注了该注解,并且被认为是任何自定义约束的最佳实践。

在本章剩下的部分中,您将学习如何声明输入参数和返回值约束,以及如何使用 ExecutableValidator API 校验它们。

3.1. 声明 method 约束

3.1.1. 参数约束

您可以通过向方法或构造函数的输入参数添加约束注解来指定方法或构造函数的前置条件,如 Example 27, “声明方法和构造函数参数约束”

Example 27. 声明方法和构造函数参数约束
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, startDate 字段不能为 null 且为将来的时间, durationInDays 字段的最小值为1。

请注意,声明方法或构造函数约束本身并不会在调用该方法时自动导致它们的进行校验。相反,必须使用 ExecutableValidator API (参见 Section 3.2, “校验 method 约束”) 来执行校验,底层的实现方法通常使用方法拦截工具(如 AOP、代理对象等)来完成。

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

3.1.1.1. 交叉参数约束

有时,一次校验不仅依赖于单个参数,而是依赖于方法或构造函数的多个甚至全部参数。这种需求可以通过交叉参数约束来实现。

交叉参数约束可视为等价于类级约束的方法校验。两者都可用于实现基于几个元素的校验需求。不同之处在于类级别约束适用于 bean 的多个属性,但交叉参数约束适用于可执行方法的多个参数。

与单参数约束不同,交叉参数约束是在方法或构造函数上声明的,如 Example 28, “声明交叉参数约束” 。这里使用了 load() 方法中声明的交叉参数约束 @LuggageCountMatchesPassengerCount ,以确保旅客的行李不超过两件。

Example 28. 声明交叉参数约束
package org.hibernate.validator.referenceguide.chapter03.crossparameter;

public class Car {

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

正如您将在下一节中了解到的,返回值约束也是在方法级别声明的。为了区分交叉参数约束和返回值约束,在自定义约束注解的实现校验器 ConstraintValidator 上,标注 @SupportedValidationTarget 注解。您可以参阅 Section 6.3, “交叉参数约束” ,该节展示了如何实现自己的交叉参数约束。

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

这样的约束必须定义一个成员 validationAppliesTo() ,它可以在声明时用于指定约束目标。如Example 29, “指定约束的目标” 通过指定 validationAppliesTo = ConstraintTarget.PARAMETERS, 将约束应用于可执行方法的输入参数校验,而 ConstraintTarget.RETURN_VALUE 用于将约束应用于可执行方法的返回值校验。

Example 29. 指定约束的目标
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;
	}
}

尽管这种约束既适用于可执行方法的参数和返回值,但目标方法通常可以自动推断。如果出现下面几种情况,就可以确定作用对象

  • 带输入参数,但无返回值函数 (约束作用于输入参数上)

  • 无输入参数,但有返回值的函数 (约束作用于返回参数上)

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

在这些情况下,您不必指定约束目标。如果它增加了源代码的可读性,仍然建议这样做。如果在无法自动确定约束目标的情况下,也未指定该约束目标,则会引发 ConstraintDeclarationException 异常。

3.1.2. 返回值约束

方法或构造函数的返回参数的校验也可以通过添加约束注解来声明,如Example 30, “声明方法和构造函数返回值约束”

Example 30. 声明方法和构造函数返回值约束
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() 返回的List不能为 null ,并且必须至少包含元素( list.size() > 0 )

  • getCustomers() 返回的List包含的元素( Customer 对象 ) 不能为 null

正如您在上面的示例中看到的,方法返回值校验支持集合元素约束。同样的方法参数校验也支持它们。

3.1.3. 级联校验

类似于 JavaBeans 属性的级联校验 (参见 Section 2.1.6, “对象图”), @Valid 注解可用于标记可执行参数和级联校验的返回值。在校验带有 @Valid 注解的输入参数或返回值时,也会校验在输入参数或返回值对象中声明的约束。

Example 31, “级联校验标记输入参数和返回值” 中, Garage#checkCar() 方法的输入参数 car 和构造函数 Garage 一样都会进行级联校验。

Example 31. 级联校验标记输入参数和返回值
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 ,那么在被引用元素上声明的约束也将得到校验。

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

在这种情况下,将校验容器中包含的每个元素。在校验 Example 32, “标记为级联校验的方法参数的容器元素” 中的 checkCars() 方法的输入参数时, 将对列表里的每一个 Car 实例进行校验,并在校验失败的情况下创建 ConstraintViolation 对象。

Example 32. 标记为级联校验的方法参数的容器元素
package org.hibernate.validator.referenceguide.chapter03.cascaded.containerelement;

public class Garage {

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

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

在继承层次结构中声明方法约束时,注意以下规则非常重要:

  • 方法的调用方要满足的输入参数不能在子类的重写方法中加强

  • 保证给方法的调用方的输出参数不能在子类的重写方法中弱化

这些规则是由 behavioral subtyping 的概念驱动的,它要求在任何使用 T 类型的地方, 用 TsubType(子类型) S 替换 T 但是不会改变程序的行为。

举个例子,假设一个类调用一个静态方法返回 T ,如果该对象的运行时类型是 S ,而 S 加强了输入参数的校验,则客户端类可能无法满足这些前置条件,因为它们不知道这些前置条件。这一规则也被称为 Liskov substitution principle

Jakarta Bean Validation 规范实现了第一个规则,它禁止对重写或实现在超类型(超类或接口)中声明的方法进行参数约束。例 Example 33, “子类型中的非法方法参数约束”

Example 33. 子类型中的非法方法参数约束
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) {
		//...
	}
}

Car#drive() 上面的 @Max 约束是非法的,因为此方法实现了接口方法 Vehicle#drive() 。注意,如果超类的方法本身没有声明任何参数约束,也不允许重写方法上的参数约束。

此外,如果一个方法覆盖或实现了在几个并行超类型中声明的方法(例如,两个没有相互扩展的接口,或者一个类和一个没有被该类实现的接口) ,那么在任何涉及的类型中都不能为该方法指定参数约束。 Example 34, “层次结构的并行类型中的非法方法参数约束” 违反了该规则。 RacingCar#drive() 重写了 Vehicle#drive()Car#drive() 方法。 所以 RacingCar#drive() 是非法的。

Example 34. 层次结构的并行类型中的非法方法参数约束
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) {
		//...
	}
}

前面描述的限制规则只适用于输入参数约束。相反,返回值约束可以添加到重写或实现任何超类方法的方法上。

在这种情况下,所有方法的返回值约束都适用于子类型方法,即子类型方法本身上声明的约束,以及重写/实现的超类型方法上的任何返回值约束。这是合法的,因为增加额外的回报值约束可能永远不会削弱方法调用者得到的后置条件保证。

Example 35, “超类型和子类型方法的返回值约束” 中,校验 Car#getPassengers() 方法的返回值, @Size 方法也会在重写的 Vehicle#getPassengers() 的返回值被校验。

Example 35. 超类型和子类型方法的返回值约束
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 实例之前,可以通过设置 MethodValidationConfigurationHibernateValidatorConfiguration 属性中包含的配置参数来放松这些规则的实施。 参见 Section 12.3, “放宽类层次中方法校验的要求”.

3.2. 校验 method 约束

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

Section 3.2.1, “获取 ExecutableValidator 实例” 你将学会如何掌握一个 ExecutableValidator 实例。 在 Section 3.2.2, “ExecutableValidator 的方法” 展示了如何使用这个接口定义的不同方法。

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

3.2.1. 获取 ExecutableValidator 实例

可以通过 Validator#forExecutables() 检索 ExecutableValidator 实例,如 Example 36, “获取 ExecutableValidator 实例”

Example 36. 获取 ExecutableValidator 实例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();

在这个例子中,方法校验器是从默认校验器工厂(default validator factory)获取的,但是如果需要做相关配置的话,您可以阅读 Chapter 9, Bootstrapping 。例如,为了使用特定的参数名称提供器 (参见 Section 9.2.4, “ParameterNameProvider)。

3.2.2. ExecutableValidator 的方法

ExecutableValidator 接口提供了四种方法:

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

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

Validator 的方法一样,所有这些方法都返回一个 Set<ConstraintViolation> ,它包含每个违反的约束的 ConstraintViolation ,如果校验成功,该返回值将为空 Set 。此外,所有方法都有一个可变长 groups 参数,您可以通过该参数传递要进行校验的校验组。

以下部分中的示例基于 Example 37, “Car 使用约束方法和构造函数” 中的 Car 类。

Example 37. 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() 法用于校验方法调用的参数。在 Example 38, “使用 ExecutableValidator#validateParameters()drive() 方法违法了 @Max 约束。

Example 38. 使用 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() 可以校验方法的返回值。在 Example 39, “使用 ExecutableValidator#validateReturnValue() 产生一个约束冲突,因为 getPassengers() 方法需要至少返回一个 Passenger 实例。

Example 39. 使用 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() 校验构造函数调用的参数,如Example 40, “使用 ExecutableValidator#validateConstructorParameters(). 因为 manufacturer 字段的 @NotNull 约束,校验调用返回一个约束冲突。

Example 40. 使用 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() ,可以校验构造函数的返回值。 Example 41, “使用 ExecutableValidator#validateConstructorReturnValue(), validateConstructorReturnValue() 返回一个约束冲突,因为构造函数返回的 Car 实例不满足 @ValidRacingCar 约束。

Example 41. 使用 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

除了 Section 2.2.3, “ConstraintViolation 介绍的方法之外,ConstraintViolation 还提供了另外两个特定于校验可执行参数和返回值的方法。

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

ConstraintViolation 所有其他方法通常都以校验 bean 的方式一样。

注意 getPropertyPath() 对于获取有关校验参数或返回值的详细信息非常有用,例如用于日志记录。特别是,您可以从路径节点检索相关方法的名称和参数类型以及相关参数的索引。Example 42, “检索方法和参数信息” 中显示了如何做到这一点。

Example 42. 检索方法和参数信息
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 (参见 Section 9.2.4, “ParameterNameProvider).

3.3. 内置 method 约束

除了 Section 2.3, “内置约束” 中讨论的内置 bean 和属性级约束之外,Hibernate Validator 目前还提供了一个方法级约束 @ParameterScriptAssert 这是一个通用的交叉参数约束,它允许使用任何与 JSR 223兼容(“ Java TM 平台的脚本编写”)的脚本语言实现校验例程,前提是在类路径中可以使用这种语言的引擎。

若要从表达式内引用可执行方法的参数,请使用从激活的参数名称提供者获得的参数名称 (参见 Section 9.2.4, “ParameterNameProvider). Example 43, “使用 @ParameterScriptAssert 示了如何在 @LuggageCountMatchesPassengerCount 的帮助下表示 Example 28, “声明交叉参数约束”@ParameterScriptAssert 约束的校验逻辑。

Example 43. 使用 @ParameterScriptAssert
package org.hibernate.validator.referenceguide.chapter03.parameterscriptassert;

public class Car {

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

4. 添加约束错误消息

消息插值是违反 Jakarta Bean Validation 约束后创建错误消息的过程。在本章中,您将学习如何定义和解析这些消息,以及如何在默认算法不足以满足需求的情况下插入自定义消息插入器。

4.1. 默认消息插值

违反约束的消息是从所谓的消息descriptors(描述器)中检索的。每个约束都定义了它们自己其默认消息descriptors(描述器)。在声明时,默认descriptors(描述器)可以被特定的值覆盖,如 Example 44, “使用消息属性指定消息描述符”.

Example 44. 使用消息属性指定消息描述符
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 ...
}

如果违反了相关约束注解,将由validation引擎使用当前配置的 MessageInterpolator 进行消息的构建。然后通过调用 ConstraintViolation#getMessage() 方法可以获取结果。

消息描述符可以包含 message parameters(消息参数) 以及在插值期间将要解析的 message expressions(消息表达式) 。消息参数是封闭在 {} 中的字符串,而消息表达式是封闭在 ${} 中的字符串。在方法插值过程中应用了以下算法:

  1. message parameters(消息参数) 作为键值(key)在资源包 ValidationMessages 中检索value值。 如果这个资源包包含给定 message parameters(消息参数) ,那么该参数的value将替换在消息中的 message parameters(消息参数) 。如果替换的值再次包含消息参数,将递归地执行此步骤。资源包应该由应用程序开发人员提供,例如,通过将名为 ValidationMessages.properties 的文件添加到 classpath 中。您还可以通过提供此包的特定于国家的语言文件(如 ValidationMessages_en_US.properties )来创建本地化的错误消息。默认情况下,在包中查找消息时将使用 JVM 的默认语言环境( Locale#getDefault() )。

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

  3. 通过将任何消息参数替换为具有相同名称的约束注解成员的值来解析它们。这允许在错误消息 (例如"must be at least ${ min }") 中引用约束的属性值(例如 Size#min())。

  4. 通过将任何消息表达式计算为统一表达式语言表达式来解析它们。请参阅Section 4.1.2, “带有消息表达式的插值” 了解有关在错误消息中使用统一 EL 的更多信息。

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

4.1.1. 特殊字符

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

  • \{ 代表字符 {

  • \} 代表字符 }

  • \$ 代表字符 $

  • \\ 代表字符 \

4.1.2. 带有消息表达式的插值

从 Hibernate Validator 5 (Bean Validation 1.1) 开始,可以在违反约束的消息中使用 Jakarta Expression Language 。这允许基于条件逻辑定义错误消息,还支持高级格式化输出。验证引擎使以下对象在 EL 上下文中可用:

  • 通过约束的属性名称映射到配置的值

  • validatedValue 表式的当前验证的值(属性、 bean、方法参数等)

  • 也可以用 format(String format, Object…​ args) 格式化输出,它的行为类似于 java.util.Formatter.format(String format, Object…​ args)

Expression Language非常灵活,Hibernate Validator 提供了几个特性级别,您可以通过 ExpressionLanguageFeatureLevel 这个枚举来启用不同等级表达式语言特性:

  • NONE: Expression Language 插值完全禁用。

  • VARIABLES: 允许通过 addExpressionVariable(), 资源包 和 formatter 对象.

  • BEAN_PROPERTIES: 除了 VARIABLES 允许的还包括插入bean的属性(Getter方法)。

  • BEAN_METHODS: 还允许使用可执行方法的返回值。对于硬编码的约束消息可以认为是安全的,但是对于需要额外注意的 custom violations (自定义约束)。

约束消息的默认特性级别是 BEAN_PROPERTIES

你也可以在启动的时候设置级别 bootstrapping the ValidatorFactory.

以下部分提供了在错误消息中使用 EL 表达式的几个示例:

4.1.3. 示例

Example 45, “指定消息descriptors(描述符)” 展示了如何使用不同的选项来指定消息descriptors(描述符)。

Example 45. 指定消息descriptors(描述符)
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 实例会产生以下违反约束的行为,用该单元测试验证 Example 46, “判断是否为预期的错误消息”

  • manufacturer 字段的 @NotNull 约束将输出错误信息 "must not be null" ,这是由的Jakarta Bean Validation 规范定义的,加之你没有特殊的指定输出信息。

  • licensePlate 字段的 @Size 约束,展示如何使用 message parameters(消息参数) ({min}, {max}) 以及如何添加当前校验值,通过EL表达式: ${validatedValue}

  • seatCount 字段的 @Min 约束,演示了如何使用带有三元表达式的 EL 表达式动态选择单数或复数形式,这取决于约束的属性("There must be at least 1 seat" vs. "There must be at least 2 seats")

  • topSpeed 字段的 @DecimalMax 展示了如何使用格式化(formatter.format)程序实例输出已验证的值

  • 最后,price 字段的 @DecimalMax 约束展示了参数插值优先于表达式求值声明,也就是 $ 符号显示在最大价格前面

只有实际的约束属性可以使用格式 {attributeName} 。当引用添加到插值上下文中的验证值或自定义表达式变量时(参见 Section 12.13.1, “HibernateConstraintValidatorContext), 必须使用形式为 ${attributeName} 的 EL 表达式。

Example 46. 判断是否为预期的错误消息
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 (参见 Section 8.1, “配置 validator factory 通过 validation.xml) 中配置它,或者启动过程中配置 ValidatorFactoryValidator 时传递它 (参见 Section 9.2.1, “MessageInterpolatorSection 9.3, “配置Validator”, respectively).

4.2.1. ResourceBundleLocator

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

Hibernate Validator 中的缺省消息内插器 ResourceBundleMessageInterpolator 通过 SPI 技术检索具体的实现。在启动加载程序 ValidatorFactory 时,使用替代的资源包只需要传递一个具有资源包 PlatformResourceBundleLocator 实例的名称,如 Example 47, “使用特定的资源包”.

Example 47. 使用特定的资源包
Validator validator = Validation.byDefaultProvider()
		.configure()
		.messageInterpolator(
				new ResourceBundleMessageInterpolator(
						new PlatformResourceBundleLocator( "MyMessages" )
				)
		)
		.buildValidatorFactory()
		.getValidator();

当然,您也可以实现一个完全不同的 ResourceBundleLocator, 例如,它可以将数据库中的记录作为返回的资源信息。在这种情况下,您可以通过 HibernateValidatorConfiguration#getDefaultResourceBundleLocator() 获得默认locator(定位器),例如,您可以使用它作为备用数据源。

除了 PlatformResourceBundleLocator 之外,Hibernate Validator还提供了另一个资源包locator(定位器)实现,即 AggregateResourceBundleLocator ,它允许从多个资源包检索错误消息。例如,您可以在多模块应用程序中使用此实现,其中每个模块需要一个消息包。Example 48, “使用 AggregateResourceBundleLocator 展示了如何使用 AggregateResourceBundleLocator

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

请注意,资源包是按照传递给构造函数的顺序处理的。这意味着,如果多个资源包包含给定消息键的条目,则该值将从包含该键的列表中的第一个资源包中取出。

5. 分组约束

在前面几章中讨论的 ValidatorExecutableValidator 中的所有校验方法都有可变长参数 groups 。到目前为止,我们一直忽略这个参数,但是现在是时候仔细研究一下了。

5.1. 请求分组

Groups(分组)允许你在校验的时候限制校验范围,校验分组的一个使用场景是UI导航,其中每个步骤中只有指定的约束子集而得到校验。目标分组作为参数传递给校验方法。

让我们来看接下来的这个例子, Example 49, “实体类 Person 中的 Person 类的 name 有一个 @NotNull 注解约束。如果没有特别指定分组,那么会分组为默认的 jakarta.validation.groups.Default

当校验一个或多个分组的时候,也没有显示的指定分组。那么会被分类至 jakarta.validation.groups.Default

Example 49. 实体类 Person
package org.hibernate.validator.referenceguide.chapter05;

public class Person {

	@NotNull
	private String name;

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

	// getters and setters ...
}

Example 50, “Driver类” 中的 Driver 类继承至 Person 类,同时添加了 agehasDrivingLicense 两个字段。司机必须年满18岁(@Min(18))并持有驾驶执照( @AssertTrue ) , 在这些属性上定义的约束都属于 DriverChecks 分组,它只是一个简单的标记接口。

使用接口使得组的使用类型安全,并且允许轻松地进行重构。这也意味着组可以通过类继承彼此继承。见 Section 5.2, “分组继承”

Example 50. 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 类(Example 51, “Car类”) 具有一些约束,这些约束是默认组的一部分,但是 passedVehicleInspection 字段的 @AssertTrue 约束注解属于 CarChecks 分组,它表示一辆汽车是否通过了道路测试。

Example 51. 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.name, Car.manufacturer, Car.licensePlateCar.seatCount 字段属于 Default 分组

  • Driver.ageDriver.hasDrivingLicense 字段属于 DriverChecks 分组

  • Car.passedVehicleInspection 字段属于 CarChecks 分组

Example 52, “使用校验分组” 展示了如何将不同的组合传递给 Validator#validate() 方法,从而导致不同的校验结果。

Example 52. 使用校验分组
// 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()
);

Example 52, “使用校验分组” 中第一次调用 validate() 方法是在没有显式指定分组的情况下完成的。没有验证错误,即使 passedVehicleInspection 的默认值是 false ,因为在该属性上定义的约束不属于默认组。

第二次校验的是 CarChecks 分组,会校验失败除非汽车通过车辆检查(passedVehicleInspection = true)。为汽车添加一个驾驶员,会再次导致校验失败,因为驾驶员尚未通过驾驶考试,只有将 passedDrivingTest 设置为 true 后,对 DriverChecks 的分组才会通过。

最后一次调用 validate() 表示所有定义的分组的约束都通过校验。

5.2. 分组继承

Example 52, “使用校验分组”, 我们需要为每个校验分组调用一次 validate() , 或者一次性指定他们所有。

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

Example 53, “SuperCar类”中,我们定义了一个 SuperCar 类和一个 RaceCarChecks 分组,RaceCarChecks 分组继承至 Default 分组。 SuperCar (超级跑车)必须配备安全带才能参加比赛。

Example 53. 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 在只有一个座位且没有安全带,是否是合格的汽车?是否是合格的竞速车辆?

Example 54. 使用分组继承
// 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. 定义分组序列

默认情况下,不管约束属于哪个分组,约束都不按特定顺序进行计算。然而,在某些情况下,控制校验约束的顺序是有很用的。

Example 52, “使用校验分组” 中。要求在检查汽车的是否是赛车之前,首先要通过所有默认的汽车约束。最后,在开车离开之前,应该检查实际的驾驶员是否满足约束。

为了实现这样的验证顺序,您只需定义一个接口并使用 @GroupSequence 对其进行注解,定义必须验证组的顺序(参见 Example 55, “定义一个分组序列”)。 如果序列组中有一个约束校验失败,序列组中剩下的约束都不能算验证通过。

Example 55. 定义一个分组序列
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

然后您可以在Example 56, “使用分组序列”使用刚刚定义的分组序列。

Example 56. 使用分组序列
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 替换该类的组的顺序。

Example 57, “为 RentalCar 重新定义默认分组” 介绍将 RentalCar 重新定义默认分组。

Example 57. 为 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 {
}

Example 58, “验证具有重定义的默认组的对象” 当做了上述定义后,当你校验 Default 分组时,会同时 RentalCar 校验中 RentalChecks, CarChecks 分组的约束 。

Example 58. 验证具有重定义的默认组的对象
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 分组序列重写对于在其上定义的类是local(本地)的,不会传播到关联的对象。对于本例,这意味着将 DriverChecks 添加到 RentalCar 的默认组序列中,而不会对其他类产生任何效果。并且只有 Default 分组关联了驾驶员相关注解。

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

5.4.2. @GroupSequenceProvider

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

为此,需要实现接口 DefaultGroupSequenceProvider ,并通过 @GroupSequenceProvider 注解向目标类注册该实现。例如,在租车场景中,您可以动态地添加 CarChecks ,如Example 59, “实现和使用默认的组序列提供程序”

Example 59. 实现和使用默认的组序列提供程序
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. 分组转化

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

让我们看看 Example 60, “@ConvertGroup 用例”。这里 @GroupSequence({ CarChecks.class, Car.class }) 是修改 Default 分组。 (see Section 5.4, “重新定义默认组序列”). 这里的 @ConvertGroup(from = Default.class, to = DriverChecks.class) 它确保在对汽车程序关联进行级联验证期间将 Default 分组转化为 DriverChecks 分组。

Example 60. @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 ...
}

Example 61, “测试用例 @ConvertGroup 的校验结果是成功的。即使 hasDrivingLicense 的约束属于 DriverChecks 分组,并且 validate() 方法只校验 Default 分组。

Example 61. 测试用例 @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. 约束注解

本节展示如何编写一个约束注解,该注解可用于校验给定的字符串完全大写或小写。稍后,这个约束将应用于 Chapter 1, 快速开始 中的 CarlicensePlate 字段。确保该字段始终是大写字母字符串。

首先需要的是一种表达这两种情况模式(大写或小写)的方法。虽然可以使用 String 常量,但更好的方法是为此使用枚举:

Example 62. 枚举 CaseMode 表示大小写两种状态
package org.hibernate.validator.referenceguide.chapter06;

public enum CaseMode {
	UPPER,
	LOWER;
}

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

Example 63. 定义 @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 属性允许对分组(see Chapter 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,它允许指定所需的 case 模式。value 值是一个特殊的值,如果它是唯一指定的属性,在使用注解时可以省略,例如 @CheckCase(CaseMode.UPPER)

此外,约束注解还使用了一些元注解:

  • @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}): 定义约束支持的目标元素类型。 @CheckCase 可用于字段 ( FIELD), JavaBeans properties 和返回值 (METHOD), 方法/构造函数参数 (PARAMETER) 和参数化类型的类型参数 (TYPE_USE)。元素类型 ANNOTATION_TYPE 允许 @CheckCase 创建组合约束。

    在创建类级别约束 (see Section 2.1.4, “类层面的约束”), TYPE 类型必须被支持。针对构造函数返回值的约束需要支持 CONSTRUCTOR 类型。 交叉参数的校验必须支持 METHODCONSTRUCTOR

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

  • @Constraint(validatedBy = CheckCaseValidator.class): 标记在使用 @CheckCase 时指定一个指定的validator(校验器)。 如果某个约束可用于多个数据类型,则可以指定多个校验器,每个校验器对应一个数据类型。

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

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

示例中还显示了这个包含注解类型名为 List 。它允许在同一个元素上指定多个 @CheckCase ,例如使用不同的校验组和消息。虽然可以使用另一个名称,但 Jakarta Bean Validation 范建议使用名称 List 并使注解成为相应约束类型的内部注解。

6.1.2. 约束校验器

在定义了注解之后,您需要创建一个约束校验器,它能够校验使用 @CheckCase 的元素。 为此,实现 ConstraintValidator,如下所示:

Example 64. 实现 @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. The ConstraintValidatorContext

Example 64, “实现 @CheckCase 约束的约束校验器” 依赖于从 isValid() 方法返回 truefalse 来判断是否生成默认错误消息。 使用作为参数传递的 ConstraintValidatorContext 对象,可以添加额外的错误消息,也可以完全禁用默认错误消息生成,并只定义自定义错误消息。 ConstraintValidatorContext API 被建模为 fluent 接口,最好用一个例子来演示:

Example 65. 使用 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;
	}
}

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

通过调用 addConstraintViolation() 添加每个已配置的约束冲突非常重要。只有在此之后才会创建新的约束冲突。

默认情况下,在自定义约束中,ConstraintValidatorContext 不允许使用EL表达式。

但是,对于某些高级需求,可能需要使用 Expression Language。

在这种情况下,您需要打开 HibernateConstraintValidatorContext 并显式启用 Expression Language。有关更多信息,请参见 Section 12.13.1, “HibernateConstraintValidatorContext

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

6.1.2.2. The HibernateConstraintValidator 扩展

Hibernate Validator 提供了 ConstraintValidator 的拓展:HibernateConstraintValidator

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

HibernateConstraintValidatorinitialize() 方法有两个参数:

  • ConstraintDescriptor 代表当前的约束。你可以使用 ConstraintDescriptor#getAnnotation() 获取相关注解。

  • HibernateConstraintValidatorInitializationContext 它提供有用的帮助和上下文信息,如时钟提供者或时间校验容忍度。

这个扩展被标记为正在孵化,因此它可能会受到更改。该计划是将其标准化,并在未来将其纳入 Jakarta Bean Validation 标准。

下面的例子展示了如何将校验器基于 HibernateConstraintValidator:

Example 66. 使用 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. 将有效负载传递给约束校验器

有时,您可能希望在某些外部参数上设定约束验证器行为的条件。

例如,如果每个国家有一个实例,那么邮政编码验证器可能会根据应用程序实例的区域设置而有所不同。另一个需求可能是在特定环境中具有不同的行为: 分段环境可能无法访问验证程序正确运行所必需的一些外部生产资源。

为所有这些用例引入了约束验证器有效负载的概念。它是一个通过 HibernateConstraintValidatorContextValidator 实例传递到每个约束验证器的对象。

下面的示例演示如何在 ValidatorFactory 初始化期间设置约束验证器有效负载。除非您覆盖这个默认值,否则这个 ValidatorFactory 创建的所有 Validators 都将设置这个约束验证器有效负载值。

Example 67. 在 ValidatorFactory 初始化时定义有效负载
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.constraintValidatorPayload( "US" )
		.buildValidatorFactory();

Validator validator = validatorFactory.getValidator();

另一个选项是使用上下文设置每个 Validator 的约束验证器有效负载:

Example 68. 获取不同有效负载的 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

一旦你设置了约束验证器有效负载,它就可以在你的约束验证器中使用,如下面的例子所示:

Example 69. 在约束验证器中使用约束验证器有效负载
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 实现通过使用 constraint violation dynamic payload mechanism 有效负载机制将有效负载传递给发出的违反约束的情况。

6.1.3. 错误信息

最后一个缺少的构建块是一个错误消息,应该在违反 @CheckCase 约束时使用。要定义这个属性,请创建一个带有以下内容的 ValidationMessages.properties 文件 (see also Section 4.1, “默认消息插值”)

Example 70. 定义 CheckCase 约束的错误信息
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.

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

6.1.4. 使用约束

现在你可以使用 Chapter 1, 快速开始 章节中的 Car 类中的用该约束来指定 licensePlate 字段应该只包含大写字符串:

Example 71. 使用 @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 ...
}

最后, Example 72, “校验 @CheckCase 约束” 演示了如何通过校验将使用无效车牌的 Car 实例来违反 @CheckCase 约束。

Example 72. 校验 @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. 类级别的约束

如前所述,还可以在类级别应用约束来验证整个对象的状态。定义类级别约束的方式与定义属性约束的方式相同。Example 73, “实现一个类级别的约束注解” 展示了 @ValidPassengerCount 注解的定义。 它的使用可以查看 Example 19, “类级别约束”

Example 73. 实现一个类级别的约束注解
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 具体不满足校验的属性,而不是 @ValidPassengerCount 约束。

Example 74, “添加关于字段信息的 ConstraintViolation 展示了如何通过使用传递给 isValid() 的约束验证器上下文来为属性 passengers 构建一个带有属性节点的自定义约束违反。注意,您还可以添加几个属性节点,指向验证 bean 的子实体。

Example 74. 添加关于字段信息的 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 )必须必须设置为 Object 或者 Object[] ,以便在 isValid() 方法中接收方法/构造函数参数数组。

下面的示例显示了交叉参数约束的定义,该约束可用于检查方法的两个 Date 参数的顺序是否正确:

Example 75. 交叉参数约束
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 。这个元注释还指定了相应的验证器,如 Example 76, “泛型和交叉参数约束” 所示。除了元素类型 METHODCONSTRUCTOR 之外,还指定了 ANNOTATION_TYPE 作为注释的目标,以便能够基于 @ConsistentDateParameters 创建组合约束(参见:Section 6.4, “约束组合”)。

跨参数约束是在方法或构造函数的声明上直接指定的,返回值约束也是这种情况。因此,为了提高代码的可读性,建议选择使约束目标明显的约束名称,例如:@ConsistentDateParameters

Example 76. 泛型和交叉参数约束
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() ,它允许约束用户指定约束的目标,如示例 Example 77, “泛型和交叉参数约束”

Example 77. 泛型和交叉参数约束
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,如 Example 78, “为一般的和交叉参数约束指定目标”

Example 78. 为一般的和交叉参数约束指定目标
@ScriptAssert(script = "arg1.size() <= arg0", validationAppliesTo = ConstraintTarget.PARAMETERS)
public Car buildCar(int seatCount, List<Passenger> passengers) {
	//...
	return null;
}

6.4. 约束组合

查看示例 Example 71, “使用 @CheckCase 约束” 中的 Car 类的 licensePlate 字段,您已经看到了三个约束注释。在更复杂的场景中,甚至可以对一个元素应用更多的约束,这可能很容易变得有点令人困惑。此外,如果在另一个类中有一个 licensePlate ,那么您也必须将所有约束声明复制到另一个类中,这违反了 DRY 原则。

您可以通过创建由几个基本约束组成的更高级别约束来解决此类问题。例子 Example 79, “创建一个 @ValidLicensePlate 约束” 展示了一个合成的约束注释,其中包括 @NotNull@Size@CheckCase :

Example 79. 创建一个 @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 字段中使用新的组合约束完全等价于前一个版本,其中三个约束直接在字段本身中声明:

Example 80. 使用组合约束 ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;

public class Car {

	@ValidLicensePlate
	private String licensePlate;

	//...
}

这个在校验 Car 实例中构建 ConstraintViolations 合集,是 @ValidLicensePlate 每个组成的约束,如果您更喜欢在任何构成约束被违反的情况下使用单个约束违反,可以使用 @ReportAsSingleViolation

Example 81. 使用 @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: for keys and values;

  • java.util.Optional, java.util.OptionalInt, java.util.OptionalLong and java.util.OptionalDouble;

  • JavaFX's ObservableValue (参见 Section 7.4, “JavaFX 值提取器” ).

Jakarta Bean Validation specification 中可以找到内置值提取器的完整列表,并包含它们行为的所有细节。

7.2. 实现一个 ValueExtractor

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

实现一个 ValueExtractor 是不够的,您还需要注册它。详情请参阅 Section 7.5, “注册一个 ValueExtractor

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

例如,让我们考虑 Guava’s Optional。这是一个简单的示例,因为我们可以参考 java.util.Optional 编写Guava的值提取器:

Example 82. 实现Guava’s 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 注解标注使用约束的变量类型:即需要校验的参数类型

  • 我们使用receiver的 value() 方法来获取 Optional 包装里的值

  • 我们不希望属性违反约束时添加新的节点信息。所以在调用 value() 方法,我们传入了 null 作为节点名称。

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

让我们首先考虑值的情况,需要一个值提取器提取它们:

Example 83. A ValueExtractor for Multimap values
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 值的约束:

Example 84. 对 Multimap 的值进行约束
private Multimap<String, @NotBlank String> map1;

另一个值提取器需要能够对 Multimap 的键进行约束:

Example 85. A ValueExtractor for Multimap keys
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 的键和值的约束:

Example 86. 对 Multimap 的键和值进行约束
private Multimap<@NotBlank String, @NotBlank String> map2;

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

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

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

  • 在第一种情况下,我们将values传递给receiver ( keyedValue() 方法的第三个参数), 在另一种情况下,我们传递键。

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

value()

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

iterableValue()

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

indexedValue()

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

keyedValue()

对于包含键值的容器 - 它是用于 Maps. 它同时用于键和值。对于校验键的情况,键也作为验证值传递。

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

选择使用的方法非常重要,因为它将上下文信息添加到违反约束的属性路径中,例如索引或验证值的键。

7.3. 非通用容器

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

Hibernate Validator 还支持非通用容器的值提取。

让我们以 java.util.OptionalInt 为例,它将一个原生类型 intOptional 包装类。

OptionalInt 的值提取器的第一次尝试如下:

Example 87. A ValueExtractor for OptionalInt
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 有效负载:

Example 88. 使用 Unwrapping.Unwrap 有效载荷
@Min(value = 5, payload = Unwrapping.Unwrap.class)
private OptionalInt optionalInt1;

如果我们退后一步,我们想要添加到 OptionalInt 属性的大部分约束(如果不是全部的话)将应用于包装的值,因此有一种方法使它成为默认值将是很好的。

这正是 @UnwrapByDefault 注释的用途:

Example 89. A ValueExtractor for OptionalInt marked with @UnwrapByDefault
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 声明这个值提取器时,约束注释默认应用于被包装的值:

Example 90. 隐式展开 @UnwrapByDefault
@Min(5)
private OptionalInt optionalInt2;

注意,您仍然可以使用 Unwrapping.Skip 有效负载为包装器本身声明一个注释:

Example 91. Avoid implicit unwrapping with Unwrapping.Skip
@NotNull(payload = Unwrapping.Skip.class)
@Min(5)
private OptionalInt optionalInt3;

OptionalInt@UnwrapByDefault 值提取器是内置值提取器的一部分: 不需要额外添加。

7.4. JavaFX 值提取器

JavaFX 中的 Bean 属性通常不是像 Stringint 这样的简单数据类型,而是包装在 Property 类型中,这使得它们可以被observable(监听变化),用于数据绑定等等。

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

JavaFX 的 ObservableValue 值提取器需要标注 @UnwrapByDefault。 因此,容器上承载的约束默认以包装后的值为目标。

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

Example 92. 约束 StringProperty
@NotBlank
private StringProperty stringProperty;

或者约束 LongProperty:

Example 93. 约束 LongProperty
@Min(5)
private LongProperty longProperty;

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

可以像约束 List 那样约束 ReadOnlyListProperty :

Example 94. 约束 ReadOnlyListProperty
@Size(min = 1)
private ReadOnlyListProperty<@NotBlank String> listProperty;

7.5. 注册一个 ValueExtractor

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

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

由验证引擎本身提供

参见 Section 7.1, “内置值提取器”.

通过 Java 服务加载器机制

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

In the META-INF/validation.xml file

有关如何在 XML 配置中注册值提取器的更多信息, 参见 Section 8.1, “配置 validator factory 通过 validation.xml

By calling Configuration#addValueExtractor(ValueExtractor<?>)

参见 Section 9.2.6, “注册 ValueExtractors”

By invoking ValidatorContext#addValueExtractor(ValueExtractor<?>)

它只声明这个 Validator 实例的值提取器。

对于给定的类型和类型参数,以较高优先级指定的值提取器会覆盖以较低优先级指定的相同类型和类型参数的任何其他提取器。

7.6. 分辨率算法

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

记住一件重要的事情:

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

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

8. 通过 XML 配置

到目前为止,我们已经使用了 Jakarta Bean Validation 的默认配置源,即注解。然而,也有两种 描述符允许通过 XML 进行配置。第一个描述符描述了一般的 Jakarta Bean Validation 行为,并作为 META-INF/validation.xml 提供。第二个描述了约束声明,并通过注解与约束声明方法紧密匹配。让我们来看看这两种文档类型。

8.1. 配置 validator factory 通过 validation.xml

启用 Hibernate Validator 的 XML 配置的关键是文件 META-INF/validation.xml 。如果这个文件存在于classpath中,那么在创建 ValidatorFactory 时,它的配置将被应用。Figure 1, “Validation configuration schema” 显示了 validation.xml 必须遵循的schema。

validation-configuration-2.0.xsd
Figure 1. Validation configuration schema

Example 95, “validation.xml 展示了 validation.xml 的几个配置选项。所有设置都是可选的,同样的配置选项也可以通过 jakarta.validation.Configuration 以编程方式提供。实际上,XML 配置将被通过编程 API 显式指定的值覆盖。甚至可以通过 Configuration#ignoreXmlConfiguration() 完全忽略 XML 配置。另见Section 9.2, “配置 ValidatorFactory

Example 95. 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>

classpath中必须只有一个名为 META-INF/validation.xml 的文件。如果发现多于一个,则抛出异常。

default-provider 允许选择 Jakarta Bean Validation 实现程序。如果classpath中有多个实现程序,则此选项非常有用。message-interpolator, traversable-resolver, constraint-validator-factory, parameter-name-providerclock-provider 的配置,分别用于指定 MessageInterpolator, TraversableResolver, ConstraintValidatorFactory, ParameterNameProviderClockProvider 接口的具体实现。有关这些接口的更多信息,请参阅 Section 9.2, “配置 ValidatorFactory

value-extractor 允许声明附加的值提取器,以便从自定义容器类型中提取值或者覆盖内置的值提取器。有关如何实现 jakarta.validation.valueextraction.ValueExtractor ,参考 Chapter 7, 值提取

executable-validation 和它的子节点定义了method约束的默认值。Jakarta Bean Validation 规范将构造函数和非 getter 方法定义为默认值。这个属性充当启用和关闭方法验证的全局切换(参见 Chapter 3, 声明和校验 method 约束)。

通过 constraint-mapping 配置,你需要列举出所有约束配置相关的 XML 文件。映射文件名必须使用其在classpath中的完全限定名指定。关于编写映射文件的详细信息可在下一节中找到。

最后但并非最不重要的一点是,您可以通过 property 节点指定提供程序特定的属性。在这个例子中,我们使用了特定于 Hibernate Validator 的 hibernate.validator.fail_fast 属性(参见Section 12.2, “快速失败模式”)。

8.2. 通过 constraint-mappings 映射约束

可以通过文件来表达 XML 中的约束,这些文件遵循 Figure 2, “Validation mapping schema” 。注意,只有在 validation.xml 中通过约束映射列出这些映射文件时,才会处理它们。

validation-mapping-2.0.xsd
Figure 2. Validation mapping schema
Example 96. 通过 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>
Example 97. 通过 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 配置和采用编程注解的效果是一样的。出于这个原因,建议在程序中添加一些注解就足够了。 default-package 用于需要类名的所有字段。如果指定的类没有完全限定,则将使用已配置的默认包。然后,每个映射文件可以有几个 bean 节点,每个 bean 节点用指定的类名描述实体上的约束。

一个类只能跨所有配置文件配置一次。对于给定的约束注解,约束定义也是如此。它只能出现在一个映射文件中。如果违反了这些规则,就会抛出 ValidationException 异常。

ignore-annotations 设置为 true 意味着放置在配置 bean 上的约束注解将被忽略。此值的默认值为 true。ignore-annotations 也可用于 class, fields, getter, constructor, method, parameter, cross-parameter and return-value。如果未在这些级别上显式指定,则默认配置为true。

class, field, getter, container-element-type, constructormethod 节点(以及他们的子节点参数)决定了约束放置在哪个级别。valid 节点用于启用级联验证和约束节点在相应级别上添加约束。每个约束定义必须通过 annotation 定义类。Jakarta Bean Validation 规范(message, groups and payload)所需的约束属性具有专用节点。所有其他特定于约束的属性都是使用元素节点配置的。

container-element-type 允许为容器元素定义级联验证行为和约束。在上面的例子中,您可以看到一个嵌套在 Map 值中的 List 上的嵌套容器元素约束的例子。 type-argument-index 用于精确地确定映射的类型参数与配置有关。如果类型只有一个类型参数(例如,示例中的 Lists ) ,则可以省略它。

class 节点还允许通过组序列节点重新配置默认的组序列(参见Section 5.4, “重新定义默认组序列”)。示例中没有显示使用 group-sequence 来指定组转换(参见Section 5.5, “分组转化”)。该节点可用于 field, getter, container-element-type, parameterreturn-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. Bootstrapping

Section 2.2.1, “获取 Validator 实例”,您已经看到了一种创建 Validator 实例的方法 - 通过 Validation#buildDefaultValidatorFactory()。在本章中,您将学习如何使用 jakarta.validation.Validation 中的其他方法。以便引导生成特定配置的校验器。

9.1. 获取 ValidatorFactoryValidator

你可以通过 jakarta.validation.Validation 的静态方法获取一个 ValidatorFactory 实例。再通过调用 ValidatorFactory 工厂实例的 getValidator() 方法获取 Validator 对象。

Example 98, “获取默认 ValidatorFactory 以及 Validator 展示了如何从默认校验器工厂获得校验器:

Example 98. 获取默认 ValidatorFactory 以及 Validator
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

生成的 ValidatorFactoryValidator 实例是线程安全的,可以进行缓存。由于 Hibernate Validator 使用工厂作为缓存约束元数据的上下文,因此建议在应用程序中只使用一个工厂实例。

Jakarta Bean Validation 支持在一个应用程序中多个provider(如 Hibernate Validator)一起工作。但是如果在classpath中存在多个provider,则不能保证在通过 buildDefaultValidatorFactory() 创建工厂时选择哪个provider。

在这种情况下,您可以通过 Validation#byProvider() 显式指定要使用的provider,传递provider的 ValidationProvider 类,如Example 99, “获取 ValidatorFactoryValidator 通过指定的provider”

Example 99. 获取 ValidatorFactoryValidator 通过指定的provider
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

注意, configure() 返回的配置对象允许在调用 buildValidatorFactory() 之前专门定制工厂实例。本章后面将讨论可用的配置选项。

类似地,您可以获取默认配置的校验器工厂,如Example 100, “获取默认配置的 ValidatorFactory

Example 100. 获取默认配置的 ValidatorFactory
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
		.configure()
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

如果 ValidatorFactory 实例不再使用,应该通过调用 ValidatorFactory#close() 来释放它。同时将释放掉给工厂分配的所有资源。

9.1.1. ValidationProviderResolver

默认情况下,可用的 Jakarta Bean Validation provider是通过使用 Java Service Provider 机制发现的。

为此,每个提供者都包含文件 META- INF/services/jakarta.validation.spi.ValidationProvider ,包含其 ValidationProvider 实现的完全限定类名。对于 Hibernate Validator,这是 org.hibernate.validator.HibernateValidator

根据您的环境及其类加载细节,Java 的服务加载器机制(SPI)可能无法工作。在这种情况下,您可以插入用于执行provider检索的自定义 ValidationProviderResolver 实现。一个例子是 OSGi,您可以在其中实现使用 OSGi 服务进行提供者发现的提供者解析器。

要使用自定义provider解析器,请通过 providerResolver() 传递它,如Example 101, “使用自定义 ValidationProviderResolver

Example 101. 使用自定义 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 (参见Chapter 8, 通过 XML 配置)配置的。

如果希望禁用基于 XML 的配置,可以通过调用 Configuration#ignoreXmlConfiguration() 来禁用。

XML 配置的不同值可以通过 Configuration#getBootstrapConfiguration() 访问。例如,如果您希望将 Jakarta Bean Validation 集成到托管环境中,并希望创建通过 XML 配置的对象的托管实例,那么这可能会很有帮助。

使用 fluent 配置 API,可以在引导 factory 时覆盖一个或多个默认配置。下面的部分将展示如何使用不同的配置选项。注意, Configuration 类公开了不同扩展点的默认实现,如果您希望将这些扩展点用作自定义实现的委托,那么这些扩展点非常有用。

9.2.1. MessageInterpolator

校验引擎使用Message interpolators(消息内插器)从约束消息描述符创建用户可读的错误消息。

如果在Chapter 4, 添加约束错误消息中插值约束错误消息不足以满足您的需要,您可以通过配置 Configuration#messageInterpolator() 传递您自己的 MessageInterpolator 接口实现,如Example 102, “使用自定义 MessageInterpolator

Example 102. 使用自定义 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 接口,可以访问哪些属性,哪些属性不受控制。Example 103, “使用自定义 TraversableResolver 展示了如何使用自定义可遍历解析器实现。

Example 103. 使用自定义 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)一起使用时,只有那些已经被持久层提供程序(ORM框架)加载的属性才被认为是可访问的,所有属性都被认为是级联的。

默认情况下,每个校验调用都缓存可遍历的解析程序返回的结果。这在 JPA 环境中特别重要,因为在这种环境中调用 isReachable() 的成本很高。

这种缓存会增加一些开销。如果您的自定义可遍历解析器非常快,那么最好考虑关闭缓存。

你可以通过 XML 配置来禁用缓存:

Example 104. 通过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:

Example 105. 通过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 需要一个public的无参构造函数来实例化 ConstraintValidator 实例(参见Section 6.1.2, “约束校验器”)。使用自定义 ConstraintValidatorFactory 可以提供在约束校验器(ConstraintValidator)实现中使用依赖注入。

要配置自定义约束校验器工厂,请调用 Configuration#constraintValidatorFactory() (Example 106, “使用自定义 ConstraintValidatorFactory)。

Example 106. 使用自定义 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 等形式的合成名称。

要使用自定义参数名称提供程序,可以在引导期间传递提供程序的实例,如Example 107, “使用自定义 ParameterNameProvider,或者指定提供程序的完全限定类名作为 META-INF/validation.xml 文件中的 <parameter-name-provider> 节点的值(参见Section 8.1, “配置 validator factory 通过 validation.xml)。示例Example 107, “使用自定义 ParameterNameProvider演示了这一点。

Example 107. 使用自定义 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 实现,它提供了几种在运行时获取参数名称的方法。请参阅Section 12.14, “基于 Paranamer 的 ParameterNameProvider以了解关于这个特定实现的更多信息。

9.2.5. ClockProvider 和时间校验容忍度

对于与时间相关的校验(例如 @Past@Future 约束) ,很值得思考如何表示 now

当您希望以可靠的方式测试约束时,这一点尤其重要。

引用时间由 ClockProvider ClockProvider 的职责是提供一个 java.time.Clock 定义 now 提供给时间相关的校验器使用。

Example 108. 使用自定义 ClockProvider
package org.hibernate.validator.referenceguide.chapter09;

import java.time.Clock;
import java.time.ZonedDateTime;

import jakarta.validation.ClockProvider;

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 实现的完全限定类名(参见Chapter 8, 通过 XML 配置)。

在校验 @Future@Past 约束时,您可能希望获得当前时间。

您可以通过调用 ConstraintValidatorContext#getClockProvider() 方法来获得校验器中的 ClockProvider

例如,如果希望用更明确的消息替换 @Future 约束的缺省消息,这可能很有用。

在处理分布式体系架构时,在应用诸如 @Past@Future 这样的时间约束时,您可能需要一定的容错能力。

通过引导 ValidatorFactory ,可以设置时间校验容忍度,如下所示:

Example 109. 使用时间校验公差
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 契约(参见Section 6.1.2.2, “The HibernateConstraintValidator 扩展”)。这份合同目前被标记为正在酝酿之中: 未来可能会发生变化。

9.2.6. 注册 ValueExtractors

正如在Chapter 7, 值提取中提到的,值提取器可以在引导过程中注册(参见Section 7.5, “注册一个 ValueExtractor的其他注册值提取器的方法)。

Example 110, “注册值提取器” 展示了我们如何注册先前创建的值提取器,以提取 Guava 的 Multimap 的键和值。

Example 110. 注册值提取器
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
		.configure()
		.addValueExtractor( new MultimapKeyValueExtractor() )
		.addValueExtractor( new MultimapValueValueExtractor() )
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

9.2.7. 添加映射流

如前所述,您可以使用基于 XML 的约束映射配置应用于 Java beans 的约束。

除了 META-INF/validation.xml 中指定的映射文件之外,还可以通过 Configuration#addMapping() 添加进一步的映射(参见Example 111, “添加约束映射流”)。注意,传递的输入流必须遵守Section 8.2, “通过 constraint-mappings 映射约束”中提出的约束映射的 XML 模式。

Example 111. 添加约束映射流
InputStream constraintMapping1 = null;
InputStream constraintMapping2 = null;
ValidatorFactory validatorFactory = Validation.byDefaultProvider()
		.configure()
		.addMapping( constraintMapping1 )
		.addMapping( constraintMapping2 )
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

在创建了校验器工厂之后,应该关闭所有传递的输入流。

9.2.8. Provider-specific 配置

通过 Validation#byProvider() ()返回的配置对象,可以配置 Provider-specific

对于 Hibernate Validator,这个例子允许您启用故障快速模式并传递一个或多个编程约束映射,如Example 112, “设置特定于 Hibernate Validator 的选项”所示。

Example 112. 设置特定于 Hibernate Validator 的选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.failFast( true )
		.addMapping( (ConstraintMapping) null )
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

另外,provider-specific 的选项可以通过 Configuration#addProperty() 传递。 Hibernate Validator 也支持以这种方式启用故障快速模式:

Example 113. 通过 addProperty() 方式启用 Hibernate Validator 特定选项
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.addProperty( "hibernate.validator.fail_fast", "true" )
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

请参阅Section 12.2, “快速失败模式”Section 12.4, “可编程约束的定义和声明”,以了解有关 Fail fast mode 和 constraint declaration API 的更多信息。

9.2.9. 配置 ScriptEvaluatorFactory

对于像 @ScriptAssert@ParameterScriptAssert 这样的约束,可以配置如何初始化脚本引擎以及如何构建脚本计算器。这可以通过设置 ScriptEvaluatorFactory 的自定义实现来实现。

特别是,这对于模块化环境(例如 OSGi)非常重要,在这种环境中,用户可能会遇到模块化类加载和 JSR 223 的问题。它还允许使用任何自定义脚本引擎,不一定基于 JSR 223 (例如 Spring Expression Language)。

9.2.9.1. XML 配置

要通过 XML 指定 ScriptEvaluatorFactory ,需要定义 hibernate.validator.script_evaluator_factory 属性。

Example 114. 通过 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 的配置提供了更大的灵活性。Example 115, “以编程方式定义 ScriptEvaluatorFactory显示了如何实现这一点。

Example 115. 以编程方式定义 ScriptEvaluatorFactory
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.scriptEvaluatorFactory( new CustomScriptEvaluatorFactory() )
		.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
9.2.9.3. 自定义 ScriptEvaluatorFactory 的实现案例

本节展示了两个自定义的 ScriptEvaluatorFactory 实现,它们可以在模块化环境中使用,也可以使用 Spring Expression Language 编写约束脚本。

模块化环境和 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 <http://www.apache.org/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 实例: 通常是所需 ScriptEngines 的类加载器。

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 <http://www.apache.org/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 ,该 BundleContext 将用于搜索 ScriptEngineFactory 作为参数。

如前所述,您还可以使用不基于 JSR 223 的脚本引擎。

例如,要使用 Spring Expression Language ,可以将 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.3. 配置Validator

在使用已配置的验证器工厂时,有时需要将不同的配置应用于单个 Validator 实例。Example 116, “配置 Validator 实例通过 usingContext() 展示了如何通过调用 ValidatorFactory#usingContext() 来实现这一点。

Example 116. 配置 Validator 实例通过 usingContext()
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 类型。

本章中提供的例子基于Example 117, “使用的演示类”中所示的类和约束声明。

Example 117. 使用的演示类
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 Validator#getConstraintsForClass() 方法,它返回 BeanDescriptor 接口的一个实例。使用这个描述符,您可以获得直接在 bean 本身(类或属性级)上声明的约束的元数据,但也可以检索表示单个属性、方法和构造函数的元数据描述符。

Example 118, “使用 BeanDescriptor 演示了如何检索 Car 类的 BeanDescriptor ,以及如何以断言的形式使用该描述符。

如果由请求的类承载的约束声明无效,则抛出 ValidationException

Example 118. 使用 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 派生的所有描述符都是通用的(参见Section 10.4, “ElementDescriptor) ,并返回一组描述符,它们表示直接在给定元素上声明的约束。对于 BeanDescriptor ,返回 bean 的类级别约束。关于 ConstraintDescriptor 的更多细节可以在Section 10.7, “ConstraintDescriptor中找到。

通过 getConstraintsForProperty()getConstraintsForMethod()getConstraintsForConstructor() ,您可以获得描述符,它表示一个给定的属性或可执行元素,通过其名称进行标识,对于方法和构造函数,还可以获得参数类型。这些方法返回的不同描述符类型将在下面的小节中描述。

请注意,这些方法根据Section 2.1.5, “约束继承”中描述的约束继承规则,考虑在超类型中声明的约束。一个例子是 manufacturer 属性的描述符,它提供了对 Vehicle#getManufacturer() 和实现方法 Car#getManufacturer() 上定义的所有约束的访问。如果指定的元素不存在或不受约束,则返回 null

方法 getConstrainedProperties()getConstrainedMethods()getConstrainedConstructors() 返回集合(可能是空的)分别具有所有约束属性、方法和构造函数。如果一个元素至少有一个约束或者被标记为级联验证,则认为该元素受到约束。在调用 getConstrainedMethods() 时,可以指定要返回的方法的类型(getter、 non-getter 或者两者都是)。

10.2. PropertyDescriptor

接口 PropertyDescriptor 表示类的一个给定属性。只要遵守 JavaBeans 命名约定,那么在字段或属性 getter 上声明约束是透明的。Example 119, “使用 PropertyDescriptor展示了如何使用 PropertyDescriptor 接口。

Example 119. 使用 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 ,提供给定属性的各个约束的更多信息。如果属性标记为级联验证(使用 @Valid 注释或通过 XML) ,则 isCascaded() 方法返回 true ,否则返回 falsegetGroupConversions() 返回任何已配置的组转换。有关 GroupConversionDescriptor 的详细信息,请参阅Section 10.6, “GroupConversionDescriptor

10.3. MethodDescriptorConstructorDescriptor

约束方法和构造函数分别由接口 MethodDescriptorConstructorDescriptor 表示。Example 120, “使用 MethodDescriptorConstructorDescriptor 演示了如何使用这些描述符。

Example 120. 使用 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() 检索当前使用的参数名称提供程序(参见 Section 9.2.4, “ParameterNameProvider)返回的索引和名称。

遵循 JavaBeans 命名约定的 Getter 方法被视为 bean 属性,但也被视为受约束的方法。

这意味着您可以通过获取 PropertyDescriptor (例如 BeanDescriptor.getConstraintsForProperty("foo") )或通过检查 getter 的 MethodDescriptor 的返回值描述符(例如 BeanDescriptor.getConstraintsForMethod("getFoo").getReturnValueDescriptor() )来检索相关的元数据。

10.4. ElementDescriptor

ElementDescriptor 接口是各个描述符类型(如 BeanDescriptor, PropertyDescriptor 等)的公共基类。除了 getConstraintDescriptors() 之外,它还提供了一些所有描述符通用的方法。

hasConstraints() 允许快速检查一个元素是否有任何直接的约束(例如,在 BeanDescriptor 中的类级约束)。

getElementClass() 返回由给定描述符表示的元素的 Java 类型:

  • BeanDescriptor 上调用时的对象类型,

  • 当分别在 PropertyDescriptorParameterDescriptor 上调用属性或参数时,

  • Object[].class 调用 CrossParameterDescriptor 时,

  • ConstructorDescriptorMethodDescriptorReturnValueDescriptor 上调用时的返回类型。类将为没有返回值的方法返回 void.class

Example 121, “使用 ElementDescriptor methods 展示了如何使用这些方法。

Example 121. 使用 ElementDescriptor methods
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 允许您以细粒度的方式查询约束元数据。 Example 122, “用法 ConstraintFinder 展示了如何通过 findConstraints() 检索 ConstraintFinder 实例,并使用 API 查询约束元数据。

Example 122. 用法 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 接口是所有支持容器元素约束和级联验证(PropertyDescriptor, ParameterDescriptor, ReturnValueDescriptor)的元素的公共接口。

它有一个方法 getConstrainedContainerElementTypes() ,它返回一组 ContainerElementTypeDescriptor

ContainerElementTypeDescriptor 扩展 ContainerDescriptor 以支持嵌套容器元素约束。

ContainerElementTypeDescriptor 包含有关容器、约束和级联验证的信息。

Example 123, “使用 ContainerElementTypeDescriptor 展示了如何使用 getConstrainedContainerElementTypes() 检索 ContainerElementTypeDescriptor 集。

Example 123. 使用 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

所有表示可以作为级联验证主题的元素的描述符类型(即 PropertyDescriptor, ParameterDescriptorReturnValueDescriptor)都通过 getGroupConversions() 提供对元素组转换的访问。返回的集合为每个配置的转换包含一个 GroupConversionDescriptor ,允许检索转换的源和目标组。Example 124, “使用 GroupConversionDescriptor 显示了一个示例。

Example 124. 使用 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 接口描述单个约束及其组成约束。通过该接口的一个实例,您可以访问约束注释及其参数。

Example 125, “使用 ConstraintDescriptor 展示了如何从 ConstraintDescriptor 中检索默认约束属性(如消息模板、组等)以及自定义约束属性(piecesOfLuggagePerPassenger)和其他元数据(如约束的注释类型及其验证器)。

Example 125. 使用 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 用于实现多层数据校验,其中约束注解只标注一次(带注解的domain域模型) ,可以在应用程序的各个不同层进行检查。由于这个原因,存在与其他技术的多个集成点。

11.1. ORM 集成

Hibernate Validator 可以和 Hibernate ORM 以及所有纯 Java 编写的持久层框架集成使用。

当需要校验延迟加载的关联时,建议将约束放在关联的 getter 方法上。Hibernate ORM 用代理实例替换了延迟加载的关联,代理实例通过 getter 请求时得到初始化/加载的值。在这种情况下,如果将约束放置在字段级别,则将使用实际的代理实例,这将导致校验错误。

11.1.1. 数据库schema级别校验

开箱即用,Hibernate ORM 会将您为实体定义的约束转换为映射元数据。例如,如果实体的一个属性被注解为 @NotNull ,那么在 Hibernate ORM 生成的 DDL 语句中,它的列将被声明为 not null

如果由于某种原因,需要禁用该特性,请将 hibernate.validator.apply_to_ddl 设置为 false。另见Section 2.3.1, “Jakarta Bean Validation 约束”Section 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-persist, jakarta.persistence.validation.group.pre-updatejakarta.persistence.validation.group.pre-remove 将组配置为根据每个事件类型进行校验。这些属性的值是要校验的组的逗号分隔的完全指定的类名。Example 126, “手动配置 BeanValidationEvenListener显示了这些属性的默认值。在这种情况下,它们也可以省略。

在违反约束时,事件将抛出一个运行时 ConstraintViolationException ,其中包含一组描述每个失败的 ConstraintViolation 实例。

如果 Hibernate Validator 出现在classpath中,Hibernate ORM 将透明地使用它。如果Hibernate Validator 位于classpath中,但是您不想对类校验,可以将 jakarta.persistence.validation.mode 设置为none。

如果没有用校验注解对 bean 进行注解,那么就不会有运行时性能开销。

如果您需要为 Hibernate ORM 手动设置事件监听器,请在 hibernate.cfg.xml 中使用以下配置:

Example 126. 手动配置 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 位于classpath中,则 JPA2规范要求启用 Jakarta Bean Validation。在这种情况下,可以在 persistence.xml. 中配置 jakarta.persistence.validation.group.pre-persist, jakarta.persistence.validation.group.pre-updatejakarta.persistence.validation.group.pre-remove 这两个属性,如Section 11.1.2, “Hibernate ORM 事件校验”所述。还定义了一个节点校验模式,可以设置为 AUTOCALLBACKNONE 。默认值是 AUTO

11.2. JSF & Seam

当使用 JSF2 或 JBoss Seam ,并且 Hibernate Validator (Jakarta Bean Validation)存在运行环境中时,对应用程序中的每个字段都会触发校验。Example 127, “在JSF2使用Jakarta Bean Validation” 展示了 JSF 页面中的 f:validateBean 标记的示例。 validationGroups 属性是可选的,可用于指定以逗号分隔的校验组列表。默认值是 jakarta.validation.groups.Default 。有关更多信息,请参考 Seam 文档或 JSF 2规范。

Example 127. 在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 Integration”一章中进行了描述。了解 JSF 2实现了一个自定义的 MessageInterpolator 以确保正确的本地化是很有趣的。为了鼓励使用 Jakarta Bean Validation 消息工具,JSF 2默认只显示生成的 Bean 校验消息。但是,这可以通过应用程序资源包配置,方法是提供以下配置( {0} 替换为 Jakarta Bean 校验消息, {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 的方法和构造函数的参数和返回值约束。

当您的应用程序在 Java EE 容器上运行时,默认情况下将启用此集成。在 Servlet 容器或纯 Java SE 环境中使用 CDI 时,可以使用 Hibernate Validator 提供的 CDI 可移植扩展。为此,将可移植扩展添加到类路径中,如Section 1.1.2, “CDI”

11.3.1. 依赖注入

CDI 的依赖注入机制使得获取 ValidatorFactoryValidator 实例。并在你管理的 bean 中使用它们变得非常容易。只需使用 @jakarta.inject.Inject 注解 bean 的实例字段即可注入对象。如Example 128, “通过 @Inject 注入 validator factory 和 validator”

Example 128. 通过 @Inject 注入 validator factory 和 validator
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 描述符,如Chapter 8, 通过 XML 配置

如果您正在与几个 Jakarta Bean Validation 实现框架一起工作,您可以通过使用 @HibernateValidator 限定符注解注入点来确保从 Hibernate Validator 注入工厂和校验程序,Example 129, “使用 @HibernateValidator 限定符注解”

Example 129. 使用 @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 ,它是 ValidationProvider 实现,用于在使用引导 API 时选择 Hibernate Validator (参见Section 9.1, “获取 ValidatorFactoryValidator)。

通过 @Inject ,您还可以将依赖关系注入约束校验器和其他 Jakarta Bean Validation 对象,例如 MessageInterpolator 实现等。

Example 130, “带注入 bean 的约束校验器” 演示了如何在 ConstraintValidator 实现中使用注入的 CDI bean 来确定给定的约束是否有效。如示例所示,您还可以使用 @PostConstruct@PreDestroy 回调来实现所需的构造和销毁逻辑。

Example 130. 带注入 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 这隐式地发生在 Java EE 执行期函式库中,或者通过添加 hibernate-validator-cdi 工件来显式地发生-参见Section 1.1.2, “CDI”

Example 131. 带有方法级约束的 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 ,并提供被违反的约束的详细信息。如果方法的返回值违反了任何返回值约束,情况也是如此。

类似地,构造函数约束在调用时自动进行校验。在示例中,将校验构造函数返回的 RentalStation 对象,因为构造函数返回值标记为 @Valid

11.3.2.1. 校验可执行类型

Jakarta Bean Validation允许对自动校验的可执行类型进行细粒度控制。默认情况下,会校验对构造函数和非 getter 方法的约束。因此,在Example 131, “带有方法级约束的 CDI 管理 bean” 中,方法 RentalStation#getAvailableCars() 上的 @NotNull 约束在调用方法时不会得到校验。

您可以通过以下选项配置哪些类型的可执行文件在调用时进行校验:

如果为给定的可执行文件指定了多个配置源,则可执行级别上的 @ValidateOnExecution 优先于类型级别上的 @ValidateOnExecution ,而 @ValidateOnExecution 通常优先于 META- INF/validation.xml 中的全局配置类型。

Example 132, “使用 @ValidateOnExecution ”展示了如何使用 @ValidateOnExecution 注解:

Example 132. 使用 @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 是一种更加紧凑的形式,用于显式地指定所有类型 CONSTRUCTORS, GETTER_METHODSNON_GETTER_METHODS

可执行校验可以通过在 META-INF/validation.xml 中指定 <executable-validation enabled="false"/> 来全局关闭。在这种情况下,所有 @ValidateOnExecution 注解都会被忽略。

注意,当一个方法覆盖或实现一个超类型方法时,配置将从该覆盖或实现的方法中获取(通过 @ValidateOnExecution 在该方法本身或超类型上给出)。这可以保护 super-type 方法的客户端免受配置的意外更改,例如禁用子类型中重写的可执行文件的校验。

如果 CDI 管理的 bean 覆盖或实现了一个超类型方法,而这个超类型方法承载了任何约束,那么可能会发生校验拦截器没有正确地注册到 bean,导致在调用时没有对 bean 的方法进行校验。在这种情况下,您可以在子类上指定 IMPLICIT 类型,如Example 133, “使用 ExecutableType.IMPLICIT ,它确保发现所有需要的元数据,并在调用 ExpressRentalStation 上的方法时启用校验拦截器。

Example 133. 使用 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. Java EE

当您的应用程序在 Java EE 应用程序服务器( WildFly)上运行时,您还可以通过管理对象(如 EJBs 等)中的 @Resource 注入获得 ValidatorValidatorFactory 实例,如Example 134, “通过 @Resource 注解注入 ValidatorValidatorFactory所示。

Example 134. 通过 @Resource 注解注入 ValidatorValidatorFactory
package org.hibernate.validator.referenceguide.chapter11.javaee;

public class RentalStationBean {

	@Resource
	private ValidatorFactory validatorFactory;

	@Resource
	private Validator validator;

	//...
}

或者,您可以分别以 "java:comp/Validator" 和 "java:comp/ValidatorFactory" 的名称从 JNDI 获得校验器和校验器工厂。

类似于通过 @Inject 实现的基于 cdi 的注入,这些对象表示默认的校验器和校验器工厂,因此可以使用 XML 描述符 META-INF/validation.xml (参见Chapter 8, 通过 XML 配置)进行配置。

当您的应用程序启用了 cdi 时,注入的对象也可以感知到 cdi,例如,在约束校验器中支持依赖注入。

11.5. JavaFX

Hibernate Validator 还支持对 JavaFX 属性进行展开。如果 JavaFX 出现在类路径上,那么 JavaFX 属性的 ValueExtractors 就会自动注册。有关示例和进一步讨论,请参见Section 7.4, “JavaFX 值提取器”

12. Hibernate Validator 特性

在本章中,您将学习如何使用 Hibernate Validator 提供的几个特性以及 Jakarta Bean Validation 规范定义的功能。这包括快速失败模式、编程约束配置的 API 和约束的布尔组合。

新的 APIs 或 SPIs 被标记为 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 引导机制使用的类(例如:验证提供程序,配置类); 有关详细信息,请参阅Chapter 9, Bootstrapping

org.hibernate.validator.cfg, org.hibernate.validator.cfg.context, org.hibernate.validator.cfg.defs, org.hibernate.validator.spi.cfg

Hibernate Validator 中有关约束声明的 fluent API。 在 org.hibernate.validator.cfg 中可以找到 ConstraintMapping 接口,在 org.hibernate.validator.cfg.defs 可以找到所有约束的定义,在 org.hibernate.validator.spi.cfg 中可以找到使用该 API 配置默认验证器工厂的回调。有关详细信息,请参阅Section 12.4, “可编程约束的定义和声明”

org.hibernate.validator.constraints, org.hibernate.validator.constraints.br, org.hibernate.validator.constraints.pl

Hibernate Validator 提供的一些有用的自定义约束,以及 Jakarta Bean Validation 规范定义的内置约束; 这些约束在Section 2.3.2, “附加约束”中有详细描述。

org.hibernate.validator.constraintvalidation

扩展的约束验证器上下文,允许为消息内插设置自定义属性。Section 12.13.1, “HibernateConstraintValidatorContext 描述了如何使用该特性。

org.hibernate.validator.group, org.hibernate.validator.spi.group

组序列提供程序特性允许您根据验证的对象状态定义动态默认组序列; 具体内容可以在Section 5.4, “重新定义默认组序列”中找到。

org.hibernate.validator.messageinterpolation, org.hibernate.validator.resourceloading, org.hibernate.validator.spi.resourceloading

与约束消息插值相关的类; 第一个包包含 Hibernate Validator 的默认消息插值器 ResourceBundleMessageInterpolator 。后两个包提供 ResourceBundleLocator SPI,用于加载资源包(请参阅Section 4.2.1, “ResourceBundleLocator)及其默认实现。

org.hibernate.validator.parameternameprovider

基于 Paranamer 库的 ParameterNameProvider ,见Section 12.14, “基于 Paranamer 的 ParameterNameProvider

org.hibernate.validator.propertypath

jakarta.validation.Path 的扩展,参见Section 12.7, “拓展 Path API”

org.hibernate.validator.spi.constraintdefinition

用于以编程方式注册附加约束验证器的 SPI,请参阅Section 12.15, “提供约束定义”

org.hibernate.validator.spi.messageinterpolation

在插入约束违背消息时,可用于调整区域设置的分辨率的 SPI。参见Section 12.12, “自定义语言环境解析”

org.hibernate.validator.spi.nodenameprovider

一个 SPI,可用于在构造属性路径时更改属性名称的解析方式。请参阅Section 12.18, “自定义违反约束的属性名解析”

Hibernate Validator 的公共包分为两类: 实际的 API 部分是用来被用户程序 invoked(调用)used(使用) 的(例如用于编程约束声明的 API 或自定义约束) ,而 SPI (服务提供者接口)包含的接口是用来被用户程序 implemented(实现) 的(例如 ResourceBundleLocator )。

该表中没有列出的任何包都是 Hibernate Validator 的内部包,用户程序不能访问。这些内部软件包的内容可以在不通知的情况下从一个版本更改到另一个版本,因此可能会破坏任何依赖它的用户程序代码。

12.2. 快速失败模式

使用快速失败模式,Hibernate Validator 允许在发生第一个约束冲突时从当前校验返回。这对于大型对象图的校验非常有用,因为您只对快速校验是否存在任何约束冲突感兴趣。

Example 135, “使用快速失败模式” 展示了如何引导和使用失败快速启用的验证程序。

Example 135. 使用快速失败模式
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 约束。如果需要,可以使用Section 5.3, “定义分组序列”

请参阅Section 9.2.8, “Provider-specific 配置”,了解启动验证程序时启用失败快速模式的不同方式。

12.3. 放宽类层次中方法校验的要求

Jakarta Bean Validation 规范定义了一组前置条件,这些前置条件在定义类层次结构中方法的约束时适用。这些先决条件在 Jakarta Bean Validation 2.0规范的 section 5.6.5 。请参阅本指南中的Section 3.1.4, “继承层次中的方法约束”

根据规范,Jakarta Bean Validation 实现程序可以放松这些先决条件。使用 Hibernate Validator,您可以通过以下两种方式之一完成此操作。

首先,你可以使用配置属性 hibernate.validator.allow_parameter_constraint_overridehibernate.validator.allow_multiple_cascaded_validation_on_resulthibernate.validator.allow_parallel_method_parameter_constraintvalidation.xml 文件中。参见 Example 136, “通过属性在类层次结构中配置方法验证行为”

Example 136. 通过属性在类层次结构中配置方法验证行为
<?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>

或者,可以在编程引导启动期间应用这些设置。

Example 137. 在类层次结构中配置方法验证行为
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 提供了一个 fluent API,允许对约束进行编程配置。用例包括在运行时动态添加约束,这取决于一些应用程序状态或测试,在这些测试中,您需要在不同场景中具有不同约束的实体,但是不希望为每个测试用例实现实际的 Java 类。

默认情况下,通过 fluent API 添加的约束是通过标准配置功能配置的约束的附加物。但是也可以在需要时忽略注解和 XML 配置的约束。

API 以 ConstraintMapping 接口为中心。您可以通过 HibernateValidatorConfiguration#createConstraintMapping() 方法获得一个新的映射,然后以fluent的方式进行配置,如示例Example 138, “编程实现约束声明”所示。

Example 138. 编程实现约束声明
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 ,如Example 139, “自定义约束的编程声明”中所示。

Example 139. 自定义约束的编程声明
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
	.type( Car.class )
		.field( "licensePlate" )
			.constraint( new GenericConstraintDef<>( CheckCase.class )
				.param( "value", CaseMode.UPPER )
			);

使用 containerElementType() ,编程 API 支持容器元素约束。

Example 140, “嵌套容器元素约束的编程声明” 展示了一个在嵌套容器元素上声明约束的例子。

Example 140. 嵌套容器元素约束的编程声明
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)。请参阅 Example 141, “为级联校验标记属性”

Example 141. 为级联校验标记属性
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 ) );

不仅可以使用 fluent API 配置 bean 约束,还可以使用方法和构造函数约束。如Example 142, “方法和构造函数约束的编程声明” 构造函数通过其参数类型和方法的名称和参数类型来标识。选择了方法或构造函数之后,可以标记其参数和/或级联验证的返回值,并添加约束和交叉参数约束。

如示例所示,还可以对容器元素类型调用 valid()

Example 142. 方法和构造函数约束的编程声明
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();

最后但并非最不重要的是,您可以配置默认组序列或类型的默认组序列提供程序,如下面的示例所示。

Example 143. 默认组序列和默认组序列提供程序的配置
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 配置的默认工厂(参见Chapter 8, 通过 XML 配置) ,则可以通过创建一个或多个约束映射贡献者来添加一个或多个约束映射。为此,实现 ConstraintMappingContributor 契约:

Example 144. 自定义 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() );
	}
}

然后,您需要使用属性键 hibernate.validator.constraint_mapping_contributorsMETA-INF/validation.xml 中指定贡献者实现的完全限定类名。可以通过用逗号分隔多个贡献者来指定它们。

12.6. 高级约束组合特性

12.6.1. 纯组合约束的验证目标规范

如果您在方法声明中指定了一个纯粹的组合约束——即一个本身没有验证程序但仅由其他组合约束组成的约束——验证引擎无法确定该约束是作为返回值约束还是作为交叉参数约束应用。

Hibernate Validator 允许通过在组合约束类型的声明上指定 @SupportedValidationTarget 注释来解决这种模糊性,如Example 145, “指定纯组合约束的验证目标”@ValidInvoiceAmount 不声明任何验证器,但它完全由 @Min@NotNull 约束组成。 @SupportedValidationTarget 确保在方法声明中给出约束时,约束应用于方法返回值。

Example 145. 指定纯组合约束的验证目标
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 指定组合约束的约束(参见Section 6.4, “约束组合”)都通过逻辑 AND 进行组合。这意味着所有组合约束都需要返回 true 才能获得全面成功的验证。

Hibernate Validator 为此提供了扩展,并允许您通过逻辑 ORNOT 来组合约束。为此,必须使用 constraint composition 注释和 enum CompositionType 及其值 ANDORALL_FALSE

Example 146, “OR 约束的组合” 展示了如何构建一个组合约束 @PatternOrSize ,其中只有一个组合约束是有效的,才能通过验证。验证过的字符串要么全部采用小写格式,要么长度在两到三个字符之间。

Example 146. 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 节点,它允许获取表示的属性的值。为此,将给定节点缩小到 org.hibernate.validator.path.PropertyNode 类型。或 org.hibernate.validator.path.ContainerElementNode 。分别使用 Node#as() 作为 ,如下例所示:

Example 147. 从属性节点获取值
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

在某些情况下,如果违反约束提供了额外的数据——所谓的动态有效负载,则违反的自动处理可以得到帮助。例如,这个动态有效负载可能包含对用户如何解决冲突的提示。

可以使用 HibernateConstraintValidatorContextcustom constraints 中设置动态有效负载。在 Example 148, “ConstraintValidator 实现设置动态有效负载” ,其中 jakarta.validation.ConstraintValidatorContext 。为了调用 withDynamicPayload ,ConstraintValidatorContext 被解包到 HibernateConstraintValidatorContext

Example 148. 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 ,以便为进一步处理检索动态有效负载。

Example 149. 检索 ConstraintViolation's 动态有效载荷
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. 启用EL表达式语言功能

Hibernate 验证器限制默认情况下公开的表达式语言特性。

为此,我们在 ExpressionLanguageFeatureLevel 中定义了几个特征级别:

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

  • VARIABLES: 允许通过 addExpressionVariable() 插入变量、资源包和 formatter 程序对象的使用。

  • BEAN_PROPERTIES: 允许所有 VARIABLES 都允许加上 BEAN 属性的插值。

  • BEAN_METHODS: 也允许执行 BEAN 方法。这可能会导致严重的安全问题,包括如果不仔细处理就会任意执行代码。

根据上下文,我们展示的特性是不同的:

  • 对于约束,默认级别为 BEAN_PROPERTIES 。要正确插入所有内置约束消息,您至少需要 VARIABLES 级别。

  • 对于通过 ConstraintValidatorContext 创建的自定义违规,默认情况下禁用 Expression Language。您可以为特定的自定义违规启用它,当启用时,它将默认为 VARIABLES

Hibernate Validator 提供了在启动 ValidatorFactory 时覆盖这些默认值的方法。

要更改约束的 Expression Language 特性级别,请使用以下命令:

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.constraintExpressionLanguageFeatureLevel( ExpressionLanguageFeatureLevel.VARIABLES )
		.buildValidatorFactory();

要更改用于自定义违规的表达式语言特性级别,请使用以下命令:

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
		.configure()
		.customViolationExpressionLanguageFeatureLevel( ExpressionLanguageFeatureLevel.VARIABLES )
		.buildValidatorFactory();

这样做将自动为应用程序中的所有自定义违规启用表达式语言。

它应该只用于兼容性和简化从旧版本 Hibernate 验证器的迁移。

还可以使用以下属性定义这些级别:

  • hibernate.validator.constraint_expression_language_feature_level

  • hibernate.validator.custom_violation_expression_language_feature_level

这些属性的可接受值是: none, variables, bean-propertiesbean-methods.

12.10. ParameterMessageInterpolator

Hibernate Validator 要求每个默认情况下都有一个统一 EL 的实现(参见Section 1.1.1, “统一的EL表达式” )。这是允许使用 Jakarta Bean Validation 规范定义的 EL 表达式插值约束错误消息所需的。

对于不能或不希望提供 EL 实现的环境,Hibernate Validator 提供了一个非 EL 的消息内插器- org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator

请参阅Section 4.2, “自定义消息插值”,了解如何插入自定义消息插值器实现。

包含 EL 表达式的约束消息将通过 org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator 返回未插入的消息。这也会影响使用 EL 表达式的内置默认约束消息。目前, DecimalMinDecimalMax 受到影响。

12.11. ResourceBundleLocator

通过 ResourceBundleLocator ,Hibernate Validator 提供了一个额外的 SPI,它允许从 ValidationMessages 之外的其他资源包中检索错误消息,同时仍然使用规范定义的实际插值算法。请参阅Section 4.2.1, “ResourceBundleLocator,以了解如何使用该 SPI。

12.12. 自定义语言环境解析

这些合同被标记为 @Incubating ,因此它们在未来可能会发生变化。

Hibernate Validator 提供了几个扩展点来构建自定义语言环境解析策略。在插入违反约束的消息时使用已解析的区域设置。

Hibernate Validator 的默认行为是始终使用系统默认语言环境(通过 Locale.getDefault() 获得)。例如,如果您通常将系统区域设置为 en-US ,但希望应用程序提供法语消息,那么这可能不是理想的行为。

下面的示例演示如何将 Hibernate Validator 缺省语言环境设置为 fr-FR:

Example 150. 配置默认语言环境
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 中提取标头。

Example 151. 对用于通过 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 实例中创建消息插值的情况下,为所提供的上下文实例存在 unwrap() 方法—— ConstraintValidatorContext MessageInterpolatorContext。Hibernate Validator 为这两个接口提供自定义扩展。

12.13.1. HibernateConstraintValidatorContext

HibernateConstraintValidatorContextConstraintValidatorContext 的一个子类型,它允许你:

  • 启用表达式语言插值为一个特定的自定义冲突-见下面

  • 使用 HibernateConstraintValidatorContext#addExpressionVariable(String, Object)HibernateConstraintValidatorContext#addMessageParameter(String, Object) 通过 Expression Language 消息插值工具设置插值的任意参数。

    Example 152. 自定义 @Future 注入表达式变量的验证程序
    package org.hibernate.validator.referenceguide.chapter12.context;
    
    import java.time.Instant;
    
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    import jakarta.validation.constraints.Future;
    
    import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
    
    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;
    	}
    }
    Example 153. 自定义 @Future 注入消息参数的验证程序
    package org.hibernate.validator.referenceguide.chapter12.context;
    
    import java.time.Instant;
    
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    import jakarta.validation.constraints.Future;
    
    import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
    
    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() 的调用之间更新参数。

  • 设置一个任意的动态有效负载——参见 Section 12.8, “动态有效载荷作为 ConstraintViolation

默认情况下,表达式语言插值是 disabled 自定义违反,这是为了避免任意代码执行或敏感的数据泄漏,如果消息模板是从不正确的转义用户输入构建的。

通过使用 enableExpressionLanguage() ,可以为给定的自定义违规启用 Expression Language,如下面的示例所示:

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

在这种情况下,消息模板将由 Expression Language 引擎插入。

默认情况下,启用表达式语言时只启用变量插值。

你可以通过使用 HibernateConstraintViolationBuilder#enableExpressionLanguage(ExpressionLanguageFeatureLevel level) 来启用更多功能。

我们为表达式语言插值定义了几个级别的特性:

  • NONE: 表达式语言插值是完全禁用-这是默认的自定义违反。

  • VARIABLES: 允许通过 addExpressionVariable() 插入变量、资源包和 formatter 程序对象的使用。

  • BEAN_PROPERTIES: 允许所有 VARIABLES 都允许加上 BEAN 属性的插值。

  • BEAN_METHODS: 也允许执行 BEAN 方法。这可能会导致严重的安全问题,包括如果不仔细处理就会任意执行代码。

使用 addExpressionVariable() 是将变量注入到表达式中的唯一安全方法,如果使用 BEAN_PROPERTIESBEAN_METHODS 特性级别,这一点尤为重要。

如果你通过简单地将用户输入连接到消息中来注入用户输入,你将允许潜在的任意代码执行和敏感的数据泄漏: 如果用户输入包含有效的表达式,它们将被表达式语言引擎执行。

这里有一个你 ABSOLUTELY NOT(绝对不能做) 的事情的例子:

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 (参见Example 154, “HibernateMessageInterpolatorContext )。引入这个子类型是为了将 Hibernate Validator 更好地集成到 Glassfish 中。在这种情况下,需要根 bean 类型来确定消息资源束的正确类装入器。如果您有任何其他用例,请让我们知道。

Example 154. 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 附带了一个利用 Paranamer 库的 ParameterNameProvider 实现。

这个库提供了几种在运行时获取参数名的方法,例如,基于 Java 编译器创建的调试符号,在编译后的步骤中将带有参数名的常量编织到字节码中,或者像 JSR 330的 @Named 注释这样的注释。

为了使用 ParanamerParameterNameProvider ,要么在引导验证程序时传递一个实例,如Example 107, “使用自定义 ParameterNameProvider ,要么指定 org.hibernate.validator.parameternameprovider.ParanamerParameterNameProvider 作为 META-INF/validation.xml 文件中 <parameter-name-provider> 元素的值。

使用此参数名提供程序时,需要将 Paranamer 库添加到类路径中。它可以在 Maven Central 存储库中获得,该存储库的组 id 为 com.thoughtworks.paranamer ,工件 id 为 paranamer

默认情况下, ParanamerParameterNameProvider 从构建时添加到字节码的常量(通过 DefaultParanamer )和调试符号(通过 BytecodeReadingParanamer )中检索参数名。或者,您可以在创建 ParanamerParameterNameProvider 实例时指定所选择的 Paranamer 实现。

12.15. 提供约束定义

Jakarta Bean Validation 允许(重新)通过 XML 在其约束映射文件中定义约束定义。有关更多信息,请参阅Section 8.2, “通过 constraint-mappings 映射约束” ,并参阅Example 96, “通过 XML 配置 Bean 约束” 。虽然这种方法对于许多用例来说已经足够了,但是在其他用例中它还是有缺点的。例如,假设有一个约束库希望为自定义类型提供约束定义。这个库可以提供一个带有它们库的映射文件,但是这个文件仍然需要库的用户引用。幸运的是,还有更好的方法。

下面的概念目前被认为是实验性的。让我们知道你是否认为他们有用,他们是否满足你的需要。

12.15.1. 通过 ServiceLoader 实现约束的定义

Hibernate Validator 允许利用 Java 的 ServiceLoader 机制来注册附加的约束定义。您所要做的就是添加 jakarta.validation.ConstraintValidator 文件。到 META-INF/services 。在此服务文件中,列出约束验证器类的完全限定类名(每行一个)。Hibernate Validator 将自动推断它们应用于的约束类型。请通过Constraint definition via service file 查看约束定义的示例。

Example 155. META-INF/services/jakarta.validation.ConstraintValidator
# Assuming a custom constraint annotation @org.mycompany.CheckCase
org.mycompany.CheckCaseValidator

要为自定义约束贡献默认消息,请在 JAR 的根目录中放置一个文件 ContributorValidationMessages.properties 和/或其特定于区域设置的专门化。Hibernate Validator 将考虑除 ValidationMessages.properties 中给出的条目之外,在类路径中发现的所有绑定包中具有此名称的条目。

这种机制在创建大型多模块应用程序时也很有帮助: 您可以在每个模块中使用一个资源包,而不是将所有约束消息放到一个包中,而只包含该模块的那些消息。

我们强烈推荐阅读 this blog post by Marko Bekhta ,指导你一步一步地创建一个包含自定义约束并通过 ServiceLoader 声明它们的独立 JAR。

12.15.2. 以编程方式添加约束定义

虽然服务加载器方法可以在许多场景中工作,但并不适用于所有场景(例如,在服务文件不可见的 OSGi 中) ,但还有另一种贡献约束定义的方法。可以使用编程式约束声明 API ——请参见Example 156, “通过编程 API 添加约束定义”

Example 156. 通过编程 API 添加约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
		.constraintDefinition( ValidPassengerCount.class )
		.validatedBy( ValidPassengerCountValidator.class );

如果你的验证器实现相当简单(比如不需要注释的初始化,也不需要 ConstraintValidatorContext ) ,你也可以使用这个替代 API 来指定使用 Lambda 表达式或方法引用的约束逻辑:

Example 157. 使用 Lambda 表达式添加约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
		.constraintDefinition( ValidPassengerCount.class )
			.validateType( Bus.class )
				.with( b -> b.getSeatCount() >= b.getPassengers().size() );

与直接向配置对象添加约束映射不同,您可以使用 ConstraintMappingContributor ,详见Section 12.5, “将程序化的约束声明应用到默认的验证器工厂” 。这在使用 META-INF/validation.xml 配置默认验证器工厂时非常有用(参见 Chapter 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) ,你可以在启动验证器工厂时提供一个特定的类加载器来加载这些资源:

Example 158. 提供一个类加载器来加载外部资源和类
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 尊重 javabean 规范,只要下面的条件之一是真实的,Hibernate Validator 就会考虑一个方法作为 getter:

  • 方法名以 get 开头,具有非空返回类型且没有参数;

  • 方法名以 is 开头,具有 boolean 的返回类型,且没有参数;

  • 方法名以 has 开头,返回 boolean 类型,没有参数(这个规则是特定于 Hibernate Validator 的,javabean 规范没有强制要求)

虽然这些规则在遵循经典 JavaBeans 约定时通常是适当的,但是它可能会发生,特别是在代码生成器中,JavaBeans 变数命名原则不遵循,而 getter 的名称遵循不同的约定。

在这种情况下,应该重新定义检测 getter 的策略,以便完全验证对象。

这个需求的一个典型例子是当类遵循一个流畅的变数命名原则时,如Example 159, “使用非标准 getter 的类”

Example 159. 使用非标准 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 执行验证,因为标准策略没有检测到它们。

Example 160. 使用默认 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 。在这种特殊情况下,可能执行的战略是:

Example 161. 自定义 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 Set<String> getGetterMethodNameCandidates(String propertyName) {
		// As method name == property name, there always is just one possible name for a method
		return Collections.singleton( propertyName );
	}
}

有多种方法可以配置 Hibernate Validator 来使用这个策略。它可以通过编程方式完成(参见Example 162, “以编程方式配置自定义 GetterPropertySelectionStrategy ) ,也可以在 XML 配置中使用 hibernate.validator.getter_property_selection_strategy 属性(参见Example 163, “使用 XML 属性配置自定义 GetterPropertySelectionStrategy )。

Example 162. 以编程方式配置自定义 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() );
Example 163. 使用 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 约束:

Example 164. 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:

Example 165. 将 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 );
	}
}

正如我们看到的,对象被序列化为:

Example 166. Person as json
{
  "first_name": "Clark",
  "last_name": "Kent"
}

注意属性的名称是如何不同的。在 Java 对象中,我们有 firstNamelastName ,而在 JSON 输出中,我们有 first_namelast_name 。我们通过 @JsonProperty 注释定制了这种行为。

现在假设我们在 REST 环境中使用这个类,其中用户可以在请求体中以 JSON 的形式发送 a Person instance as JSON 。在指示验证失败的字段时,最好指出他们在 JSON 请求中使用的 first_name ,而不是我们在内部 Java 代码中使用的名称,firstName

org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider 契约允许我们这样做。通过实现它,我们可以定义在验证期间如何解析属性的名称。在我们的示例中,我们希望从 Jackson 配置中读取值。

如何做到这一点的一个例子是利用 Jackson API:

Example 167. 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();
	}
}

在进行验证时:

Example 168. 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 上时,这也是可行的:

Example 169. 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() 方法,可用于获取属性的“original”名称。这个接口应该用作解析名称的默认方式(参见Example 167, “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 Central 等常见的存储库 GAV org.hibernate.validator:hibernate-validator-annotation-processor:7.0.1.Final 中找到 Hibernate Validator Annotation Processor 作为发行包的一部分。

13.1. 先决条件

Hibernate Validator 注解处理器基于 JSR 269 定义的"Pluggable Annotation Processing API", JSR 269 是 Java 平台的一部分。

13.2. 功能

从 Hibernate Validator 7.0.1.Final 开始,Hibernate Validator 注解处理器检查如下:

  • 在允许注解元素的类型上使用约束注解

  • 只在非静态字段或方法用约束注解

  • 只在非原始字段或方法用 @Valid 注解

  • 只有在 JavaBeans 的 getter 方法上用约束注解,这些约束注解是有效的(可选地,参见下面)

  • 只有这样的注解类型用约束注解,这些约束注解本身就是约束注解

  • 使用 @GroupSequenceProvider 定义动态默认组序列是有效的

  • 注解参数值是有意义和有效的

  • 继承层次结构中的方法参数约束尊重继承规则

  • 方法在继承层次结构中返回值约束遵循继承规则

13.3. 选项

Hibernate Validator Annotation Processor 的行为可以通过以下 processor options 来控制:

diagnosticKind

控制如何报告约束问题。必须是 enum javax.tools.Diagnostic.Kind 中某个值的字符串表示形式。例如 WARNING 。每当 AP 检测到约束问题时,ERROR 值将导致编译停止。默认为 ERROR

methodConstraintsSupported

控制在任何类型的方法中是否允许约束。使用 Hibernate Validator 支持的方法级别约束时,必须将其设置为 true 。可以将其设置为 false ,以便只允许在由 Jakarta Bean Validation API 定义的 javabean getter 方法中存在约束。默认为 true

verbose

控制是否显示详细的处理信息,这对调试有用。一定是 true 的 或 false 。默认为 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 选项设置如下:

Example 170. 在 Maven 中使用 Hibernate Validator 注解处理器
<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>7.0.1.Final</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            [...]
        </plugins>
        [...]
    </build>
    [...]
</project>
13.4.1.2. Gradle

当使用 Gradle 时,将注解处理器作为一个 annotationProcessor 依赖项引用就足够了。

Example 171. 使用 Gradle 的注解处理器
dependencies {
	annotationProcessor group: 'org.hibernate.validator', name: 'hibernate-validator-annotation-processor', version: '7.0.1.Final'

	// any other dependencies ...
}
13.4.1.3. Apache Ant

与直接使用 javac 类似,在调用 Apache Antjavac task ,可以将注解处理器作为编译器参数添加:

Example 172. 使用 Ant 的注解处理器
<javac srcdir="src/main"
       destdir="build/classes"
       classpath="/path/to/validation-api-3.0.0.jar">
       <compilerarg value="-processorpath" />
       <compilerarg value="/path/to/hibernate-validator-annotation-processor-7.0.1.Final.jar"/>
</javac>
13.4.1.4. javac

在命令行上使用 javac 进行编译时,使用“ processorpath”选项指定 JAR hibernate-validator-annotation-processor-7.0.1.Final.jar ,如下面的清单所示。编译器将自动检测处理器并在编译期间调用它。

Example 173. 使用带 javac 的注解处理器
javac src/main/java/org/hibernate/validator/ap/demo/Car.java \
   -cp /path/to/validation-api-3.0.0.jar \
   -processorpath /path/to/hibernate-validator-annotation-processor-7.0.1.Final.jar

13.4.2. IDE builds

13.4.2.1. Eclipse

如果您已经安装了 M2E Eclipse plug-in 插件,那么将为上述配置的 Maven 项目自动设置注解处理器。

对于普通的 Eclipse 项目,按照以下步骤设置注解处理器:

  • 右键单击项目,选择 "Properties"

  • 进入 "Java Compiler" ,确保 "Compiler compliance level" 设定为 "1.8" 。否则处理器将不会被激活

  • 进入 "Java Compiler - Annotation Processing" 并选择 "Enable annotation processing"

  • 转到 "Java Compiler - Annotation Processing - Factory Path" ,添加 JAR hibernate-validator-annotation-processor-7.0.1.Final.jar

  • 确认并重新构建

现在,您应该可以在编辑器和 "Problem" 视图中看到任何注解问题,它们都是常规的错误标记:

annotation processor eclipse
13.4.2.2. IntelliJ IDEA

使用 IntelliJ IDEA (9及以上版本)中的注解处理器必须遵循以下步骤:

  • 点击 "File", 然后是 "Settings",

  • 展开 "Compiler", 选择 "Annotation Processors"

  • 选择 "Enable annotation processing" 然后在 "Processor path" 输入: /path/to/hibernate-validator-annotation-processor-7.0.1.Final.jar

  • 添加处理器的标准名称 org.hibernate.validator.ap.ConstraintValidationProcessor 到“Annotation Processors”列表

  • 如果适用,将模块添加到"Processed Modules"列表中

然后,重建项目应显示任何错误的约束注释:

annotation processor intellij
13.4.2.3. NetBeans

NetBeans 支持在IDE内部使用注解处理器,为此,请执行以下操作:

  • 右键单击您的项目,选择 "Properties"

  • 转到 "Libraries", 选项卡 "Processor", 然后添加 JAR 包hibernate-validator-annotation-processor-7.0.1.Final.jar

  • 转到 "Build - Compiling", 选择 "Enable Annotation Processing" 和 "Enable Annotation Processing in Editor"。 通过指定其标准名称 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 的 tests 可能会引起人们的兴趣。 The Jakarta Bean Validation 规范本身也是加深您对 Jakarta Bean Validation和 Hibernate Validator 的理解的一个很好的方法。

如果你对 Hibernate Validator 有任何进一步的问题,或者想要分享你的一些用例,可以看看 Hibernate Validator WikiHibernate Validator ForumHibernate Validator tag on Stack Overflow

如果您想报告一个 bug,请使用 Hibernate’s Jira 。欢迎反馈!