Hibernate Validator 中文操作指南

5. Grouping constraints

前面几章讨论过的 ValidatorExecutableValidator 上的所有验证方法也需要一个可变参数 groups。到目前为止,我们一直在忽略这个参数,但现在是仔细了解它的好时机了。

All validation methods on Validator and ExecutableValidator discussed in earlier chapters also take a var-arg argument groups. So far we have been ignoring this parameter, but it is time to have a closer look.

5.1. Requesting groups

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

Groups allow you to restrict the set of constraints applied during validation. One use case for validation groups are UI wizards where in each step only a specified subset of constraints should get validated. The groups targeted are passed as var-arg parameters to the appropriate validate method.

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

Let’s have a look at an example. The class Person in Example 5.1, “Example class Person has a @NotNull constraint on name. Since no group is specified for this annotation the default group jakarta.validation.groups.Default is assumed.

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

When more than one group is requested, the order in which the groups are evaluated is not deterministic. If no group is specified the default group jakarta.validation.groups.Default is assumed.

示例 5.1:示例类 Person

. Example 5.1: Example class 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 5.2, “Driver” 中的类 Driver_扩展了 _Person,并添加了属性 agehasDrivingLicense。驾驶员必须至少 18 岁(@Min(18))并持有驾照(@AssertTrue)。定义在这些属性上的两个约束属于组 DriverChecks,它只是一个简单的标记接口。

The class Driver in Example 5.2, “Driver” extends Person and adds the properties age and hasDrivingLicense. Drivers must be at least 18 years old (@Min(18)) and have a driving license (@AssertTrue). Both constraints defined on these properties belong to the group DriverChecks which is just a simple tagging interface.

使用接口使组的使用类型安全,并允许轻松重构。这意味着组可以通过类继承互相继承。另请参阅 Section 5.2, “Group inheritance”

Using interfaces makes the usage of groups type-safe and allows for easy refactoring. It also means that groups can inherit from each other via class inheritance. See Section 5.2, “Group inheritance”.

示例 5.2:驾驶员

. Example 5.2: Driver

package org.hibernate.validator.referenceguide.chapter05;

public class Driver extends Person {

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

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

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

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

    public int getAge() {
        return age;
    }

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

public interface DriverChecks {
}

最后,类 CarExample 5.3, “Car”)有一些约束,这些约束既属于默认组,也属于组 CarChecks 中的 @AssertTrue,该组位于 passedVehicleInspection 属性上,表示汽车是否通过了路况测试。

Finally the class Car (Example 5.3, “Car”) has some constraints which are part of the default group as well as @AssertTrue in the group CarChecks on the property passedVehicleInspection which indicates whether a car passed the road worthy tests.

示例 5.3:汽车

. Example 5.3: Car

package org.hibernate.validator.referenceguide.chapter05;

public class Car {
    @NotNull
    private String manufacturer;

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

    @Min(2)
    private int seatCount;

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

    @Valid
    private Driver driver;

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

    public boolean isPassedVehicleInspection() {
        return passedVehicleInspection;
    }

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

    public Driver getDriver() {
        return driver;
    }

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

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

public interface CarChecks {
}

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

Overall three different groups are used in the example:

  1. The constraints on Person.name, Car.manufacturer, Car.licensePlate and Car.seatCount all belong to the Default group

  2. The constraints on Driver.age and Driver.hasDrivingLicense belong to DriverChecks

  3. The constraint on Car.passedVehicleInspection belongs to the group CarChecks

Example 5.4, “Using validation groups”展示了如何将不同的组组合传递给 _Validator#validate()_方法,从而产生不同的验证结果。

Example 5.4, “Using validation groups” shows how passing different group combinations to the Validator#validate() method results in different validation results.

示例 5.4:使用验证组

. Example 5.4: Using validation groups

// 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 5.4, “Using validation groups” 中,第一个 validate()_调用未使用显式组。不会出现验证错误,即使属性 _passedVehicleInspection 默认是 false,因为定义在此属性上的约束不属于默认组。

The first validate() call in Example 5.4, “Using validation groups” is done using no explicit group. There are no validation errors, even though the property passedVehicleInspection is per default false as the constraint defined on this property does not belong to the default group.

使用 CarChecks 组进行的下一个验证在车辆通过车辆检查之前失败。将驾驶员添加到汽车并再次针对 DriverChecks 验证产生了一条约束违规,原因是驾驶员还未通过驾驶测试。只有在将 passedDrivingTest 设置为 true 后,针对 DriverChecks 的验证才通过。

The next validation using the CarChecks group fails until the car passes the vehicle inspection. Adding a driver to the car and validating against DriverChecks again yields one constraint violation due to the fact that the driver has not yet passed the driving test. Only after setting passedDrivingTest to true the validation against DriverChecks passes.

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

The last validate() call finally shows that all constraints are passing by validating against all defined groups.

5.2. Group inheritance

Example 5.4, “Using validation groups” 中,我们需要针对每个验证组调用 validate(),或逐个指定所有组。

In Example 5.4, “Using validation groups”, we need to call validate() for each validation group, or specify all of them one by one.

在某些情况下,你可能想定义一组包含另一组的约束。你可以使用组继承来完成此项操作。

In some situations, you may want to define a group of constraints which includes another group. You can do that using group inheritance.

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

In Example 5.5, “SuperCar”, we define a SuperCar and a group RaceCarChecks that extends the Default group. A SuperCar must have safety belts to be allowed to run in races.

示例 5.5:SuperCar

. Example 5.5: SuperCar

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

public class SuperCar extends Car {

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

    // getters and setters ...

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

import jakarta.validation.groups.Default;

public interface RaceCarChecks extends Default {
}

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

In the example below, we will check if a SuperCar with one seat and no security belts is a valid car and if it is a valid race-car.

示例 5.6:使用组继承

. Example 5.6: Using group inheritance

// 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 组的约束。

On the first call to validate(), we do not specify a group. There is one validation error because a car must have at least one seat. It is the constraint from the Default group.

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

On the second call, we specify only the group RaceCarChecks. There are two validation errors: one about the missing seat from the Default group, another one about the fact that there is no safety belts coming from the RaceCarChecks group.

5.3. Defining group sequences

默认情况下,约束按无特定顺序进行评估,无论属于哪个组。然而,在某些情况下,控制约束的评估顺序很有用。

By default, constraints are evaluated in no particular order, regardless of which groups they belong to. In some situations, however, it is useful to control the order in which constraints are evaluated.

Example 5.4, “Using validation groups” 的示例中,在检查汽车道路行驶能力之前,可能要求首先通过所有默认汽车约束。最后,在开车离开之前,应检查实际驾驶员约束。

In the example from Example 5.4, “Using validation groups” it could for instance be required that first all default car constraints are passing before checking the road worthiness of the car. Finally, before driving away, the actual driver constraints should be checked.

为了实现这样的验证顺序,你只需要定义一个接口并用 @GroupSequence 对其进行注释,定义验证组的顺序(参见 Example 5.7, “Defining a group sequence”)。如果一个有序组中至少有一个约束失败,则序列中后续组的约束都不会被验证。

In order to implement such a validation order you just need to define an interface and annotate it with @GroupSequence, defining the order in which the groups have to be validated (see Example 5.7, “Defining a group sequence”). If at least one constraint fails in a sequenced group, none of the constraints of the following groups in the sequence get validated.

示例 5.7:定义组顺序

. Example 5.7: Defining a group sequence

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

Groups defining a sequence and groups composing a sequence must not be involved in a cyclic dependency either directly or indirectly, either through cascaded sequence definition or group inheritance. If a group containing such a circularity is evaluated, a GroupDefinitionException is raised.

然后,你可以像 Example 5.8, “Using a group sequence” 中所示那样使用新序列。

You then can use the new sequence as shown in in Example 5.8, “Using a group sequence”.

示例 5.8:使用组顺序

. Example 5.8: Using a group sequence

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. Redefining the default group sequence

5.4.1. @GroupSequence

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

Besides defining group sequences, the @GroupSequence annotation also allows to redefine the default group for a given class. To do so, just add the @GroupSequence annotation to the class and specify the sequence of groups which substitute Default for this class within the annotation.

Example 5.9, “Class RentalCar with redefined default group” 引入了一个带有重新定义的默认组的新类 RentalCar

Example 5.9, “Class RentalCar with redefined default group” introduces a new class RentalCar with a redefined default group.

示例 5.9:具有重新定义的默认组的类 RentalCar

. Example 5.9: Class RentalCar with redefined default group

package org.hibernate.validator.referenceguide.chapter05;

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

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

    public boolean isRented() {
        return rented;
    }

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

public interface RentalChecks {
}

有了这个定义,你可以通过仅请求 Default 组(如 Example 5.10, “Validating an object with redefined default group” 中所示)来评估属于 RentalChecksCarChecksRentalCar 的约束。

With this definition you can evaluate the constraints belonging to RentalChecks, CarChecks and RentalCar by just requesting the Default group as seen in Example 5.10, “Validating an object with redefined default group”.

示例 5.10:验证具有重新定义的默认组的对象

. Example 5.10: Validating an object with redefined default group

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 添加到序列中。而必须添加类本身。

Since there must be no cyclic dependency in the group and group sequence definitions, one cannot just add Default to the sequence redefining Default for a class. Instead the class itself has to be added.

Default 组序列重写对定义它的类是局部的,不会传播到关联的对象。对于该示例,这意味着将 DriverChecks 添加到 RentalCar 的默认组序列不会产生任何影响。只有组 Default 将传播到驱动程序关联。

The Default group sequence overriding is local to the class it is defined on and is not propagated to associated objects. For the example, this means that adding DriverChecks to the default group sequence of RentalCar would not have any effects. Only the group Default will be propagated to the driver association.

请注意,你可以通过声明组转换规则来控制传播组(参见 Section 5.5, “Group conversion”)。

Note that you can control the propagated group(s) by declaring a group conversion rule (see Section 5.5, “Group conversion”).

5.4.2. @GroupSequenceProvider

除了通过 @GroupSequence 静态重新定义默认组序列外,Hibernate 验证器还为动态重新定义默认组序列提供了一个 SPI,具体取决于对象状态。

In addition to statically redefining default group sequences via @GroupSequence, Hibernate Validator also provides an SPI for the dynamic redefinition of default group sequences depending on the object state.

为此,你需要实现接口 DefaultGroupSequenceProvider,并通过 @GroupSequenceProvider 注释将此实现注册到目标类中。在租赁汽车场景中,你可以动态添加 CarChecks,如 Example 5.11, “Implementing and using a default group sequence provider” 中所示。

For that purpose, you need to implement the interface DefaultGroupSequenceProvider and register this implementation with the target class via the @GroupSequenceProvider annotation. In the rental car scenario, you could for instance dynamically add the CarChecks as seen in Example 5.11, “Implementing and using a default group sequence provider”.

示例 5.11:实现和使用默认组序列提供程序

. Example 5.11: Implementing and using a default group sequence provider

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. Group conversion

如果你想将汽车相关检查和驾驶员检查一起验证,该怎么办?当然,你可以将需要的组明确传递到 validate 调用中,但如果你想让这些验证作为 Default 组验证的一部分发生,该怎么办?此时 @ConvertGroup 发挥了作用,它允许你在级联验证中使用不同于最初请求的组。

What if you wanted to validate the car related checks together with the driver checks? Of course you could pass the required groups to the validate call explicitly, but what if you wanted to make these validations occur as part of the Default group validation? Here @ConvertGroup comes into play which allows you to use a different group than the originally requested one during cascaded validation.

我们来看一下 Example 5.12, “@ConvertGroup usage” 。这里 @GroupSequence({ CarChecks.class, Car.class }) 用于将与汽车相关的约束组合在 Default 组下(参见 Section 5.4, “Redefining the default group sequence” )。还有一个 @ConvertGroup(from = Default.class, to = DriverChecks.class) ,它确保在驾驶员关联的级联验证期间将 Default 组转换为 DriverChecks 组。

Let’s have a look at Example 5.12, “@ConvertGroup usage”. Here @GroupSequence({ CarChecks.class, Car.class }) is used to combine the car related constraints under the Default group (see Section 5.4, “Redefining the default group sequence”). There is also a @ConvertGroup(from = Default.class, to = DriverChecks.class) which ensures the Default group gets converted to the DriverChecks group during cascaded validation of the driver association.

示例 5.12: @ConvertGroup 用法

. Example 5.12: @ConvertGroup usage

package org.hibernate.validator.referenceguide.chapter05.groupconversion;

public class Driver {

    @NotNull
    private String name;

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

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

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

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

    public int getAge() {
        return age;
    }

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

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

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

    @NotNull
    private String manufacturer;

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

    @Min(2)
    private int seatCount;

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

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

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

    public boolean isPassedVehicleInspection() {
        return passedVehicleInspection;
    }

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

    public Driver getDriver() {
        return driver;
    }

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

    // getters and setters ...
}

这样,即使 hasDrivingLicense 上的约束属于 DriverChecks 组并且 validate() 调用中只请求 Default 组, Example 5.13, “Test case for @ConvertGroup 中的验证也会成功。

As a result the validation in Example 5.13, “Test case for @ConvertGroup succeeds, even though the constraint on hasDrivingLicense belongs to the DriverChecks group and only the Default group is requested in the validate() call.

示例 5.13: @ConvertGroup 的测试用例

. Example 5.13: Test case for @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 指定多项转换。

You can define group conversions wherever @Valid can be used, namely associations as well as method and constructor parameters and return values. Multiple conversions can be specified using @ConvertGroup.List.

但是,以下限制适用:

However, the following restrictions apply:

  1. @ConvertGroup must only be used in combination with @Valid. If used without, a ConstraintDeclarationException is thrown.

  2. It is not legal to have multiple conversion rules on the same element with the same from value. In this case, a ConstraintDeclarationException is raised.

  3. The from attribute must not refer to a group sequence. A ConstraintDeclarationException is raised in this situation.

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

Rules are not executed recursively. The first matching conversion rule is used and subsequent rules are ignored. For example if a set of @ConvertGroup declarations chains group A to B and B to C, the group A will be converted to B and not to C.