Hibernate Validator 中文操作指南

12. Hibernate Validator Specifics

在本章中,您将了解如何利用 Hibernate 验证器提供的多项功能,除了Jakarta Bean Validation 规范定义的功能之外。这包括快速失败模式、用于编程约束配置的 API 以及约束的布尔组合。

只要 org.hibernate.validator.Incubating 注释仍在开发中,就会标记新的 API 或 SPI。这意味着此类元素(例如,包、类型、方法、常数等)在后续版本中可能会被不兼容地更改或删除。鼓励使用正在孵化的 API/SPI 成员(以便开发团队可以对这些新功能获得反馈),但您应该准备好根据需要在升级到 Hibernate 验证器的更高版本时更新使用它们时的代码。

使用以下部分中描述的功能可能会导致在 Jakarta Bean Validation 提供程序之间不可移植的应用程序代码。

12.1. Public API

不过,让我们从查看 Hibernate 验证器的公开 API 开始。您可以在下面找到属于此 API 的所有包及其用途的列表。请注意,当包是公开 API 的一部分时,其子包不一定也是公开 API 的一部分。

org.hibernate.validator

用于 Jakarta Bean 验证引导机制的类(例如,验证提供程序、配置类);有关详细信息,请参阅 Chapter 9, Bootstrapping

org.hibernate.validator.cfg, org.hibernate.validator.cfg.context, org.hibernate.validator.cfg.defs, org.hibernate.validator.spi.cfg

Hibernate Validator 约束声明的 fluent API;在 org.hibernate.validator.cfg 中,您将找到 ConstraintMapping 接口,在 org.hibernate.validator.cfg.defs 中找到所有约束定义,在 org.hibernate.validator.spi.cfg 中找到用于配置默认验证器工厂的 API 的回调。有关详细信息,请参阅 Section 12.4, “Programmatic constraint definition and declaration”

org.hibernate.validator.constraints, org.hibernate.validator.constraints.br, org.hibernate.validator.constraints.pl

除了 Jakarta Bean 验证规范定义的内置约束外,Hibernate 验证器提供的一些有用的自定义约束;约束在 Section 2.3.2, “Additional constraints” 中进行了详细描述。

org.hibernate.validator.constraintvalidation

扩展的约束验证器上下文,该上下文允许为消息插值设置自定义属性。 Section 12.13.1, “HibernateConstraintValidatorContext 介绍了如何利用该功能。

org.hibernate.validator.group, org.hibernate.validator.spi.group

组序列提供程序功能,该功能允许您根据已验证对象状态定义动态默认组序列;具体内容可以在 Section 5.4, “Redefining the default group sequence” 中找到。

org.hibernate.validator.messageinterpolation, org.hibernate.validator.resourceloading, org.hibernate.validator.spi.resourceloading

与约束消息插值相关的类;第一个软件包包含 Hibernate 验证器的默认消息内插器 ResourceBundleMessageInterpolator 。后两个软件包提供了用于加载资源包的 ResourceBundleLocator SPI(请参阅 Section 4.2.1, “ResourceBundleLocator )及其默认实现。

org.hibernate.validator.parameternameprovider

基于 Paranamer 库的 ParameterNameProvider ,请参阅 Section 12.14, “Paranamer based ParameterNameProvider

org.hibernate.validator.propertypath

jakarta.validation.Path API 的扩展,请参阅 Section 12.7, “Extensions of the Path API”

org.hibernate.validator.spi.constraintdefinition

用于以编程方式注册其他约束验证器的 SPI,请参阅 Section 12.15, “Providing constraint definitions”

org.hibernate.validator.spi.messageinterpolation

用于在插值约束违规消息时调整区域设置解析的 SPI。请参阅 Section 12.12, “Customizing the locale resolution”

org.hibernate.validator.spi.nodenameprovider

用于在构造属性路径时修改属性名称解析方式的 SPI。请参阅 Section 12.18, “Customizing the property name resolution for constraint violations”

Hibernate Validator 的公共包分为两类:而实际 API 部分旨在由客户端 invokedused(例如用于以编程方式声明约束的 API 或自定义约束),但 SPI(服务提供程序接口)包包含旨在 implemented 让客户端(例如 ResourceBundleLocator)。

该表中未列出的任何包都是 Hibernate 验证程序的内部包,不打算由客户端访问。这些内部包的内容可能在版本之间发生变化而无需通知,从而可能破坏依赖它们的任何客户端代码。

12.2. Fail fast mode

使用快速失败模式,Hibernate 验证器允许在发生第一个约束违反时立即返回当前验证。这对于验证大型对象图表很有用,而您只需要快速检查是否存在任何约束违反。

Example 12.1, “Using the fail fast validation mode”显示如何引导并使用已启用快速失败的验证器。

示例 12.1:使用快速失败验证模式
package org.hibernate.validator.referenceguide.chapter12.failfast;

public class Car {

    @NotNull
    private String manufacturer;

    @AssertTrue
    private boolean isRegistered;

    public Car(String manufacturer, boolean isRegistered) {
        this.manufacturer = manufacturer;
        this.isRegistered = isRegistered;
    }

    //getters and setters...
}
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .buildValidatorFactory()
        .getValidator();

Car car = new Car( null, false );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

这里,经过验证的对象实际上未能满足 Car 类中声明的两个约束,但由于启用了快速失败模式,因此验证调用只产生一个 ConstraintViolation

不能保证约束的评估顺序,也就是说,无法确定返回的违规是否来自 @NotNull@AssertTrue 约束。如果需要,可以使用 Section 5.3, “Defining group sequences” 中描述的分组序列来强制执行确定性的评估顺序。

请参阅 Section 9.2.8, “Provider-specific settings”以了解在引导验证器时启用快速失败模式的不同方法。

12.3. Relaxation of requirements for method validation in class hierarchies

Jakarta Bean Validation 规范定义了一组在类层次结构中定义方法约束时适用的前提条件。这些前提条件在 Jakarta Bean Validation 规范的 section 5.6.5中定义。另请参阅本指南中的 Section 3.1.4, “Method constraints in inheritance hierarchies”

根据规范,Jakarta Bean Validation 提供者可以放宽这些前提条件。借助 Hibernate 验证器,您可以通过两种方式之一来执行此操作。

首先,您可以在 validation.xml_中使用配置属性_hibernate.validator.allow_parameter_constraint_overridehibernate.validator.allow_multiple_cascaded_validation_on_result_和_hibernate.validator.allow_parallel_method_parameter_constraint。请参见示例 Example 12.2, “Configuring method validation behaviour in class hierarchies via properties”

示例 12.2:通过属性配置类层次结构中的方法验证行为
<?xml version="1.0" encoding="UTF-8"?>
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">
    <default-provider>org.hibernate.validator.HibernateValidator</default-provider>

    <property name="hibernate.validator.allow_parameter_constraint_override">true</property>
    <property name="hibernate.validator.allow_multiple_cascaded_validation_on_result">true</property>
    <property name="hibernate.validator.allow_parallel_method_parameter_constraint">true</property>
</validation-config>

或者,这些设置可以在编程引导期间应用。

示例 12.3:配置类层次结构中的方法验证行为
HibernateValidatorConfiguration configuration = Validation.byProvider( HibernateValidator.class ).configure();

configuration.allowMultipleCascadedValidationOnReturnValues( true )
        .allowOverridingMethodAlterParameterConstraint( true )
        .allowParallelMethodsDefineParameterConstraints( true );

默认情况下,所有这些属性都为 false,实现了 Jakarta Bean Validation 规范中定义的默认行为。

更改方法验证的默认行为将导致不符合规范且不可移植的应用程序。确保了解自己正在做什么,并且您的用例确实需要更改默认行为。

12.4. Programmatic constraint definition and declaration

根据 Jakarta Bean Validation 规范,您可以使用 Java 注释和基于 XML 的约束映射来定义和声明约束。

此外,Hibernate 验证器提供了一个流畅的 API,允许以编程方式配置约束。用例包括根据某些应用程序状态动态添加约束或在不同场景中需要具有不同约束的实体的测试,但不希望为每个测试用例实现实际的 Java 类。

默认情况下,通过流畅 API 添加的约束会附加到通过标准配置功能配置的约束。但在需要时,也可以忽略已配置的注释和 XML 约束。

该 API 以_ConstraintMapping_接口为中心。您可以通过_HibernateValidatorConfiguration#createConstraintMapping()_获得新映射,然后以流畅的方式对其进行配置,如 Example 12.4, “Programmatic constraint declaration”所示。

示例 12.4:以编程方式声明约束
HibernateValidatorConfiguration configuration = Validation
        .byProvider( HibernateValidator.class )
        .configure();

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "manufacturer" )
            .constraint( new NotNullDef() )
        .field( "licensePlate" )
            .ignoreAnnotations( true )
            .constraint( new NotNullDef() )
            .constraint( new SizeDef().min( 2 ).max( 14 ) )
    .type( RentalCar.class )
        .getter( "rentalStation" )
            .constraint( new NotNullDef() );

Validator validator = configuration.addMapping( constraintMapping )
        .buildValidatorFactory()
        .getValidator();

可以使用方法链在多个类和属性上配置约束。约束定义类 NotNullDefSizeDef 是帮助类,允许以类型安全的方式配置约束参数。org.hibernate.validator.cfg.defs 包中所有内置约束都有定义类。通过调用 ignoreAnnotations(),将忽略通过注释或 XML 为给定元素配置的任何约束。

每个元素(类型、属性、方法等)只能在一个验证器工厂设置中所使用的所有约束映射中配置一次。否则将引发 ValidationException

不支持通过配置子类型向非覆盖的超类型属性和方法添加约束。相反,您需要在这种情况下配置超类型。

配置映射后,你必须将其添加回配置对象,然后可以从中获取验证器工厂。

对于自定义约束,您可以创建自己的定义类来扩展_ConstraintDef_,也可以使用_GenericConstraintDef_(如 Example 12.5, “Programmatic declaration of a custom constraint”所示)。

示例 12.5:以编程方式声明自定义约束

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "licensePlate" )
            .constraint( new GenericConstraintDef<>( CheckCase.class )
                .param( "value", CaseMode.UPPER )
            );

容器元素约束受程序化 API 支持,使用 containerElementType()

Example 12.6, “Programmatic declaration of a nested container element constraint”显示了在嵌套容器元素上声明约束的示例。

示例 12.6:以编程方式声明嵌套容器元素约束

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "manufacturer" )
            .constraint( new NotNullDef() )
        .field( "licensePlate" )
            .ignoreAnnotations( true )
            .constraint( new NotNullDef() )
            .constraint( new SizeDef().min( 2 ).max( 14 ) )
        .field( "partManufacturers" )
            .containerElementType( 0 )
                .constraint( new NotNullDef() )
            .containerElementType( 1, 0 )
                .constraint( new NotNullDef() )
    .type( RentalCar.class )
        .getter( "rentalStation" )
            .constraint( new NotNullDef() );

如所示,传递给 containerElementType() 的参数是用于获取所需的嵌套容器元素类型的类型参数索引的路径。

通过调用 valid(),您可以标记一个成员进行级联验证,这等效于使用 @Valid_对其进行注解。使用 _convertGroup()_方法(等效于 _@ConvertGroup)配置在级联验证期间应用的任何组转换。可以在 Example 12.7, “Marking a property for cascaded validation”中看到一个示例。

示例 12.7:标记一个属性进行级联验证

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .field( "driver" )
            .constraint( new NotNullDef() )
            .valid()
            .convertGroup( Default.class ).to( PersonDefault.class )
        .field( "partManufacturers" )
            .containerElementType( 0 )
                .valid()
            .containerElementType( 1, 0 )
                .valid()
    .type( Person.class )
        .field( "name" )
            .constraint( new NotNullDef().groups( PersonDefault.class ) );

您不仅可以使用流畅 API 配置 Bean 约束,还可以配置方法和构造函数约束。如 Example 12.8, “Programmatic declaration of method and constructor constraints”所示,构造函数由其参数类型标识,而方法则由其名称和参数类型标识。选择了一个方法或构造函数后,您可以标记其参数和/或返回值以进行级联验证,并添加约束以及跨参数约束。

如示例中所示,也可以对容器元素类型调用 valid()

示例 12.8:以编程方式声明方法和构造函数约束

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .constructor( String.class )
            .parameter( 0 )
                .constraint( new SizeDef().min( 3 ).max( 50 ) )
            .returnValue()
                .valid()
        .method( "drive", int.class )
            .parameter( 0 )
                .constraint( new MaxDef().value( 75 ) )
        .method( "load", List.class, List.class )
            .crossParameter()
                .constraint( new GenericConstraintDef<>(
                        LuggageCountMatchesPassengerCount.class ).param(
                            "piecesOfLuggagePerPassenger", 2
                        )
                )
        .method( "getDriver" )
            .returnValue()
                .constraint( new NotNullDef() )
                .valid();

最后但并非最不重要的是,你可以配置类型的默认组序列或默认组序列提供程序,如下面的示例所示。

示例 12.9:默认组顺序和默认组顺序提供程序的配置

ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
    .type( Car.class )
        .defaultGroupSequence( Car.class, CarChecks.class )
    .type( RentalCar.class )
        .defaultGroupSequenceProviderClass( RentalCarGroupSequenceProvider.class );

12.5. Applying programmatic constraint declarations to the default validator factory

如果您不是手动自举验证程序工厂,而是使用通过 META-INF/validation.xml 配置的默认工厂(请参见 Chapter 8, Configuring via XML ),您可以通过创建一个或多个约束映射贡献者来添加一个或多个约束映射。为此,请实现 ConstraintMappingContributor 契约:

示例 12.10:自定义 ConstraintMappingContributor 实现

package org.hibernate.validator.referenceguide.chapter12.constraintapi;

public class MyConstraintMappingContributor implements ConstraintMappingContributor {

    @Override
    public void createConstraintMappings(ConstraintMappingBuilder builder) {
        builder.addConstraintMapping()
            .type( Marathon.class )
                .getter( "name" )
                    .constraint( new NotNullDef() )
                .field( "numberOfHelpers" )
                    .constraint( new MinDef().value( 1 ) );

        builder.addConstraintMapping()
            .type( Runner.class )
                .field( "paidEntryFee" )
                    .constraint( new AssertTrueDef() );
    }
}

然后,你需要在 META-INF/validation.xml 中使用属性 key hibernate.validator.constraint_mapping_contributors 指定生成器实现的完全限定类名。你可以通过逗号分隔来指定多个生成器。

12.6. Advanced constraint composition features

12.6.1. Validation target specification for purely composed constraints

如果你在方法声明上指定了一个纯粹的组合约束(即一个本身没有验证器的约束,但仅由其他组合约束组成),验证引擎无法确定是否要将该约束应用为返回值约束或跨参数约束。

Hibernate Validator 允许通过在复合约束类型声明中指定 @SupportedValidationTarget_注解(如 Example 12.11, “Specifying the validation target of a purely composed constraint”所示)来解决此类歧义。@ValidInvoiceAmount_不声明任何验证器,但它完全由_@Min_和_@NotNull_约束组成。当在方法声明中给定时,_@SupportedValidationTarget_确保约束应用于方法返回值。

示例 12.11:指定纯复合约束的验证目标

package org.hibernate.validator.referenceguide.chapter12.purelycomposed;

@Min(value = 0)
@NotNull
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {})
@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
@ReportAsSingleViolation
public @interface ValidInvoiceAmount {

    String message() default "{org.hibernate.validator.referenceguide.chapter11.purelycomposed."
            + "ValidInvoiceAmount.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @OverridesAttribute(constraint = Min.class, name = "value")
    long value();
}

12.6.2. Boolean composition of constraints

Jakarta Bean Validation 规定,复合约束(见 Section 6.4, “Constraint composition”)的约束通过逻辑_AND_组合。这意味着所有组成约束都需要返回 true 才能获得总体成功的验证。

Hibernate Validator 提供了对此的扩展,并允许你通过逻辑 ORNOT 来组合约束。为此,你必须使用 ConstraintComposition 注释和枚举 CompositionType 及其值 ANDORALL_FALSE

Example 12.12, “OR composition of constraints”显示如何构建复合约束 @PatternOrSize,其中为了通过验证,只需要一个组成约束有效即可。已验证的字符串要么全部为小写,要么长度在 2 到 3 个字符之间。

示例 12.12:约束的 OR 组合

package org.hibernate.validator.referenceguide.chapter12.booleancomposition;

@ConstraintComposition(OR)
@Pattern(regexp = "[a-z]")
@Size(min = 2, max = 3)
@ReportAsSingleViolation
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
public @interface PatternOrSize {
    String message() default "{org.hibernate.validator.referenceguide.chapter11." +
            "booleancomposition.PatternOrSize.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };
}

隐式地使用 ALL_FALSE 作为组成类型强制执行只在约束组合验证失败时报告单 个违规行为。

12.7. Extensions of the Path API

Hibernate Validator 提供了对 jakarta.validation.Path API 的扩展。对于 ElementKind.PROPERTYElementKind.CONTAINER_ELEMENT 的节点,它允许获取所表示属性的值。为此,可以使用 Node#as() 将给定节点分别缩小为 org.hibernate.validator.path.PropertyNodeorg.hibernate.validator.path.ContainerElementNode 类型,如下面的示例所示:

示例 12.13:从属性节点获取值

Building building = new Building();

// Assume the name of the person violates a @Size constraint
Person bob = new Person( "Bob" );
Apartment bobsApartment = new Apartment( bob );
building.getApartments().add( bobsApartment );

Set<ConstraintViolation<Building>> constraintViolations = validator.validate( building );

Path path = constraintViolations.iterator().next().getPropertyPath();
Iterator<Path.Node> nodeIterator = path.iterator();

Path.Node node = nodeIterator.next();
assertEquals( node.getName(), "apartments" );
assertSame( node.as( PropertyNode.class ).getValue(), bobsApartment );

node = nodeIterator.next();
assertEquals( node.getName(), "resident" );
assertSame( node.as( PropertyNode.class ).getValue(), bob );

node = nodeIterator.next();
assertEquals( node.getName(), "name" );
assertEquals( node.as( PropertyNode.class ).getValue(), "Bob" );

这也非常便于获取属性路径上 Set 属性的元素(例如示例中的 apartments),否则该元素将无法识别(与 MapList 不同,在这种情况下没有键或索引)。

12.8. Dynamic payload as part of ConstraintViolation

有时,如果约束冲突提供附加数据(即所谓的动态负载),可以辅助对冲突进行自动处理。例如,此动态负载可以包含提示用户如何解决冲突的提示。

可以在 custom constraints 中使用 HibernateConstraintValidatorContext 设置动态负载。示例 Example 12.14, “ConstraintValidator implementation setting a dynamic payload” 中显示了此操作,其中 jakarta.validation.ConstraintValidatorContext 被解包到 HibernateConstraintValidatorContext 中以调用 withDynamicPayload

示例 12.14:设定动态负载的 ConstraintValidator 实现

package org.hibernate.validator.referenceguide.chapter12.dynamicpayload;

import static org.hibernate.validator.internal.util.CollectionHelper.newHashMap;

public class ValidPassengerCountValidator implements ConstraintValidator<ValidPassengerCount, Car> {

    private static final Map<Integer, String> suggestedCars = newHashMap();

    static {
        suggestedCars.put( 2, "Chevrolet Corvette" );
        suggestedCars.put( 3, "Toyota Volta" );
        suggestedCars.put( 4, "Maserati GranCabrio" );
        suggestedCars.put( 5, " Mercedes-Benz E-Class" );
    }

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {
    }

    @Override
    public boolean isValid(Car car, ConstraintValidatorContext context) {
        if ( car == null ) {
            return true;
        }

        int passengerCount = car.getPassengers().size();
        if ( car.getSeatCount() >= passengerCount ) {
            return true;
        }
        else {

            if ( suggestedCars.containsKey( passengerCount ) ) {
                HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                        HibernateConstraintValidatorContext.class
                );
                hibernateContext.withDynamicPayload( suggestedCars.get( passengerCount ) );
            }
            return false;
        }
    }
}

在约束冲突处理方面,然后可以将 jakarta.validation.ConstraintViolation 解包为 HibernateConstraintViolation,以便检索动态负载进行进一步处理。

示例 12.15:检索 ConstraintViolation 的动态负载

Car car = new Car( 2 );
car.addPassenger( new Person() );
car.addPassenger( new Person() );
car.addPassenger( new Person() );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );

ConstraintViolation<Car> constraintViolation = constraintViolations.iterator().next();
HibernateConstraintViolation<Car> hibernateConstraintViolation = constraintViolation.unwrap(
        HibernateConstraintViolation.class
);
String suggestedCar = hibernateConstraintViolation.getDynamicPayload( String.class );
assertEquals( "Toyota Volta", suggestedCar );

12.9. Enabling Expression Language features

Hibernate Validator 限制了默认公开的表达式语言功能。

为此目的,我们在 ExpressionLanguageFeatureLevel 中定义了多个功能级别:

  1. NONE:表达式语言插值完全被禁用。

  2. VARIABLES:允许插值通过 addExpressionVariable() 注入的变量、资源包以及使用 formatter 对象。

  3. BEAN_PROPERTIES:允许 VARIABLES 允许的一切内容以及 Bean 属性的插值。

  4. BEAN_METHODS:还允许执行 Bean 方法。如果处理不当,这会导致严重的安全性问题,包括任意代码执行。

根据上下文,我们公开的功能是不同的:

  1. 对于约束,默认级别为 BEAN_PROPERTIES。为了正确内插所有内置约束消息,您至少需要 VARIABLES 级别。

  2. 对于自定义违规行为,通过 ConstraintValidatorContext 创建,默认情况下禁用表达式语言。您可以为特定的自定义违规行为启用它,并且在启用时,它将默认为 VARIABLES

引导 ValidatorFactory 时,Hibernate Validator 提供了方法来覆盖这些默认值。

要在约束中更改表达式语言功能级别,请使用以下内容:

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .constraintExpressionLanguageFeatureLevel( ExpressionLanguageFeatureLevel.VARIABLES )
        .buildValidatorFactory();

要在特定冲突中更改表达式语言功能级别,请使用以下内容:

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .customViolationExpressionLanguageFeatureLevel( ExpressionLanguageFeatureLevel.VARIABLES )
        .buildValidatorFactory();

执行此操作将自动为应用程序中的所有自定义违规启用表达式语言。

它应仅用于兼容性并简化从旧版 Hibernate Validator 版本的迁移。

还可以使用以下属性定义这些级别:

  1. hibernate.validator.constraint_expression_language_feature_level

  2. hibernate.validator.custom_violation_expression_language_feature_level

这些属性的接受值包括:nonevariablesbean-propertiesbean-methods

12.10. ParameterMessageInterpolator

默认情况下,Hibernate Validator 要求 Unified EL(请参见 Section 1.1.1, “Unified EL”)的实现可用。这是为了允许使用 Jakarta Bean Validation 规范定义的 EL 表达式插值约束错误消息。

对于无法或不希望提供 EL 实现的环境,Hibernate Validator 提供了基于非 EL 的消息内插器 - org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator

请参阅 Section 4.2, “Custom message interpolation”以了解如何插入自定义消息插值器实现。

包含 EL 表达式的约束消息将以未插值的形式由 org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator 返回。这也影响了 使用 EL 表达式的内置默认约束消息。目前,DecimalMinDecimalMax 会受到影响。

12.11. ResourceBundleLocator

通过 ResourceBundleLocator ,Hibernate Validator 提供了一个其他 SPI,允许从 ValidationMessages 以外的其他资源包中检索错误消息,同时仍在使用由规范定义的实际内插算法。请参阅 Section 4.2.1, “ResourceBundleLocator 以了解如何利用该 SPI。

12.12. Customizing the locale resolution

这些合约已标记为 @Incubating,因此将来可能会更改。

Hibernate 验证程序提供多个扩展点来构建自定义区域设置解析策略。在内插约束违规消息时,使用已解析的区域设置。

Hibernate Validator 的默认行为是始终使用系统默认语言环境(通过 Locale.getDefault() 获得)。如果(例如)你通常将你的系统语言环境设置为 en-US 但希望你的应用程序提供法语消息,则这可能不是你期望的行为。

以下示例展示了如何将 Hibernate Validator 默认语言环境设置为 fr-FR

示例 12.16:配置默认区域设置

Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .defaultLocale( Locale.FRANCE )
        .buildValidatorFactory()
        .getValidator();

Set<ConstraintViolation<Bean>> violations = validator.validate( new Bean() );
assertEquals( "doit avoir la valeur vrai", violations.iterator().next().getMessage() );

虽然这已是一个不错的改进,但在完全国际化的应用程序中,这还不够:你需要 Hibernate 根据用户上下文选择语言环境。

Hibernate Validator 提供 org.hibernate.validator.spi.messageinterpolation.LocaleResolver SPI,该 SPI 允许微调语言环境解析。通常,在 JAX-RS 环境中,你可以从 Accept-Language HTTP 标头解析需要使用的语言环境。

在以下示例中,我们使用了硬编码值但是,例如,对于 RESTEasy 应用程序,你可以从 ResteasyContext 提取标头。

示例 12.17:通过 LocaleResolver 微调用于内插消息的区域设置

LocaleResolver localeResolver = new LocaleResolver() {

    @Override
    public Locale resolve(LocaleResolverContext context) {
        // get the locales supported by the client from the Accept-Language header
        String acceptLanguageHeader = "it-IT;q=0.9,en-US;q=0.7";

        List<LanguageRange> acceptedLanguages = LanguageRange.parse( acceptLanguageHeader );
        List<Locale> resolvedLocales = Locale.filter( acceptedLanguages, context.getSupportedLocales() );

        if ( resolvedLocales.size() > 0 ) {
            return resolvedLocales.get( 0 );
        }

        return context.getDefaultLocale();
    }
};

Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .defaultLocale( Locale.FRANCE )
        .locales( Locale.FRANCE, Locale.ITALY, Locale.US )
        .localeResolver( localeResolver )
        .buildValidatorFactory()
        .getValidator();

Set<ConstraintViolation<Bean>> violations = validator.validate( new Bean() );
assertEquals( "deve essere true", violations.iterator().next().getMessage() );

使用 LocaleResolver 时,必须通过 locales() 方法定义支持的语言环境列表。

12.13. Custom contexts

Jakarta Bean Validation 规范在 API 中提供了在多个节点处将给定接口解包到特定于实现的子类型中的可能。在 ConstraintValidator 实现中的约束违例创建和 MessageInterpolator 实例中的消息插值的情况下,存在 unwrap() 方法用于提供的上下文实例——分别为 ConstraintValidatorContextMessageInterpolatorContext。Hibernate Validator 为这两个接口提供自定义扩展。

12.13.1. HibernateConstraintValidatorContext

HibernateConstraintValidatorContextConstraintValidatorContext 的子类型,它允许您:

  1. 为特定自定义违规行为启用表达式语言内插 - 参见下文

  2. 使用 HibernateConstraintValidatorContext#addExpressionVariable(String, Object)HibernateConstraintValidatorContext#addMessageParameter(String, Object) 通过表达式语言消息内插工具为内插设置任意参数。

示例 155. 自定义 @Future 验证器注入表达式变量_程序包 org.hibernate.validator.referenceguide.chapter12.context;

public class MyFutureValidator implements ConstraintValidator<Future, Instant> {

@Override public void initialize(Future constraintAnnotation) { }

@Override public boolean isValid(Instant value, ConstraintValidatorContext context) { if ( value == null ) { return true; }

HibernateConstraintValidatorContext hibernateContext = context.unwrap( HibernateConstraintValidatorContext.class );

Instant now = Instant.now( context.getClockProvider().getClock() );

if ( !value.isAfter( now ) ) { hibernateContext.disableDefaultConstraintViolation(); hibernateContext .addExpressionVariable( "now", now ) .buildConstraintViolationWithTemplate( "Must be after ${now}" ) .addConstraintViolation();

    return false;
}
        return true;
    }
}_
Example 156. Custom _@Future_ validator injecting a message parameter_package org.hibernate.validator.referenceguide.chapter12.context;

public class MyFutureValidatorMessageParameter implements ConstraintValidator<Future, Instant> {

@Override public void initialize(Future constraintAnnotation) { }

@Override public boolean isValid(Instant value, ConstraintValidatorContext context) { if ( value == null ) { return true; }

HibernateConstraintValidatorContext hibernateContext = context.unwrap( HibernateConstraintValidatorContext.class );

Instant now = Instant.now( context.getClockProvider().getClock() );

if ( !value.isAfter( now ) ) { hibernateContext.disableDefaultConstraintViolation(); hibernateContext .addMessageParameter( "now", now ) .buildConstraintViolationWithTemplate( "Must be after {now}" ) .addConstraintViolation();

    return false;
}
        return true;
    }
}_Apart from the syntax, the main difference between message parameters and expression variables is that message parameters are simply interpolated whereas expression variables are interpreted using the Expression Language engine. In practice, use message parameters if you do not need the advanced features of an Expression Language.

请注意,通过 addExpressionVariable(String, Object)addMessageParameter(String, Object) 指定的参数是全局参数,并应用于此 isValid() 调 用创建的所有约束违规行为。这包括默认约束违规行为,以及通过 ConstraintViolationBuilder 创建的所有违规行为。但是,你可以在 ConstraintViolationBuilder#addConstraintViolation() 的调用之间更新参数。

  1. 设置一个任意的动态负载 - 参阅 Section 12.8, “Dynamic payload as part of ConstraintViolation

默认情况下,Expression Language 插值对自定义违规禁用,这是为了避免任意代码执行或在消息模板是由未正确转义的用户输入构建时发生敏感数据泄漏。

可以通过使用 enableExpressionLanguage() 为给定的自定义违例启用表达式语言,如下例所示:

public class SafeValidator implements ConstraintValidator<ZipCode, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }

        HibernateConstraintValidatorContext hibernateContext = context.unwrap(
                HibernateConstraintValidatorContext.class );
        hibernateContext.disableDefaultConstraintViolation();

        if ( isInvalid( value ) ) {
            hibernateContext
                    .addExpressionVariable( "validatedValue", value )
                    .buildConstraintViolationWithTemplate( "${validatedValue} is not a valid ZIP code" )
                    .enableExpressionLanguage()
                    .addConstraintViolation();

            return false;
        }

        return true;
    }

    private boolean isInvalid(String value) {
        // ...
        return false;
    }
}

在这种情况下,消息模板将由表达式语言引擎插值。

默认情况下,在启用表达式语言时仅启用变量插值。

您可以使用 HibernateConstraintViolationBuilder#enableExpressionLanguage(ExpressionLanguageFeatureLevel level) 启用更多功能。

我们为表达式语言插值定义了几个级别的功能:

  1. NONE:表达式语言内插完全禁用 - 这是自定义违规行为的默认设置。

  2. VARIABLES:允许插值通过 addExpressionVariable() 注入的变量、资源包以及使用 formatter 对象。

  3. BEAN_PROPERTIES:允许 VARIABLES 允许的一切内容以及 Bean 属性的插值。

  4. BEAN_METHODS:还允许执行 Bean 方法。如果处理不当,这会导致严重的安全性问题,包括任意代码执行。====== 使用 addExpressionVariable() 是将变量注入表达式中的唯一安全方法,如果你使用 BEAN_PROPERTIES 或 5 @{s} 功能级别,那它尤其重要。

如果你通过在消息中简单的连接用户输入来注入用户输入,那将允许执行任意代码并造成敏感数据泄漏:如果用户输入包含有效的表达式,那它们将由 Expression Language 引擎执行。

以下是你绝对不应做的事情的一个示例:

_public class UnsafeValidator implements ConstraintValidator<ZipCode, String> {

@Override public boolean isValid(String value, ConstraintValidatorContext context) { if ( value == null ) { return true; }

context.disableDefaultConstraintViolation();

HibernateConstraintValidatorContext hibernateContext = context.unwrap( HibernateConstraintValidatorContext.class ); hibernateContext.disableDefaultConstraintViolation();

if ( isInvalid( value ) ) { hibernateContext // THIS IS UNSAFE, DO NOT COPY THIS EXAMPLE .buildConstraintViolationWithTemplate( value + " is not a valid ZIP code" ) .enableExpressionLanguage() .addConstraintViolation();

    return false;
}
    return true;
}
    private boolean isInvalid(String value) {
        // ...
        return false;
    }
}_
_public class UnsafeValidator implements ConstraintValidator<ZipCode, String> {

@Override public boolean isValid(String value, ConstraintValidatorContext context) { if ( value == null ) { return true; }

context.disableDefaultConstraintViolation();

HibernateConstraintValidatorContext hibernateContext = context.unwrap( HibernateConstraintValidatorContext.class ); hibernateContext.disableDefaultConstraintViolation();

if ( isInvalid( value ) ) { hibernateContext // THIS IS UNSAFE, DO NOT COPY THIS EXAMPLE .buildConstraintViolationWithTemplate( value + " is not a valid ZIP code" ) .enableExpressionLanguage() .addConstraintViolation();

    return false;
}
    return true;
}
    private boolean isInvalid(String value) {
        // ...
        return false;
    }
}_
In the example above, if _value_, which might be user input, contains a valid expression, it will be interpolated by the Expression Language engine, potentially leading to unsafe behaviors.

==== 12.13.2. HibernateMessageInterpolatorContext

Hibernate Validator 也提供了一个 MessageInterpolatorContext 的自定义扩展,即 HibernateMessageInterpolatorContext (参阅 Example 12.18, “HibernateMessageInterpolatorContext )。引入此子类型是为了允许 Hibernate Validator 更好地集成到 Glassfish 中。在这种情况下,需要根 bean 类型以确定消息资源包的正确类加载器。如果你有其他用例,请告知我们。

Example 12.18: HibernateMessageInterpolatorContext
public interface HibernateMessageInterpolatorContext extends MessageInterpolator.Context {

    /**
     * Returns the currently validated root bean type.
     *
     * @return The currently validated root bean type.
     */
    Class<?> getRootBeanType();

    /**
     * @return the message parameters added to this context for interpolation
     *
     * @since 5.4.1
     */
    Map<String, Object> getMessageParameters();

    /**
     * @return the expression variables added to this context for EL interpolation
     *
     * @since 5.4.1
     */
    Map<String, Object> getExpressionVariables();

    /**
     * @return the path to the validated constraint starting from the root bean
     *
     * @since 6.1
     */
    Path getPropertyPath();

    /**
     * @return the level of features enabled for the Expression Language engine
     *
     * @since 6.2
     */
    ExpressionLanguageFeatureLevel getExpressionLanguageFeatureLevel();
}

=== 12.14. Paranamer based ParameterNameProvider

Hibernate Validator 带有一个 ParameterNameProvider 实现,它利用 Paranamer 库。

该库提供了在运行时获取参数名称的多种方式,例如基于 Java 编译器创建的调试符号、在后编译步骤中以字节码形式插入参数名称的常量或如 JSR 330 中的 @Named 注释。

为了使用 ParanamerParameterNameProvider ,可以在引导验证器时传递一个实例,如 Example 9.10, “Using a custom ParameterNameProvider 中所示,或将 org.hibernate.validator.parameternameprovider.ParanamerParameterNameProvider 指定为 META-INF/validation.xml 文件中 <parameter-name-provider> 元素的值。

当使用此参数名称提供程序时,你需要将 Paranamer 库添加到你的类路径中。它在 Maven 中心仓库中提供,组 ID 为 com.thoughtworks.paranamer,构件 ID 为 paranamer

默认情况下,ParanamerParameterNameProvider 从构建时添加到字节码的常量(通过 DefaultParanamer)和调试符号(通过 BytecodeReadingParanamer)中检索参数名称。或者,你可以在创建 ParanamerParameterNameProvider 实例时指定你自己选择的 Paranamer 实现。

=== 12.15. Providing constraint definitions

Jakarta Bean Validation 允许在约束映射文件中通过 XML 重新定义约束定义。有关更多信息,请参阅 Section 8.2, “Mapping constraints via constraint-mappings ,有关示例,请参阅 Example 8.2, “Bean constraints configured via XML” 。虽然此方法足以满足许多用例,但它在其他方面有其缺点。例如,想象一个约束库希望为自定义类型添加约束定义。此库可以提供包含其库的映射文件,但此文件仍需要库的用户引用。幸运的是,有更好的方法。

以下概念目前被视为实验性的。请告知我们你发现它们是否有用,以及它们是否满足你的需求。

==== 12.15.1. Constraint definitions via ServiceLoader

Hibernate Validator 允许利用 Java 的 ServiceLoader 机制来注册额外的约束定义。您只需将 jakarta.validation.ConstraintValidator 文件添加到 META-INF/services 中。在这个服务文件中,您列出约束验证器类的完全限定类名(每行一个)。Hibernate Validator 会自动推断出它们适用的约束类型。请查看 Constraint definition via service file 查看示例。

Example 12.19: META-INF/services/jakarta.validation.ConstraintValidator
# Assuming a custom constraint annotation @org.mycompany.CheckCase
org.mycompany.CheckCaseValidator

要为自定义约束提供默认消息,请将文件 ContributorValidationMessages.properties 和/或其特定区域设置的专有化置于 JAR 的根目录中。除了在 ValidationMessages.properties 中给出的那些消息之外,Hibernate Validator 还将考虑类路径中发现的具有该名称的所有包中的条目。

在创建大型多模块应用程序时,此机制也很有帮助:您不必将所有约束消息放入一个包中,而是可以为包含该模块中那些消息的每个模块设置一个资源包。

我们强烈建议阅读 this blog post by Marko Bekhta,一步步指导你完成创建包含自定义约束并通过 ServiceLoader 声明它们的独立 JAR 的过程。

==== 12.15.2. Adding constraint definitions programmatically

虽然服务加载程序方法可在许多场景中使用,但不能在所有场景中使用(例如,服务文件不可见的 OSGi),但是还有另一种方式来提供约束定义。您可以使用编程约束声明 API - 请参阅 Example 12.20, “Adding constraint definitions through the programmatic API”

Example 12.20:通过编程 API 添加约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
        .constraintDefinition( ValidPassengerCount.class )
        .validatedBy( ValidPassengerCountValidator.class );

如果验证器实现比较简单(即无需从注释进行初始化,且不使用 ConstraintValidatorContext),还可以使用此替代 API 来使用 Lambda 表达式或方法引用指定约束逻辑:

Example 12.21:使用 Lambda 表达式添加约束定义
ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping
        .constraintDefinition( ValidPassengerCount.class )
            .validateType( Bus.class )
                .with( b -> b.getSeatCount() >= b.getPassengers().size() );

你可以使用一个 ConstraintMappingContributor ,按照 Section 12.5, “Applying programmatic constraint declarations to the default validator factory” 中的详细说明,而不是直接将约束映射添加到配置对象。在使用 META-INF/validation.xml 配置默认验证器工厂时,这可能很有用(参阅 Chapter 8, Configuring via XML )。

通过编程 API 注册约束定义的一个用例是能够为 @URL 约束指定备用约束验证器。从历史上看,Hibernate Validator 的此约束的默认约束验证器使用 java.net.URL 构造函数验证 URL。但是,还可以使用 ConstraintDefinitionContributor 配置的基于纯正则表达式的版本:

使用编程约束声明 API 注册一个基于正则表达式的约束定义以获取 _@URL__ConstraintMapping constraintMapping = configuration.createConstraintMapping();

constraintMapping .constraintDefinition( URL.class ) .includeExistingValidators( false ) .validatedBy( RegexpURLValidator.class );_

使用基于程序约束声明的 API 为 @URL _ConstraintMapping constraintMapping = configuration.createConstraintMapping() 注册一个基于正则表达式的约束定义。

constraintMapping .constraintDefinition( URL.class ) .includeExistingValidators( false ) .validatedBy( RegexpURLValidator.class );_

=== 12.16. Customizing class-loading

在以下几种情况下,Hibernate Validator 需要加载以名称给出的资源或类:

  1. XML 描述符(以及 XML 约束映射)

  2. XML 描述符中按名称指定的类(例如自定义消息内插器等)

  3. the ValidationMessages resource bundle

  4. 用于基于表达式的消息内插的 ExpressionFactory 实现

默认情况下,Hibernate 验证器尝试通过当前线程上下文类加载器加载这些资源。如果这样做不成功,则 Hibernate 验证器自己的类加载器将被尝试作为备选方案。

在该策略不合适的情况下(例如模块化环境,如 OSGi),您可以在启动验证器工厂时为加载这些资源提供一个特定的类加载器:

示例 12.22:提供一个类加载器来加载外部资源和类
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .externalClassLoader( classLoader )
        .buildValidatorFactory()
        .getValidator();

在 OSGi 的情况下,您可以传递一个类加载器,该类加载器来自引导 Hibernate 验证器的软件包或自定义类加载器实现(它委托给 Bundle#loadClass() 等)。

如果不再需要给定的验证器工厂实例,请调用 ValidatorFactory#close()。如果不这样做,可能会导致类加载器泄露,在应用程序/软件包重新部署的情况下,非关闭验证器工厂仍被应用程序代码引用。

=== 12.17. Customizing the getter property selection strategy

当由 Hibernate 验证器验证一个 Bean 时,它的属性也会得到验证。属性可以是字段或 getter。默认情况下,Hibernate 验证器遵循 JavaBeans 规范并且在下述条件之一为 true 时将方法视为 getter:

  1. 方法名称以 get 开头,它有非空返回类型且没有参数;

  2. 方法名称以 is 开头,返回类型为 boolean 且没有参数;

  3. 方法名称以 has 开头,返回类型为 boolean 且没有参数(此规则特定于 Hibernate Validator,并且不是 JavaBeans 规范要求的)

虽然在遵循经典 JavaBeans 惯例时这些规则通常是合适的,但可能发生这种情况,尤其是使用代码生成器时,JavaBeans 命名惯例未得到遵循并且 getter 的名称遵循不同的惯例。

在这种情况下,应重新定义检测 getter 的策略,以完全验证对象。

当类遵循流利的命名约定时,这也是需求的一个经典示例,如 Example 12.23, “A class that uses non-standard getters” 中所示。

示例 12.23:一个使用非标准 getter 的类
package org.hibernate.validator.referenceguide.chapter12.getterselectionstrategy;

public class User {

    private String firstName;
    private String lastName;
    private String email;

    // [...]

    @NotEmpty
    public String firstName() {
        return firstName;
    }

    @NotEmpty
    public String lastName() {
        return lastName;
    }

    @Email
    public String email() {
        return email;
    }
}

如果此类对象得到验证,则不执行 getter 上的验证,因为标准策略未检测到它们。

示例 12.24:使用默认 getter 属性选择策略验证一个具有非标准 getter 的类
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .buildValidatorFactory()
        .getValidator();

User user = new User( "", "", "not an email" );

Set<ConstraintViolation<User>> constraintViolations = validator.validate( user );

// as User has non-standard getters no violations are triggered
assertEquals( 0, constraintViolations.size() );

要使 Hibernate 验证器将此类方法视为属性,应配置一个自定义 GetterPropertySelectionStrategy。在此特定情况下,策略的一个可能实现是:

示例 12.25:自定义 GetterPropertySelectionStrategy 实现
package org.hibernate.validator.referenceguide.chapter12.getterselectionstrategy;

public class FluentGetterPropertySelectionStrategy implements GetterPropertySelectionStrategy {

    private final Set<String> methodNamesToIgnore;

    public FluentGetterPropertySelectionStrategy() {
        // we will ignore all the method names coming from Object
        this.methodNamesToIgnore = Arrays.stream( Object.class.getDeclaredMethods() )
                .map( Method::getName )
                .collect( Collectors.toSet() );
    }

    @Override
    public Optional<String> getProperty(ConstrainableExecutable executable) {
        if ( methodNamesToIgnore.contains( executable.getName() )
                || executable.getReturnType() == void.class
                || executable.getParameterTypes().length > 0 ) {
            return Optional.empty();
        }

        return Optional.of( executable.getName() );
    }

    @Override
    public List<String> getGetterMethodNameCandidates(String propertyName) {
        // As method name == property name, there always is just one possible name for a method
        return Collections.singletonList( propertyName );
    }
}

配置 Hibernate Validator 以使用此策略有多种方法。它可以通过编程方式(参见 Example 12.26, “Configuring a custom GetterPropertySelectionStrategy programmatically” )或通过 XML 配置中的 hibernate.validator.getter_property_selection_strategy 属性(参见 Example 12.27, “Configuring a custom GetterPropertySelectionStrategy using an XML property” )来完成。

示例 12.26:以编程方式配置一个自定义 GetterPropertySelectionStrategy
Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        // Setting a custom getter property selection strategy
        .getterPropertySelectionStrategy( new FluentGetterPropertySelectionStrategy() )
        .buildValidatorFactory()
        .getValidator();

User user = new User( "", "", "not an email" );

Set<ConstraintViolation<User>> constraintViolations = validator.validate( user );

assertEquals( 3, constraintViolations.size() );
示例 12.27:使用 XML 属性配置一个自定义 GetterPropertySelectionStrategy
<validation-config
        xmlns="https://jakarta.ee/xml/ns/validation/configuration"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
            https://jakarta.ee/xml/ns/validation/validation-configuration-3.0.xsd"
        version="3.0">

    <property name="hibernate.validator.getter_property_selection_strategy">
        org.hibernate.validator.referenceguide.chapter12.getterselectionstrategy.NoPrefixGetterPropertySelectionStrategy
    </property>

</validation-config>

需要提到的是,在使用 HibernateValidatorConfiguration#addMapping(ConstraintMapping) 添加程序约束的情况下,应该始终在配置所需的 getter 属性选择策略后才添加映射。否则,将对在定义策略之前添加的映射使用默认策略。

=== 12.18. Customizing the property name resolution for constraint violations

想象一下,我们有一个带有 @NotNull 约束的简单数据类:

示例 12.28:Person 数据类
public class Person {
    @NotNull
    @JsonProperty("first_name")
    private final String firstName;

    @JsonProperty("last_name")
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

通过使用 Jackson库,此类可序列化为 JSON:

示例 12.29:将 Person 对象序列化为 JSON
public class PersonSerializationTest {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Test
    public void personIsSerialized() throws JsonProcessingException {
        Person person = new Person( "Clark", "Kent" );

        String serializedPerson = objectMapper.writeValueAsString( person );

        assertEquals( "{\"first_name\":\"Clark\",\"last_name\":\"Kent\"}", serializedPerson );
    }
}

顾名思义,对象被序列化为:

示例 12.30:Person 作为 json
{
  "first_name": "Clark",
  "last_name": "Kent"
}

请注意,属性的名称如何不同。在 Java 对象中,我们有 firstNamelastName,而在 JSON 输出中,我们有 first_namelast_name。我们通过 @JsonProperty 注释自定义了此行为。

现在想象一下我们在 REST 环境中使用此类,其中用户可以在请求体中发送 a Person instance as JSON 。在指示验证失败的字段时,最好指示他们在 JSON 请求中使用的名称 first_name ,而不是我们在 Java 代码中内部使用的名称 firstName

org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider 合同允许我们这样做。通过实现它,我们可以定义在验证期间如何解析属性的名称。在我们的示例中,我们希望从 Jackson 配置中读取值。

这里有一个如何执行此操作的示例:

示例 12.31:JacksonPropertyNodeNameProvider 实现
import org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty;
import org.hibernate.validator.spi.nodenameprovider.Property;
import org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;

public class JacksonPropertyNodeNameProvider implements PropertyNodeNameProvider {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String getName(Property property) {
        if ( property instanceof JavaBeanProperty ) {
            return getJavaBeanPropertyName( (JavaBeanProperty) property );
        }

        return getDefaultName( property );
    }

    private String getJavaBeanPropertyName(JavaBeanProperty property) {
        JavaType type = objectMapper.constructType( property.getDeclaringClass() );
        BeanDescription desc = objectMapper.getSerializationConfig().introspect( type );

        return desc.findProperties()
                .stream()
                .filter( prop -> prop.getInternalName().equals( property.getName() ) )
                .map( BeanPropertyDefinition::getName )
                .findFirst()
                .orElse( property.getName() );
    }

    private String getDefaultName(Property property) {
        return property.getName();
    }
}

并在进行验证时:

示例 12.32:JacksonPropertyNodeNameProvider 用法
public class JacksonPropertyNodeNameProviderTest {
    @Test
    public void nameIsReadFromJacksonAnnotationOnField() {
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .propertyNodeNameProvider( new JacksonPropertyNodeNameProvider() )
                .buildValidatorFactory();

        Validator validator = validatorFactory.getValidator();

        Person clarkKent = new Person( null, "Kent" );

        Set<ConstraintViolation<Person>> violations = validator.validate( clarkKent );
        ConstraintViolation<Person> violation = violations.iterator().next();

        assertEquals( violation.getPropertyPath().toString(), "first_name" );
    }

我们可以看到属性路径现在返回 first_name

请注意,当 getter 中存在这些注解时,此操作同样有效:

示例 12.33:在 getter 上进行注释
@Test
public void nameIsReadFromJacksonAnnotationOnGetter() {
    ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
            .configure()
            .propertyNodeNameProvider( new JacksonPropertyNodeNameProvider() )
            .buildValidatorFactory();

    Validator validator = validatorFactory.getValidator();

    Person clarkKent = new Person( null, "Kent" );

    Set<ConstraintViolation<Person>> violations = validator.validate( clarkKent );
    ConstraintViolation<Person> violation = violations.iterator().next();

    assertEquals( violation.getPropertyPath().toString(), "first_name" );
}

public class Person {
    private final String firstName;

    @JsonProperty("last_name")
    private final String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @NotNull
    @JsonProperty("first_name")
    public String getFirstName() {
        return firstName;
    }
}

这只是我们想要更改属性名称解析方式的一个用例。

org.hibernate.validator.spi.nodenameprovider.PropertyNodeNameProvider 可以按你认为合适的任何方式实现以提供属性名称(例如,从注解中读取)。

值得一提的是还有另外两个界面:

  1. org.hibernate.validator.spi.nodenameprovider.Property 是一个基本接口,它包含有关属性的元数据。它有一个单一的 String getName() 方法,可用于获取属性的“原始”名称。此接口应被用作解析名称的默认方式(参见如何在 Example 12.31, “JacksonPropertyNodeNameProvider implementation” 中使用)。

  2. org.hibernate.validator.spi.nodenameprovider.JavaBeanProperty 是一个包含有关 Bean 属性的元数据的接口。它扩展了 org.hibernate.validator.spi.nodenameprovider.Property 并提供了一些附加方法,如 Class&lt;?&gt; getDeclaringClass(),它返回属性所有者的类。