这是关于 Bean Validation 规范(JSR 303)的一系列博客条目的第三部分。如果您还没有读过,请先阅读 第一部分第二部分

Bean Validation 规范允许您在领域模型上定义约束。在某些情况下,需要验证约束的子集。

在向导风格屏幕上,信息随着时间的推移从屏幕到屏幕部分提炼。验证每个屏幕上的数据是有用的,但从整体来看,数据尚未准备好进行完整的约束验证。

某些约束需要在其他约束之后运行。有几种用例需要排序约束

  • 高级约束可能期望在触发自身之前由基本约束预先验证数据
  • 某些约束在 CPU、内存、时间甚至金钱上都很昂贵。需要在运行之前约束并快速失败
  • 某些用例需要验证其他用例不需要的附加或特定约束

为了解决这类问题,Bean Validation 规范使用了分组的概念。每个约束都可以定义它所属的约束组集。在验证对象图时,可以请求一个或多个组。

原则

让我们首先描述分组背后的理论

将约束声明为属于一个组

每个约束都有一个groups元素,其默认值是一个空数组。当此元素在约束注解上未显式声明时,假定默认组

public class Address {
        //not null belongs to the "default" group
        @NotNull String street1;
}

任何约束声明都可以定义一个或多个它所属的组。请注意,“默认”是特殊的默认组。

public class Address {
        @NotNull(groups={"basic", "default"}) private String street1;
}

在这个例子中,@NotNull当请求基本默认组时都会被检查。

定义约束排序

为了执行顺序而对顺序约束进行限制并不太多意义,因为Bean Validation实现最终会执行所有约束并返回所有失败(对于一组给定的组)。然而,如果其他约束失败,则避免执行约束的能力是有用的。

Bean Validation规范使用组序列的概念。组序列声明一组组。每个组必须按列表顺序依次验证。验证一个组意味着验证属于该组的对象图中的所有约束。如果给定组中有一个或多个约束失败,则不会处理组序列中的后续组。

组序列使用以下方式声明:@GroupSequence并应用于设置的类。

@GroupSequence(name="default", sequence={"basic", "complex"})
@AddressChecker(groups="complex")
public class Address {
        @NotNull(groups="basic") private String street1;
        ...
}

在对象图(@Valid)中,子对象继承父对象的组序列。@GroupSequences用于在类中声明多个序列。组序列可以包含另一个组序列的名称。

验证一个组

默认情况下,validate方法验证默认组。您可以选择验证一个或多个组

validator.validate(address, "basic");
validator.validate(address, "firstscreen", "secondscreen");

传递给validate方法的组不会按特定顺序处理(如果需要顺序,请使用组序列)。

实际用途

理论已经足够,让我们看看组和组序列如何解决我们的用例。

特定用例的验证

根据特定用例定义约束有时是有用的。一个典型的例子是,在不同的上下文中,为了不同的目的,重复使用同一个对象模型。

在我们的例子中,账户可以处于三种状态:默认、canbuy和oneclick。一个canbuy账户有足够的信息来购买商品,一个oneclick账户可以...嗯,一键购买。

public class Account {

        @NotEmpty @Length(max=50) private String firstname;
        @Length(max=50) private String middleName;
        @NotEmpty @Length(max=50) private String lastname;
        
        @NotNull(groups="canbuy") @Phone private String cellPhone;

        @Valid @NotEmpty(groups="canbuy")
        private Set<Address> addresses;

        @Valid @NotNull(groups="canbuy")
        private Address defaultAddress;
        
        @Valid @NotEmpty(groups="canbuy")
        private Set<PaymentMethod> paymentMethod;

        @Valid @NotNull(groups="oneclick")
        private Address defaultPaymentMethod;

}

让我们分析一下前面的例子。一个默认账户必须有名字和姓氏。如果它有地址、支付方式和电话号码,则每个这些属性都必须有效。然而,这些属性不需要填写才能有效。

一个账户可以购买,如果它有一个默认地址和至少一种支付方式。一个账户可以一键购买,如果它有一个默认支付方式。

我们可以想象以下逻辑

Set<InvalidConstraint> accountIssues = validator.validate(account);
if ( accountIssues.size() > 0 ) {
        //push that error list to the user
}
else {
        if ( validator.validate(account, "canbuy").size() == 0 ) {
                enableBuyButton();
                if ( validator.validate(account, "oneclick").size() == 0 ) {
                        enableOneClick();
                }
        }
}

数据一致性通过默认组来保证。并且一些附加的状态检查由附加的组提供。请注意,在集成的世界中,您的应用程序框架或Web框架会允许您在给定的页面上下文、页面、会话等中声明需要验证的组。应用程序避免了手动执行验证的额外工作。

部分数据验证:向导屏幕模型

在向导样式的用户界面中,数据是部分提供的,当用户从一个向导屏幕移动到另一个时,数据变得越来越完整。

让我们拿前面的例子,并想象一个从零开始构建一键账户的向导

  • 第一个屏幕获取名字、姓氏和电话
  • 第二个屏幕添加一个或多个地址
  • 第三个屏幕添加一个或多个支付方式
  • 第四个屏幕确保用户选择默认地址和支付方式,并且一切设置为一键流程
  • 第五个屏幕是摘要,确保一切就绪
public class Account {

        @NotEmpty(groups={"firstscreen", "default"}) 
        @Length(max=50, groups={"firstscreen", "default"}) 
        private String firstname;
        
        @NotEmpty(groups={"firstscreen", "default"})
        @Length(max=50, groups={"firstscreen", "default"}) 
        private String lastname;
        
        @NotNull(groups={"firstscreen", "canbuy"}) 
        @Phone(groups={"firstscreen", "default"})
        private String cellPhone;

        @Valid @NotEmpty(groups={"secondscreen", "canbuy"})
        private Set<Address> addresses;

        @Valid @NotNull(groups={"forthscreen", "canbuy"})
        private Address defaultAddress;
        
        @Valid @NotEmpty(groups={"thirdscreen", "canbuy"})
        private Set<PaymentMethod> paymentMethod;

        @Valid @NotNull(groups={"forthscreen", "oneclick"})
        private Address defaultPaymentMethod;

}

对于最后一个屏幕以外的每个屏幕,都设置了一个特定的组。最后一个屏幕需要确保账户对于一键操作是有效的。然后它需要验证默认, canbuyoneclick组。

虽然可以通过编程方式调用组验证,但我们期望应用程序框架和Web框架允许您声明性地定义目标组。

在JSF中,它可能看起来像

<s:validateAll groups="firstscreen">
  <s:decorate>
    <h:inputText id="firstname" value="#{account.firstname}" />
    <h:message for="firstname" styleClass="error" />
    <br/>
    <h:inputText id="lastname" value="#{account.lastname}" />
    <h:message for="lastname" styleClass="error" />
    <br/>
    <h:inputText id="cellPhone" value="#{account.cellPhone}" />
    <h:message for="cellPhone" styleClass="error" />
  </s:decorate>
</s:validateAll>

这只是一个提案/愿景。此集成不是JSR 303规范的一部分。

约束排序

另一个用例是当一组约束失败时,能够停止约束验证。这在(至少)以下两种情况下非常有用:

  • 某些约束验证(尤其是类级约束)期望在预验证的约束上工作(例如,使用nullability、长度和全局模式匹配进行预验证):基本级别约束必须有效,然后才能调用更高级别的约束检查。
  • 某些约束运行起来很昂贵(长时间处理、访问外部资源等)

验证地址可以相当简单(一些基本约束),尤其是在针对一个国家时。它也可能更加复杂,并涉及地址一致性检查(邮编必须与城市匹配,街道名称必须与邮编匹配等)。在基本约束有效且期望一些预标准化数据之前,无法真正应用地址一致性检查。此外,我们的应用程序必须调用外部服务来检查一致性。结果是调用此服务有两个缺点:

  • 它有点慢(至少比其他约束验证慢)
  • 每次执行都要收费

我们想确保只有在其他约束有效时,才调用地址一致性约束。

@GroupSequence(name="default", sequence={"basic", "complex"})
@Address(groups="complex")
public class Address {
    @NotNull(groups="basic") @Max(50, groups="basic")) private String street1;
    @Max(50, groups="basic")) private String street2;
    @Max(10, groups="basic")) @NotNull(groups="basic") private String zipCode;
    @Max(20, groups="basic")) @NotNull(groups="basic") String city;
    @NotNull(groups="basic") private Country country;
    
    ...
}

组序列将首先验证标记为基本的约束,然后验证标记为复杂的约束除非组中的一个或多个约束失败。基本

结论

在大多数情况下,默认组就足够了。但对于更复杂的用例,需要在约束定义和验证方面有更多的灵活性。虽然声明组是应用程序开发人员的责任,但验证给定的组集很可能是由客户端框架(应用程序框架、Web框架以及在一定程度上是持久性框架)驱动的。无论是使用方法注解还是模板声明,WebBeans或JSF之类的框架都将捕获请求的约束验证组,并执行验证调用。

本规范的这一部分可能是最具争议的部分。当前的提案试图实现几个目标

  • 采用声明性模型
  • 尽可能简单易懂
  • 提供足够的灵活性,以匹配大多数用例

专家小组正在寻求对以下几种形式的反馈:groups方法

  • 用例:描述您的用例,看看它们如何(或是否)可以通过当前提案来解决
  • 增强:在概念上提出增强,使其更加灵活或更加简单groups替代方案:您认为您有完美的解决方案?去描述它,让我们看看它如何适应模型

我们鼓励您阅读规范,并在此论坛上提供反馈和评论。


返回顶部