Hibernate Validator 中文操作指南

5. Grouping constraints

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

5.1. Requesting groups

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

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

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

示例 5.1:示例类 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,它只是一个简单的标记接口。

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

示例 5.2:驾驶员

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 属性上,表示汽车是否通过了路况测试。

示例 5.3:汽车

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

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

  1. Person.nameCar.manufacturerCar.licensePlateCar.seatCount 上的约束都属于 Default

  2. Driver.ageDriver.hasDrivingLicense 上的约束属于 DriverChecks

  3. Car.passedVehicleInspection 的约束属于 CarChecks 类别

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

示例 5.4:使用验证组

// 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,因为定义在此属性上的约束不属于默认组。

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

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

5.2. Group inheritance

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

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

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

示例 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 是否是一辆有效的汽车,以及它是否是一辆有效的赛车。

示例 5.6:使用组继承

// 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. Defining group sequences

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

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

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

示例 5.7:定义组顺序

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 5.8, “Using a group sequence” 中所示那样使用新序列。

示例 5.8:使用组顺序

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 的组的序列。

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

示例 5.9:具有重新定义的默认组的类 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 {
}

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

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

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

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

5.4.2. @GroupSequenceProvider

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

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

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

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 发挥了作用,它允许你在级联验证中使用不同于最初请求的组。

我们来看一下 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 组。

示例 5.12: @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 ...
}

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

示例 5.13: @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 指定多项转换。

但是,以下限制适用:

  1. @ConvertGroup 只能与 @Valid 搭配使用。如果不搭配使用,将抛出 ConstraintDeclarationException 异常。

  2. 在同一个元素上为同一个值设置多个转换规则是被禁止的。在这种情况下,将抛出 ConstraintDeclarationException 异常。

  3. from 属性不得引用组序列。在这种情况下,将抛出 ConstraintDeclarationException 异常。

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