Hibernate Validator 中文操作指南

10. Using constraint metadata

Jakarta Bean Validation 规范不仅提供了校验引擎,还提供了一个 API,用于以统一的方式检索约束元数据,无论约束是使用注释还是通过 XML 映射声明的。阅读本章以了解有关此 API 及其功能的更多信息。您可以在包 jakarta.validation.metadata 中找到所有元数据 API 类型。

本章中介绍的示例基于 @“19”中所示的类和约束声明。

示例 10.1:示例类
package org.hibernate.validator.referenceguide.chapter10;

public class Person {

    public interface Basic {
    }

    @NotNull
    private String name;

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

public interface Vehicle {

    public interface Basic {
    }

    @NotNull(groups = Vehicle.Basic.class)
    String getManufacturer();
}
package org.hibernate.validator.referenceguide.chapter10;

@ValidCar
public class Car implements Vehicle {

    public interface SeverityInfo extends Payload {
    }

    private String manufacturer;

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

    private Person driver;

    private String modelName;

    public Car() {
    }

    public Car(
            @NotNull String manufacturer,
            String licencePlate,
            Person driver,
            String modelName) {

        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.driver = driver;
        this.modelName = modelName;
    }

    public void driveAway(@Max(75) int speed) {
        //...
    }

    @LuggageCountMatchesPassengerCount(
            piecesOfLuggagePerPassenger = 2,
            validationAppliesTo = ConstraintTarget.PARAMETERS,
            payload = SeverityInfo.class,
            message = "There must not be more than {piecesOfLuggagePerPassenger} pieces " +
                    "of luggage per passenger."
    )
    public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
        //...
    }

    @Override
    @Size(min = 3)
    public String getManufacturer() {
        return manufacturer;
    }

    public void setManufacturer(String manufacturer) {
        this.manufacturer = manufacturer;
    }

    @Valid
    @ConvertGroup(from = Default.class, to = Person.Basic.class)
    public Person getDriver() {
        return driver;
    }

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

public class Library {

    @NotNull
    private String name;

    private List<@NotNull @Valid Book> books;

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

public class Book {

    @NotEmpty
    private String title;

    @NotEmpty
    private String author;

    //getters and setters ...
}

10.1. BeanDescriptor

元数据 API 的入口点是方法 Validator#getConstraintsForClass() ,它返回 BeanDescriptor 接口的实例。使用此描述符,您可以获取直接在 bean 本身(类级或属性级)上声明的约束的元数据,还可以检索表示单个属性、方法和构造函数的元数据描述符。

Example 10.2, “Using BeanDescriptor 展示了如何为 Car 类检索 BeanDescriptor ,以及如何以断言的形式使用此描述符。

如果请求的类中托管的约束声明无效,将抛出 ValidationException

示例 10.2:使用 BeanDescriptor
BeanDescriptor carDescriptor = validator.getConstraintsForClass( Car.class );

assertTrue( carDescriptor.isBeanConstrained() );

//one class-level constraint
assertEquals( 1, carDescriptor.getConstraintDescriptors().size() );

//manufacturer, licensePlate, driver
assertEquals( 3, carDescriptor.getConstrainedProperties().size() );

//property has constraint
assertNotNull( carDescriptor.getConstraintsForProperty( "licensePlate" ) );

//property is marked with @Valid
assertNotNull( carDescriptor.getConstraintsForProperty( "driver" ) );

//constraints from getter method in interface and implementation class are returned
assertEquals(
        2,
        carDescriptor.getConstraintsForProperty( "manufacturer" )
                .getConstraintDescriptors()
                .size()
);

//property is not constrained
assertNull( carDescriptor.getConstraintsForProperty( "modelName" ) );

//driveAway(int), load(List<Person>, List<PieceOfLuggage>)
assertEquals( 2, carDescriptor.getConstrainedMethods( MethodType.NON_GETTER ).size() );

//driveAway(int), getManufacturer(), getDriver(), load(List<Person>, List<PieceOfLuggage>)
assertEquals(
        4,
        carDescriptor.getConstrainedMethods( MethodType.NON_GETTER, MethodType.GETTER )
                .size()
);

//driveAway(int)
assertNotNull( carDescriptor.getConstraintsForMethod( "driveAway", int.class ) );

//getManufacturer()
assertNotNull( carDescriptor.getConstraintsForMethod( "getManufacturer" ) );

//setManufacturer() is not constrained
assertNull( carDescriptor.getConstraintsForMethod( "setManufacturer", String.class ) );

//Car(String, String, Person, String)
assertEquals( 1, carDescriptor.getConstrainedConstructors().size() );

//Car(String, String, Person, String)
assertNotNull(
        carDescriptor.getConstraintsForConstructor(
                String.class,
                String.class,
                Person.class,
                String.class
        )
);

您可以通过 isBeanConstrained() 确定指定的类是否承载任何类或属性级别的约束。isBeanConstrained() 不考虑方法或构造函数约束。

方法 getConstraintDescriptors() 适用于所有从 ElementDescriptor 派生的描述符(请参阅 Section 10.4, “ElementDescriptor ),并返回一组描述符,代表给定元素上直接声明的约束。对于 BeanDescriptor ,将返回 bean 的类级约束。可在 Section 10.7, “ConstraintDescriptor 中找到关于 ConstraintDescriptor 的更多详细信息。

通过 getConstraintsForProperty()getConstraintsForMethod()getConstraintsForConstructor(),您可以获取一个描述符,该描述符表示一个给定的属性或可执行元素,该元素由其名称标识,并且在方法和构造函数中由其参数类型标识。在以下部分中描述了这些方法返回的不同描述符类型。

请注意,这些方法根据 @“34”中所述的约束继承规则考虑在超类型中声明的约束。示例是 @“30”属性的描述符,它提供对 @“31”上定义的所有约束以及实现方法 @“32”的访问。如果指定的元素不存在或未受约束,则返回 @“33”。

方法 getConstrainedProperties()getConstrainedMethods()getConstrainedConstructors() 分别返回(可能是空的)集合,其中包含所有受约束的属性、方法和构造函数。如果一个元素至少有一个约束或标记为级联验证,则该元素被视为受约束。在调用 getConstrainedMethods() 时,可以指定要返回的方法的类型(getters、non-getters 或两者)。

10.2. PropertyDescriptor

接口 PropertyDescriptor 表示类的某个给定属性。只要遵守 JavaBeans 命名约定,就可以透明地声明对字段或属性 getter 的约束。 Example 10.3, “Using PropertyDescriptor 展示了如何使用 PropertyDescriptor 接口。

示例 10.3:使用 PropertyDescriptor
PropertyDescriptor licensePlateDescriptor = carDescriptor.getConstraintsForProperty(
        "licensePlate"
);

//"licensePlate" has two constraints, is not marked with @Valid and defines no group conversions
assertEquals( "licensePlate", licensePlateDescriptor.getPropertyName() );
assertEquals( 2, licensePlateDescriptor.getConstraintDescriptors().size() );
assertTrue( licensePlateDescriptor.hasConstraints() );
assertFalse( licensePlateDescriptor.isCascaded() );
assertTrue( licensePlateDescriptor.getGroupConversions().isEmpty() );

PropertyDescriptor driverDescriptor = carDescriptor.getConstraintsForProperty( "driver" );

//"driver" has no constraints, is marked with @Valid and defines one group conversion
assertEquals( "driver", driverDescriptor.getPropertyName() );
assertTrue( driverDescriptor.getConstraintDescriptors().isEmpty() );
assertFalse( driverDescriptor.hasConstraints() );
assertTrue( driverDescriptor.isCascaded() );
assertEquals( 1, driverDescriptor.getGroupConversions().size() );

使用 getConstraintDescriptors() ,您可以检索一组 ConstraintDescriptors ,其中提供有关给定属性的各个约束的更多信息。如果将属性标记为级联验证(使用 @Valid 注释或通过 XML),则方法 isCascaded() 返回 true ;否则返回 false 。由 getGroupConversions() 返回任何配置的组转换。有关 GroupConversionDescriptor 的更多详细信息,请参阅 Section 10.6, “GroupConversionDescriptor

10.3. MethodDescriptor and ConstructorDescriptor

受约束的方法和构造函数分别由界面 MethodDescriptor ConstructorDescriptor 表示。 Example 10.4, “Using MethodDescriptor and ConstructorDescriptor 展示了如何使用这些描述符。

示例 10.4:使用 MethodDescriptorConstructorDescriptor
//driveAway(int) has a constrained parameter and an unconstrained return value
MethodDescriptor driveAwayDescriptor = carDescriptor.getConstraintsForMethod(
        "driveAway",
        int.class
);
assertEquals( "driveAway", driveAwayDescriptor.getName() );
assertTrue( driveAwayDescriptor.hasConstrainedParameters() );
assertFalse( driveAwayDescriptor.hasConstrainedReturnValue() );

//always returns an empty set; constraints are retrievable by navigating to
//one of the sub-descriptors, e.g. for the return value
assertTrue( driveAwayDescriptor.getConstraintDescriptors().isEmpty() );

ParameterDescriptor speedDescriptor = driveAwayDescriptor.getParameterDescriptors()
        .get( 0 );

//The "speed" parameter is located at index 0, has one constraint and is not cascaded
//nor does it define group conversions
assertEquals( "speed", speedDescriptor.getName() );
assertEquals( 0, speedDescriptor.getIndex() );
assertEquals( 1, speedDescriptor.getConstraintDescriptors().size() );
assertFalse( speedDescriptor.isCascaded() );
assert speedDescriptor.getGroupConversions().isEmpty();

//getDriver() has no constrained parameters but its return value is marked for cascaded
//validation and declares one group conversion
MethodDescriptor getDriverDescriptor = carDescriptor.getConstraintsForMethod(
        "getDriver"
);
assertFalse( getDriverDescriptor.hasConstrainedParameters() );
assertTrue( getDriverDescriptor.hasConstrainedReturnValue() );

ReturnValueDescriptor returnValueDescriptor = getDriverDescriptor.getReturnValueDescriptor();
assertTrue( returnValueDescriptor.getConstraintDescriptors().isEmpty() );
assertTrue( returnValueDescriptor.isCascaded() );
assertEquals( 1, returnValueDescriptor.getGroupConversions().size() );

//load(List<Person>, List<PieceOfLuggage>) has one cross-parameter constraint
MethodDescriptor loadDescriptor = carDescriptor.getConstraintsForMethod(
        "load",
        List.class,
        List.class
);
assertTrue( loadDescriptor.hasConstrainedParameters() );
assertFalse( loadDescriptor.hasConstrainedReturnValue() );
assertEquals(
        1,
        loadDescriptor.getCrossParameterDescriptor().getConstraintDescriptors().size()
);

//Car(String, String, Person, String) has one constrained parameter
ConstructorDescriptor constructorDescriptor = carDescriptor.getConstraintsForConstructor(
        String.class,
        String.class,
        Person.class,
        String.class
);

assertEquals( "Car", constructorDescriptor.getName() );
assertFalse( constructorDescriptor.hasConstrainedReturnValue() );
assertTrue( constructorDescriptor.hasConstrainedParameters() );
assertEquals(
        1,
        constructorDescriptor.getParameterDescriptors()
                .get( 0 )
                .getConstraintDescriptors()
                .size()
);

getName() 返回给定方法或构造函数的名称。方法 hasConstrainedParameters()hasConstrainedReturnValue() 可用于快速检查可执行元素是否有任何参数约束(单个参数上的约束或跨参数约束)或返回值约束。

请注意,约束不会直接在 MethodDescriptorConstructorDescriptor 上公开,而是在表示可执行参数、返回值和跨参数约束的专用描述符上公开的。要获取这些描述符中的一个,请分别调用 getParameterDescriptors()getReturnValueDescriptor()getCrossParameterDescriptor()

这些描述符提供对元素约束 ( getConstraintDescriptors() ) 的访问,在参数和返回值的情况下,还提供对级联验证的配置 ( isValid()getGroupConversions() )。对于参数,您还可以通过 getName()getIndex() 检索由当前使用的参数名称提供程序(请参阅 Section 9.2.4, “ParameterNameProvider )返回的索引和名称。

遵循 JavaBeans 命名约定的 getter 方法被认为是 bean 属性,也是受约束方法。

这意味着您可以通过获取 PropertyDescriptor (例如 BeanDescriptor.getConstraintsForProperty("foo") )或检查 getter 的 MethodDescriptor (例如 BeanDescriptor.getConstraintsForMethod("getFoo").getReturnValueDescriptor()) )的返回值描述符来检索相关的元数据。

10.4. ElementDescriptor

ElementDescriptor 接口是单个描述符类型(如 BeanDescriptorPropertyDescriptor 等)的通用基类。除了 getConstraintDescriptors() 之外,它还提供了一些对所有描述符通用的方法。

hasConstraints() 可快速查看元素是否具有任何直接约束(例如,在 BeanDescriptor 的情况下为类级别约束)。

getElementClass() 返回由给定描述符表示的元素的 Java 类型。更具体地说,该方法返回:

  1. BeanDescriptor 上调用时为对象类型,

  2. 分别在 PropertyDescriptorParameterDescriptor 上调用时为属性或参数的类型,

  3. CrossParameterDescriptor 上调用时为 Object[].class

  4. ConstructorDescriptor, MethodDescriptorReturnValueDescriptor 上调用时的返回类型。对于没有返回值的方法将返回 void.class

Example 10.5, “Using ElementDescriptor methods 展示了如何使用这些方法。

示例 10.5:使用 ElementDescriptor methods
PropertyDescriptor manufacturerDescriptor = carDescriptor.getConstraintsForProperty(
        "manufacturer"
);

assertTrue( manufacturerDescriptor.hasConstraints() );
assertEquals( String.class, manufacturerDescriptor.getElementClass() );

CrossParameterDescriptor loadCrossParameterDescriptor = carDescriptor.getConstraintsForMethod(
        "load",
        List.class,
        List.class
).getCrossParameterDescriptor();

assertTrue( loadCrossParameterDescriptor.hasConstraints() );
assertEquals( Object[].class, loadCrossParameterDescriptor.getElementClass() );

最后, ElementDescriptor 提供对 ConstraintFinder API 的访问,该 API 可以精细地查询约束条件元数据。 Example 10.6, “Usage of ConstraintFinder 展示了如何通过 findConstraints() 检索 ConstraintFinder 实例,并使用 API 查询约束条件元数据。

示例 10.6: ConstraintFinder 用法
PropertyDescriptor manufacturerDescriptor = carDescriptor.getConstraintsForProperty(
        "manufacturer"
);

//"manufacturer" constraints are declared on the getter, not the field
assertTrue(
        manufacturerDescriptor.findConstraints()
                .declaredOn( ElementType.FIELD )
                .getConstraintDescriptors()
                .isEmpty()
);

//@NotNull on Vehicle#getManufacturer() is part of another group
assertEquals(
        1,
        manufacturerDescriptor.findConstraints()
                .unorderedAndMatchingGroups( Default.class )
                .getConstraintDescriptors()
                .size()
);

//@Size on Car#getManufacturer()
assertEquals(
        1,
        manufacturerDescriptor.findConstraints()
                .lookingAt( Scope.LOCAL_ELEMENT )
                .getConstraintDescriptors()
                .size()
);

//@Size on Car#getManufacturer() and @NotNull on Vehicle#getManufacturer()
assertEquals(
        2,
        manufacturerDescriptor.findConstraints()
                .lookingAt( Scope.HIERARCHY )
                .getConstraintDescriptors()
                .size()
);

//Combining several filter options
assertEquals(
        1,
        manufacturerDescriptor.findConstraints()
                .declaredOn( ElementType.METHOD )
                .lookingAt( Scope.HIERARCHY )
                .unorderedAndMatchingGroups( Vehicle.Basic.class )
                .getConstraintDescriptors()
                .size()
);

通过 declaredOn(),您可以在某些元素类型上搜索已声明的 ConstraintDescriptors。这对于查找在字段或 getter 方法上声明的属性约束很有用。

unorderedAndMatchingGroups() 将所得约束限制为与给定验证组匹配的约束。

lookingAt() 允许区分直接在元素上指定的约束 (Scope.LOCAL_ELEMENT) 或属于该元素但托管在类层次结构中的约束 (Scope.HIERARCHY)。

您还可以结合不同的选项,如最后一个示例所示。

unorderedAndMatchingGroups() 不受尊重,但尊重组继承和序列继承。

10.5. ContainerDescriptor and ContainerElementTypeDescriptor

ContainerDescriptor 接口是支持容器元素约束和级联验证( PropertyDescriptorParameterDescriptorReturnValueDescriptor )的所有元素的通用接口。

它有一个返回 ContainerElementTypeDescriptor 集合的单个方法 getConstrainedContainerElementTypes()

ContainerElementTypeDescriptor 扩展 ContainerDescriptor 以支持嵌套容器元素约束。

ContainerElementTypeDescriptor 包含有关容器、约束和级联验证的信息。

Example 10.7, “Using ContainerElementTypeDescriptor 展示了如何使用 getConstrainedContainerElementTypes() 检索 ContainerElementTypeDescriptor 的集合。

示例 10.7:使用 ContainerElementTypeDescriptor
PropertyDescriptor booksDescriptor = libraryDescriptor.getConstraintsForProperty(
        "books"
);

Set<ContainerElementTypeDescriptor> booksContainerElementTypeDescriptors =
        booksDescriptor.getConstrainedContainerElementTypes();
ContainerElementTypeDescriptor booksContainerElementTypeDescriptor =
        booksContainerElementTypeDescriptors.iterator().next();

assertTrue( booksContainerElementTypeDescriptor.hasConstraints() );
assertTrue( booksContainerElementTypeDescriptor.isCascaded() );
assertEquals(
        0,
        booksContainerElementTypeDescriptor.getTypeArgumentIndex().intValue()
);
assertEquals(
        List.class,
        booksContainerElementTypeDescriptor.getContainerClass()
);

Set<ConstraintDescriptor<?>> constraintDescriptors =
        booksContainerElementTypeDescriptor.getConstraintDescriptors();
ConstraintDescriptor<?> constraintDescriptor =
        constraintDescriptors.iterator().next();

assertEquals(
        NotNull.class,
        constraintDescriptor.getAnnotation().annotationType()
);

10.6. GroupConversionDescriptor

所有表示可能是级联验证对象(即 PropertyDescriptorParameterDescriptorReturnValueDescriptor )的元素的描述符类型都通过 getGroupConversions() 提供对元素组转换的访问。返回的集合包含每个已配置转换的 GroupConversionDescriptor ,允许检索转换的源组和目标组。 Example 10.8, “Using GroupConversionDescriptor 展示了一个示例。

示例 10.8:使用 GroupConversionDescriptor
PropertyDescriptor driverDescriptor = carDescriptor.getConstraintsForProperty( "driver" );

Set<GroupConversionDescriptor> groupConversions = driverDescriptor.getGroupConversions();
assertEquals( 1, groupConversions.size() );

GroupConversionDescriptor groupConversionDescriptor = groupConversions.iterator()
        .next();
assertEquals( Default.class, groupConversionDescriptor.getFrom() );
assertEquals( Person.Basic.class, groupConversionDescriptor.getTo() );

10.7. ConstraintDescriptor

最后但并非最不重要的一点, ConstraintDescriptor 接口描述了单个约束及其组成约束。通过此接口的实例,您可以访问约束注释及其参数。

Example 10.9, “Using ConstraintDescriptor 展示了如何从 ConstraintDescriptor 检索默认约束属性(如消息模板、组等),以及自定义约束属性 ( piecesOfLuggagePerPassenger ) 和其他元数据,例如约束的注释类型及其验证者。

示例 10.9:使用 ConstraintDescriptor
//descriptor for the @LuggageCountMatchesPassengerCount constraint on the
//load(List<Person>, List<PieceOfLuggage>) method
ConstraintDescriptor<?> constraintDescriptor = carDescriptor.getConstraintsForMethod(
        "load",
        List.class,
        List.class
).getCrossParameterDescriptor().getConstraintDescriptors().iterator().next();

//constraint type
assertEquals(
        LuggageCountMatchesPassengerCount.class,
        constraintDescriptor.getAnnotation().annotationType()
);

//standard constraint attributes
assertEquals( SeverityInfo.class, constraintDescriptor.getPayload().iterator().next() );
assertEquals(
        ConstraintTarget.PARAMETERS,
        constraintDescriptor.getValidationAppliesTo()
);
assertEquals( Default.class, constraintDescriptor.getGroups().iterator().next() );
assertEquals(
        "There must not be more than {piecesOfLuggagePerPassenger} pieces of luggage per " +
        "passenger.",
        constraintDescriptor.getMessageTemplate()
);

//custom constraint attribute
assertEquals(
        2,
        constraintDescriptor.getAttributes().get( "piecesOfLuggagePerPassenger" )
);

//no composing constraints
assertTrue( constraintDescriptor.getComposingConstraints().isEmpty() );

//validator class
assertEquals(
        Arrays.<Class<?>>asList( LuggageCountMatchesPassengerCount.Validator.class ),
        constraintDescriptor.getConstraintValidatorClasses()
);