Hibernate Validator 中文操作指南
5. Grouping constraints
前面几章讨论过的 Validator 和 ExecutableValidator 上的所有验证方法也需要一个可变参数 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,并添加了属性 age 和 hasDrivingLicense。驾驶员必须至少 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 {
}
最后,类 Car( Example 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 {
}
总共在示例中使用了三个不同的组:
-
Person.name、Car.manufacturer、Car.licensePlate 和 Car.seatCount 上的约束都属于 Default 组
-
Driver.age 和 Driver.hasDrivingLicense 上的约束属于 DriverChecks
-
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” 中所示)来评估属于 RentalChecks、CarChecks 和 RentalCar 的约束。
示例 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 组。
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” 中的验证也会成功。
// 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 指定多项转换。
但是,以下限制适用:
-
@ConvertGroup 只能与 @Valid 搭配使用。如果不搭配使用,将抛出 ConstraintDeclarationException 异常。
-
在同一个元素上为同一个值设置多个转换规则是被禁止的。在这种情况下,将抛出 ConstraintDeclarationException 异常。
-
from 属性不得引用组序列。在这种情况下,将抛出 ConstraintDeclarationException 异常。
规则不会以递归方式执行。将使用第一个匹配的转换规则,并忽略后续规则。例如,如果一组 @ConvertGroup 声明将组 A 链接到 B,将 B 链接到 C,那么组 A 将被转换为 B,而不是 C。