Hibernate Validator 中文操作指南

7. Value extraction

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

7.1. Built-in value extractors

Hibernate Validator 为通常的 Java 容器类型提供了内置值抽取器,因此,除非您使用自己的自定义容器类型(或外部库的类型,例如 Guava's Multimap),否则您不必添加自己的值抽取器。

下列容器类型均具有内置值提取器:

  1. java.util.Iterable;

  2. java.util.List;

  3. java.util.Map:适用于键和值;

  4. java.util.Optional, java.util.OptionalInt, java.util.OptionalLongjava.util.OptionalDouble

  5. JavaFXObservableValue(有关详细信息,请参见 Section 7.4, “JavaFX value extractors”)。

可以在 Jakarta Bean Validation specification 中找到内含内置值抽取器的完整列表以及它们的行为详情。

7.2. Implementing a ValueExtractor

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

实现 ValueExtractor 不够,您还需要注册它。有关更多详细信息,请参见 Section 7.5, “Registering a ValueExtractor

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

例如,让我们考虑 Guava 的 Optional 案例。这是一个简单的示例,因为我们可以针对 java.util.Optional 一个塑造其值提取器:

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

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

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

需要一些解释:

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

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

  3. 我们不想向约束违规的属性路径中添加节点,因为我们希望违规报告为直接在属性上,所以我们向 value() 传递 null 节点名称。

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

我们首先来考虑值的情况。需要提取它们的提取器:

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

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

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

它允许您为 Multimap 的值验证约束:

  1. 示例 7.3: Multimap 值的约束

private Multimap<String, @NotBlank String> map1;

要能够对 Multimap 的键施加约束,还需要另一个提取器:

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

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

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

一旦注册了这两个值提取器,便可以在 Multimap 的键和值上声明约束:

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

这两者的区别乍一看可能有些微妙,因此我们对它们进行一些说明:

  1. @ExtractedValue 注释标记了目标类型参数(在此情况下为 KV)。

  2. 我们使用不同的节点名称(&lt;multimap key&gt; 对比 &lt;multimap value&gt;)。

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

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

value()

用于简单的包装容器 - 用作 _Optional_s

iterableValue()

用于可迭代容器 - 用作 _Set_s

indexedValue()

用于包含索引值的容器 - 用作 _List_s

keyedValue()

用于包含键控值的容器 - 用作 _Map_s。它同时用于键和值。对于键,键也作为验证值传递。

对于以上所有方法,您需要传递一个节点名称:它是包含在约束违规属性路径中的节点的名称。如前所述,如果节点名称是_null_,则不会向属性路径中添加任何节点:这对于类似于_Optional_的纯包装器类型非常有用。

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

7.3. Non generic containers

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

Hibernate 验证工具还支持对非泛型容器的值提取。

我们来看看_java.util.OptionalInt_的情况,它将原始_int_包装到_Optional_类容器中。

第一次尝试_OptionalInt_的值提取器可能如下所示:

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

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

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

对于非泛型容器来说,缺少一个明显的东西:我们没有类型参数。它有两个结果:

  1. 我们无法使用类型参数来确定验证值的类型;

  2. 我们无法对类型参数添加约束(例如 Container&lt;@NotNull String&gt;)。

首先,我们需要一种方法来告诉 Hibernate 验证工具从_OptionalInt_中提取的值的类型为_Integer_。如您在上面的示例中所见,_@ExtractedValue_注释的_type_属性允许向验证引擎提供此信息。

然后,您必须告诉验证引擎,您想要添加到_OptionalInt_属性的_Min_约束与包装的值相关,而不是包装本身。

Jakarta Bean 验证为此情况提供了_Unwrapping.Unwrap_负载:

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

如果我们退一步,我们希望添加到_OptionalInt_属性的大多数(如果不是全部)约束将应用于包装值,因此,找到一种方法使其成为默认值将很好。

这正是_@UnwrapByDefault_注释的用途:

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

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

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

在为_OptionalInt_声明此值提取器时,约束注释将默认应用于包装值:

示例 7.9:由于 @UnwrapByDefault 而隐式拆箱
@Min(5)
private OptionalInt optionalInt2;

请注意,您仍然可以使用_Unwrapping.Skip_负载为容器本身声明注释:

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

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

7.4. JavaFX value extractors

JavaFX 中的 Bean 属性通常不是像 Stringint 这样的简单数据类型,而是包装在 Property 类型中,以便使其可观察、用于数据绑定等。

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

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

因此,您可以按如下方式限制 StringProperty

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

LongProperty

示例 7.12:约束 LongProperty
@Min(5)
private LongProperty longProperty;
  • 可迭代属性类型,即ReadOnlyListPropertyListProperty及其SetMap对应类型是通用的,因此可以使用容器元素约束。因此,它们有未标记为@UnwrapByDefault**的特定值提取器。

自然会将ReadOnlyListProperty约束为List

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

7.5. Registering a ValueExtractor

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

注册值提取器有几种方法(按优先级升序):

Provided by the validation engine itself

Via the Java service loader mechanism

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

In the META-INF/validation.xml file

有关如何在 XML 配置中注册值提取器的更多信息,请参阅 Section 8.1, “Configuring the validator factory in validation.xml

By calling Configuration#addValueExtractor(ValueExtractor<?>)

有关更多信息,请参阅 Section 9.2.6, “Registering _ValueExtractor_s”

By invoking ValidatorContext#addValueExtractor(ValueExtractor<?>)

它仅声明该 Validator 实例的值提取器。

给定类型的优先级和类型参数的值提取器覆盖对相同类型和低优先级类型参数的任何其他提取器。

7.6. Resolution algorithms

在大多数情况下,您不必担心这一点,但如果您要覆盖现有的值提取器,可以在 Jakarta Bean 验证规范中找到值提取器解析算法的详细描述:

要注意的一件重要事情是:

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

  2. 对于级联验证,它就是运行时类型。