Bean Validation 2.0 (JSR 380) 最重要的功能是对容器元素约束的支持。也就是说,现在你可以通过注解类型参数(这得益于Java 8)来将约束应用于容器类型的内容,例如 ListMapOptionalList<@Future LocalDate> shipmentDates

在这篇博客文章中,你将了解如何利用这一点来验证自定义容器类型,例如来自Google广受欢迎的 Guava库 中的MultimapTableGraph

作为一个例子,让我们考虑一个Person类,它应该能够保存多种类型的多个电子邮件地址(例如,两种类型的“工作”地址和两种“私人”地址)。这种用例可以很好地使用Guava的Multimap类型来建模,该类型允许为单个键存储多个值

public class Person {

    public Multimap<String, String> emailsByType;

    // constructor etc.
}

为了确保只存储有效数据,让我们放置一些约束

public Multimap<@NotBlank String, @NotBlank @Email String> emailsByType;

这应该确保如果emailsByType映射包含

  • 任何空白字符串作为键

  • 任何空白字符串或不是有效电子邮件地址的字符串作为值

现在让我们看看如果我们验证一个Person实例会发生什么

Person bob = new Person();
bob.emailsByType.put( "work", "bob@example.com" );
bob.emailsByType.put( "work", "not-an-email" );
bob.emailsByType.put( "private", "bob@home.com" );

Validator validator = Validation.buildDefaultValidatorFactory()
                .getValidator();

Set<ConstraintViolation<Bean>> violations = validator.validate ( bob );

我们应该得到一条关于第二个“工作”电子邮件的约束违规(它不是一个有效的电子邮件地址),对吗?

不幸的是,情况并非如此;相反,抛出了一个异常

javax.validation.ConstraintDeclarationException: HV000197:
No value extractor found for type parameter 'K' of type com.google.common.collect.Multimap.

这是Hibernate Validator告诉我们它检测到应用于Multimap<K>类型参数)的类型参数的约束,但它缺乏从该容器中获取要验证的值(在这种情况下是映射键)的信息。

从规范的角度来看,这是有意义的。虽然Bean Validation规范定义了对JDK集合类型的内置支持,但它不能对由Guava等提供的自定义容器类型做出假设,更不用说仅在你项目中定义的具体容器类型。

但上面的异常信息指向了正确的方向:规范不是仅仅强制支持固定数量的容器类型,而是定义了用于检索容器元素的值提取器SPI。通过插入您使用的自定义容器类型的提取器,您可以为它们添加约束,Bean Validation提供者将调用该SPI来获取容器元素以便进行验证。

多值映射的值提取器

因此,让我们利用这个SPI来支持Guava的Multimap键和值的Bean Validation约束。

对于每个应该能够进行约束的类型参数,需要一个实现javax.validation.valueextraction.ValueExtractor的实例。让我们首先创建用于多值映射值的提取器。

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

    @Override
    public void extractValues(Multimap<?, ?> originalValue, ValueReceiver receiver) {
        // TODO
    }
}

ValueExtractor接口用从其中提取的类型参数进行了参数化(在我们的例子中是Multimap)。由于容器可能支持多个类型参数的约束,因此使用@ExtractedValue注解来标记该提取器所处理的类型参数。

该接口仅定义了一个方法,即extractValues()。在这里,我们需要实现从容器中获取与提取器处理的类型参数对应的元素的逻辑。每个这样的元素都应传递给给定ValueReceiver对象的适当方法。

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

ValueReceiver提供了多个方法,例如keyedValue()indexedValue()等。必须为容器的每个元素调用其中一个方法。所有接收器方法都接受一个节点名称(如果验证产生任何ConstraintViolation,它将被用于相应的属性路径)和元素值。我们的实现遍历Multimap条目,并为每个条目将字符串文字<multimap value>和条目值传递给接收器。

根据提取器调用的接收器方法,结果ConstraintViolation中的属性路径节点也将从Node#getKey()返回一个键或从Node#getIndex()返回一个集合索引。应该调用哪个接收器方法取决于容器类型的语义。如果它支持通过索引访问(例如List),则应调用indexedValue()。对于具有键式访问的容器(例如MapMultimap),则应使用keyedValue()。对于其他多值容器(例如Iterable),应调用iterableValue(),最后,对于任何单值容器(例如Optional),只需调用value()即可。

类似于多值映射值的提取器,我们还声明了一个用于其键的提取器。

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

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

在这种情况下,@ExtractedValue注解标记了Multimap的类型参数<K>,并且提取器将映射键传递给接收器作为验证值。

注册值提取器

创建了两个用于Multimap的值提取器后,它们必须注册到Bean Validation引擎中。有多种方法可以实现这一点(例如,我们可以在程序创建ValidatorFactory时将它们传递给启动API),但最方便的方法是依赖于服务加载器机制。

为此,我们只需声明一个名为META-INF/services/javax.validation.valueextraction.ValueExtractor的文件,并将我们的自定义提取器实现的完全限定名称作为内容提供。

com.example.MultimapKeyExtractor
com.example.MultimapValueExtractor

Bean Validation提供者将自动拾取以这种方式注册的所有提取器实现。

最后,让我们再次运行我们的示例,看看产生的ConstraintViolation及其属性路径看起来如何。(示例中的所有断言都是真实的)

Person bob = new Person();
bob.emailsByType.put( "work", "bob@example.com" );
bob.emailsByType.put( "work", "not-an-email" );

Validator validator = Validation.buildDefaultValidatorFactory()
    .getValidator();

Set<ConstraintViolation<Bean>> violations = validator.validate (bean );
assert violations.size() == 1;

// one violation of the @Email constraint
ConstraintViolation<Bean> violation = violations.iterator().next();
assert violation.getInvalidValue().equals( "not-an-email" );
assert violation.getConstraintDescriptor().getAnnotation().annotationType().equals( Email.class );

Iterator<Node> pathNodes = violation.getPropertyPath().iterator();
assert pathNodes.hasNext() == true;

// first property path node
Node node = pathNodes.next();
assert node.getName().equals( "emailsByType" );
assert node.getKind() == ElementKind.PROPERTY;

assert pathNodes.hasNext() == true;

// second node
node = pathNodes.next();
assert node.getName().equals( "<multimap value>" );
assert node.getKind() == ElementKind.CONTAINER_ELEMENT;
assert node.getKey().equals( "work" );

assert pathNodes.hasNext() == false;

特别关注属性路径中的第二个节点。它属于CONTAINER_ELEMENT类型,并返回我们在值提取器中传递的名称和键。可以通过ConstraintViolation#getInvalidValue()方法获取无效元素的值。

摘要

虽然Bean Validation 2.0自带了对许多容器类型的支持无需额外配置(除了JDK集合类型外,还有对Optional和JavaFX属性类型的支持),但通过实现ValueExtractor SPI,也非常容易添加对其他自定义容器类型的支持。

要了解更多信息,请参阅Hibernate Validator参考指南中的值提取章节。它讨论了一些更高级的情况(例如对非泛型容器的支持)以及所有注册自定义提取器的方法。

您可以在我们的演示存储库中找到这篇博客文章的完整示例和源代码。如果您对值提取器有任何疑问,请在下面的评论中告诉我们。


回到顶部