Hibernate Validator 中文操作指南

4. Interpolating constraint error messages

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

Message interpolation is the process of creating error messages for violated Jakarta Bean Validation constraints. In this chapter you will learn how such messages are defined and resolved and how you can plug in custom message interpolators in case the default algorithm is not sufficient for your requirements.

4.1. Default message interpolation

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

Constraint violation messages are retrieved from so called message descriptors. Each constraint defines its default message descriptor using the message attribute. At declaration time, the default descriptor can be overridden with a specific value as shown in Example 4.1, “Specifying a message descriptor using the message attribute”.

示例 4.1:使用 message 属性指定消息描述符

. Example 4.1: Specifying a message descriptor using the message attribute

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() 从结果约束违规中检索插值后的错误消息。

If a constraint is violated, its descriptor will be interpolated by the validation engine using the currently configured MessageInterpolator. The interpolated error message can then be retrieved from the resulting constraint violation by calling ConstraintViolation#getMessage().

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

Message descriptors can contain message parameters as well as message expressions which will be resolved during interpolation. Message parameters are string literals enclosed in {}, while message expressions are string literals enclosed in ${}. The following algorithm is applied during method interpolation:

  • Resolve any message parameters by using them as key for the resource bundle ValidationMessages. If this bundle contains an entry for a given message parameter, that parameter will be replaced in the message with the corresponding value from the bundle. This step will be executed recursively in case the replaced value again contains message parameters. The resource bundle is expected to be provided by the application developer, e.g. by adding a file named ValidationMessages.properties to the classpath. You can also create localized error messages by providing locale specific variations of this bundle, such as ValidationMessages_en_US.properties. By default, the JVM’s default locale (Locale#getDefault()) will be used when looking up messages in the bundle.

  • Resolve any message parameters by using them as key for a resource bundle containing the standard error messages for the built-in constraints as defined in Appendix B of the Jakarta Bean Validation specification. In the case of Hibernate Validator, this bundle is named org.hibernate.validator.ValidationMessages. If this step triggers a replacement, step 1 is executed again, otherwise step 3 is applied.

  • Resolve any message parameters by replacing them with the value of the constraint annotation member of the same name. This allows to refer to attribute values of the constraint (e.g. Size#min()) in the error message (e.g. "must be at least ${min}").

  • Resolve any message expressions by evaluating them as expressions of the Unified Expression Language. See Section 4.1.2, “Interpolation with message expressions” to learn more about the usage of Unified EL in error messages.

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

You can find the formal definition of the interpolation algorithm in section 6.3.1.1 of the Jakarta Bean Validation specification.

4.1.1. Special characters

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

Since the characters {, } and $ have a special meaning in message descriptors, they need to be escaped if you want to use them literally. The following rules apply:

  1. \{ is considered as the literal {

  2. \} is considered as the literal }

  3. \$ is considered as the literal $

  4. \\ is considered as the literal \

4.1.2. Interpolation with message expressions

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

As of Hibernate Validator 5 (Bean Validation 1.1) it is possible to use the Jakarta Expression Language in constraint violation messages. This allows to define error messages based on conditional logic and also enables advanced formatting options. The validation engine makes the following objects available in the EL context:

  1. the attribute values of the constraint mapped to the attribute names

  2. the currently validated value (property, bean, method parameter etc.) under the name validatedValue

  3. a bean mapped to the name formatter exposing the var-arg method format(String format, Object…​ args) which behaves like java.util.Formatter.format(String format, Object…​ args).

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

Expression Language is very flexible and Hibernate Validator offers several feature levels that you can use to enable Expression Language features through the ExpressionLanguageFeatureLevel enum:

  1. NONE: Expression Language interpolation is fully disabled.

  2. VARIABLES: Allow interpolation of the variables injected via addExpressionVariable(), resources bundles and usage of the formatter object.

  3. BEAN_PROPERTIES: Allow everything VARIABLES allows plus the interpolation of bean properties.

  4. BEAN_METHODS: Also allow execution of bean methods. Can be considered safe for hardcoded constraint messages but not for custom violations where extra care is required.

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

The default feature level for constraint messages is BEAN_PROPERTIES.

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

You can define the Expression Language feature level when bootstrapping the ValidatorFactory.

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

The following section provides several examples for using EL expressions in error messages.

4.1.3. Examples

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

Example 4.2, “Specifying message descriptors” shows how to make use of the different options for specifying message descriptors.

示例 4.2:指定消息描述符

. Example 4.2: Specifying message descriptors

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” 中断言所示消息的约束违规:

Validating an invalid Car instance yields constraint violations with the messages shown by the assertions in Example 4.3, “Expected error messages”:

  1. the @NotNull constraint on the manufacturer field causes the error message "must not be null", as this is the default message defined by the Jakarta Bean Validation specification and no specific descriptor is given in the message attribute

  2. the @Size constraint on the licensePlate field shows the interpolation of message parameters ({min}, {max}) and how to add the validated value to the error message using the EL expression ${validatedValue}

  3. the @Min constraint on seatCount demonstrates how to use an EL expression with a ternary expression to dynamically choose singular or plural form, depending on an attribute of the constraint ("There must be at least 1 seat" vs. "There must be at least 2 seats")

  4. the message for the @DecimalMax constraint on topSpeed shows how to format the validated value using the formatter instance

  5. finally, the @DecimalMax constraint on price shows that parameter interpolation has precedence over expression evaluation, causing the $ sign to show up in front of the maximum price

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

Only actual constraint attributes can be interpolated using message parameters in the form {attributeName}. When referring to the validated value or custom expression variables added to the interpolation context (see Section 12.13.1, “HibernateConstraintValidatorContext), an EL expression in the form ${attributeName} must be used.

示例 4.3:预期的错误消息

. Example 4.3: Expected error messages

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 实现。

If the default message interpolation algorithm does not fit your requirements, it is also possible to plug in a custom MessageInterpolator implementation.

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

Custom interpolators must implement the interface jakarta.validation.MessageInterpolator. Note that implementations must be thread-safe. It is recommended that custom message interpolators delegate final implementation to the default interpolator, which can be obtained via 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” )。

In order to use a custom message interpolator it must be registered either by configuring it in the Jakarta Bean Validation XML descriptor META-INF/validation.xml (see Section 8.1, “Configuring the validator factory in validation.xml) or by passing it when bootstrapping a ValidatorFactory or Validator (see Section 9.2.1, “MessageInterpolator and Section 9.3, “Configuring a Validator”, respectively).

4.2.1. ResourceBundleLocator

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

In some use cases, you want to use the message interpolation algorithm as defined by the Bean Validation specification, but retrieve error messages from other resource bundles than ValidationMessages. In this situation Hibernate Validator’s ResourceBundleLocator SPI can help.

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

The default message interpolator in Hibernate Validator, ResourceBundleMessageInterpolator, delegates retrieval of resource bundles to that SPI. Using an alternative bundle only requires passing an instance of PlatformResourceBundleLocator with the bundle name when bootstrapping the ValidatorFactory as shown in Example 4.4, “Using a specific resource bundle”.

示例 4.4:使用特定的资源包

. Example 4.4: Using a specific resource bundle

Validator validator = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
                new ResourceBundleMessageInterpolator(
                        new PlatformResourceBundleLocator( "MyMessages" )
                )
        )
        .buildValidatorFactory()
        .getValidator();

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

Of course you also could implement a completely different ResourceBundleLocator, which for instance returns bundles backed by records in a database. In this case, you can obtain the default locator via HibernateValidatorConfiguration#getDefaultResourceBundleLocator(), which you e.g. could use as fall-back for your custom locator.

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

Besides PlatformResourceBundleLocator, Hibernate Validator provides another resource bundle locator implementation out of the box, namely AggregateResourceBundleLocator, which allows to retrieve error messages from more than one resource bundle. You could for instance use this implementation in a multi-module application where you want to have one message bundle per module. Example 4.5, “Using AggregateResourceBundleLocator shows how to use AggregateResourceBundleLocator.

示例 4.5:使用 AggregateResourceBundleLocator

. Example 4.5: Using AggregateResourceBundleLocator

Validator validator = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
                new ResourceBundleMessageInterpolator(
                        new AggregateResourceBundleLocator(
                                Arrays.asList(
                                        "MyMessages",
                                        "MyOtherMessages"
                                )
                        )
                )
        )
        .buildValidatorFactory()
        .getValidator();

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

Note that the bundles are processed in the order as passed to the constructor. That means if several bundles contain an entry for a given message key, the value will be taken from the first bundle in the list containing the key.