Hibernate Validator 中文操作指南

4. Interpolating constraint error messages

消息插值是在违反 Jakarta Bean Validation 约束时创建错误消息的过程。在本章中,你将了解如何定义和解析此类消息,以及在默认算法不满足你的要求的情况下如何插入自定义消息插值器。

4.1. Default message interpolation

约束违规消息从所谓的描述符中检索。每个约束使用 message 属性定义其默认消息描述符。在声明时,可以使用特定值覆盖默认描述符,如 Example 4.1, “Specifying a message descriptor using the message attribute” 所示。

示例 4.1:使用 message 属性指定消息描述符
package org.hibernate.validator.referenceguide.chapter04;

public class Car {

    @NotNull(message = "The manufacturer name must not be null")
    private String manufacturer;

    //constructor, getters and setters ...
}

如果违反某约束,则验证引擎将使用当前配置的 MessageInterpolator 对其描述符进行插值。然后,可以通过调用 ConstraintViolation#getMessage() 从结果约束违规中检索插值后的错误消息。

消息描述符可以包含 message parametersmessage expressions,它们将在插值期间得到解析。消息参数是 {} 中包含的字符串文字,而消息表达式是 ${} 中包含的字符串文字。在方法插值期间应用以下算法:

  • 使用消息参数作为 ValidationMessages 资源包的键,来解析任何消息参数。如果此包包含给定消息参数的条目,那么该参数将使用包中的相应值替换消息中的参数。如果替换的值再次包含消息参数,则将递归执行此步骤。资源包预计由应用程序开发人员提供,例如通过将一个名为 ValidationMessages.properties 的文件添加到类路径中。您还可以通过提供此包的语言环境特定变体来创建本地化的错误消息,例如 ValidationMessages_en_US.properties。默认情况下,在包中查找消息时,将使用 JVM 的默认语言环境 (Locale#getDefault())。

  • 使用消息参数作为 Jakarta Bean Validation 规范的 B 附录中定义的内置约束的标准错误消息的资源包的键,来解析任何消息参数。对于 Hibernate Validator,此包名为 org.hibernate.validator.ValidationMessages。如果此步骤触发了替换,则再次执行步骤 1,否则应用步骤 3。

  • 通过将消息参数替换为具有相同名称的约束注释成员的来解析任何消息参数。这允许在错误消息中引用约束的属性值(例如 Size#min())(例如“必须至少为 ${min}”)。

  • 将任何消息表达式解析为统一表达式语言的表达式。请参阅 Section 4.1.2, “Interpolation with message expressions” 了解更多有关在错误消息中使用统一 EL 的信息。

你可以在 Jakarta Bean Validation 规范的第 6.3.1.1 节中找到插值算法的正式定义。

4.1.1. Special characters

由于字符 {}$ 在消息描述符中具有特殊含义,如果要按字面意思使用它们,则需要对它们进行转义。下述规则适用:

  1. \{ 被认为是文本 {

  2. \} 被认为是文本 }

  3. \$ 被认为是文本 $

  4. \\ 被认为是文本 \

4.1.2. Interpolation with message expressions

从 Hibernate Validator 5(Bean Validation 1.1)开始,可以在约束违反消息中使用 Jakarta Expression Language。这允许基于条件逻辑定义错误消息,也支持高级格式化选项。验证引擎在 EL 上下文中提供了以下对象:

  1. 约束的属性值映射到属性名称

  2. 当前验证的值(属性、bean、方法参数等),名称为 validatedValue

  3. 映射到名称格式化程序的 Bean,暴露 var-arg 方法 format(String format, Object…​ args),其行为类似于 java.util.Formatter.format(String format, Object…​ args)

表达式语言非常灵活,Hibernate Validator 提供了几个功能级别,你可以通过 ExpressionLanguageFeatureLevel 枚举使用这些功能级别来启用表达式语言功能:

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

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

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

  4. BEAN_METHODS:还允许执行 Bean 方法。对于硬编码的约束消息来说可以被认为是安全的,但对于在其中需要特别小心的 custom violations 来说不行。

约束消息的默认功能级别是 BEAN_PROPERTIES

当使用 bootstrapping the ValidatorFactory 时,可以定义表达式语言功能级别。

以下部分提供了在错误消息中使用 EL 表达式的几个示例。

4.1.3. Examples

Example 4.2, “Specifying message descriptors” 展示了如何利用不同的选项来指定消息描述符。

示例 4.2:指定消息描述符
package org.hibernate.validator.referenceguide.chapter04.complete;

public class Car {

    @NotNull
    private String manufacturer;

    @Size(
            min = 2,
            max = 14,
            message = "The license plate '${validatedValue}' must be between {min} and {max} characters long"
    )
    private String licensePlate;

    @Min(
            value = 2,
            message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
    )
    private int seatCount;

    @DecimalMax(
            value = "350",
            message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher " +
                    "than {value}"
    )
    private double topSpeed;

    @DecimalMax(value = "100000", message = "Price must not be higher than ${value}")
    private BigDecimal price;

    public Car(
            String manufacturer,
            String licensePlate,
            int seatCount,
            double topSpeed,
            BigDecimal price) {
        this.manufacturer = manufacturer;
        this.licensePlate = licensePlate;
        this.seatCount = seatCount;
        this.topSpeed = topSpeed;
        this.price = price;
    }

    //getters and setters ...
}

验证一个无效的 Car 实例会产生带有 Example 4.3, “Expected error messages” 中断言所示消息的约束违规:

  1. manufacturer 字段上的 @NotNull 约束导致错误消息“不能为 null”,因为这是 Jakarta Bean Validation 规范定义的默认消息,并且消息属性中没有给出特定描述符

  2. licensePlate 字段上的 @Size 约束显示了消息参数的插值({min}{max})以及如何使用 EL 表达式 ${validatedValue} 将验证值添加到错误消息中的内容

  3. seatCount 上的 @Min 约束展示了如何使用三元表达式 EL 表达式,根据约束的属性动态选择单数或复数形式(“必须至少有 1 个座位” 相对于 “必须至少有 2 个座位”)。

  4. topSpeed 上的 @DecimalMax 约束的消息展示了如何使用格式化程序实例格式化验证值

  5. 最后,price 上的 @DecimalMax 约束表明参数插值优先于表达式求值,导致 $ 符号出现在最高价格前面

只有实际限制属性可以使用 {attributeName} 形式的消息参数进行插值。当引用经验证的值或添加到插值上下文(参见 Section 12.13.1, “HibernateConstraintValidatorContext )的自定义表达式变量时,必须使用 ${attributeName} 形式的 EL 表达式。

示例 4.3:预期的错误消息
Car car = new Car( null, "A", 1, 400.123456, BigDecimal.valueOf( 200000 ) );

String message = validator.validateProperty( car, "manufacturer" )
        .iterator()
        .next()
        .getMessage();
assertEquals( "must not be null", message );

message = validator.validateProperty( car, "licensePlate" )
        .iterator()
        .next()
        .getMessage();
assertEquals(
        "The license plate 'A' must be between 2 and 14 characters long",
        message
);

message = validator.validateProperty( car, "seatCount" ).iterator().next().getMessage();
assertEquals( "There must be at least 2 seats", message );

message = validator.validateProperty( car, "topSpeed" ).iterator().next().getMessage();
assertEquals( "The top speed 400.12 is higher than 350", message );

message = validator.validateProperty( car, "price" ).iterator().next().getMessage();
assertEquals( "Price must not be higher than $100000", message );

4.2. Custom message interpolation

如果默认消息插值算法不符合你的要求,还可以插入自定义 MessageInterpolator 实现。

自定义插值器必须实现接口 jakarta.validation.MessageInterpolator。请注意,实现必须是线程安全的。建议自定义消息插值器将最终实现委派给可以通过 Configuration#getDefaultMessageInterpolator() 获得的默认插值器。

为了使用自定义消息插值器,必须通过在 Jakarta Bean Validation XML 描述符 META-INF/validation.xml 中进行配置(参见 Section 8.1, “Configuring the validator factory in validation.xml )或在引导 ValidatorFactoryValidator 时传递它来对其进行注册(分别参见 Section 9.2.1, “MessageInterpolatorSection 9.3, “Configuring a Validator” )。

4.2.1. ResourceBundleLocator

在某些用例中,您想使用 Bean Validation 规范中定义的消息内插算法,但从除 ValidationMessages 以外的其他资源包中检索错误消息。在此情况下 Hibernate Validator 的 ResourceBundleLocator SPI 可以提供帮助。

Hibernate 验证器中的默认消息插值器 ResourceBundleMessageInterpolator 将资源包的检索委托给 SPI。使用备用包只需要在启动 ValidatorFactory 时传递一个带有包名的 PlatformResourceBundleLocator 实例,如 Example 4.4, “Using a specific resource bundle” 中所示。

示例 4.4:使用特定的资源包
Validator validator = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
                new ResourceBundleMessageInterpolator(
                        new PlatformResourceBundleLocator( "MyMessages" )
                )
        )
        .buildValidatorFactory()
        .getValidator();

当然,您还可以实现一个完全不同的 ResourceBundleLocator,例如它返回由数据库中的记录支持的程序包。在这种情况下,您可以通过 HibernateValidatorConfiguration#getDefaultResourceBundleLocator() 获取默认定位器,例如,您可以将它用作自定义定位器的后备。

除了 PlatformResourceBundleLocator 之外,Hibernate 验证器还提供另一个现成的资源包定位器实现,即 AggregateResourceBundleLocator ,它允许从多个资源包中检索错误消息。例如,可以在多模块应用程序中使用这个实现,在其中希望每个模块都有一个消息包。 Example 4.5, “Using AggregateResourceBundleLocator 展示了如何使用 AggregateResourceBundleLocator

示例 4.5:使用 AggregateResourceBundleLocator
Validator validator = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
                new ResourceBundleMessageInterpolator(
                        new AggregateResourceBundleLocator(
                                Arrays.asList(
                                        "MyMessages",
                                        "MyOtherMessages"
                                )
                        )
                )
        )
        .buildValidatorFactory()
        .getValidator();

请注意,这些程序包按传递给构造函数的顺序进行处理。这意味着如果几个程序包为给定的消息键包含一个条目,那么该值将取自第一个包含该键的程序包。