播放地址 4. Interpolating constraint error messages(插值约束错误消息)

消息插值是为违反Bean验证约束创建错误消息的过程。在本章中,您将了解如何定义和解析此类消息,以及如何在缺省算法不足以满足需求的情况下插入自定义消息插值器。(Message interpolation is the process of creating error messages for violated 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(插值)

从所谓的消息描述符中检索约束违反消息。每个约束使用消息属性定义它的默认消息描述符。在声明时,可以使用示例4.1中所示的特定值覆盖默认描述符,“使用消息属性指定消息描述符”。

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()可以从得到的约束违反中检索内插的错误消息。

消息描述符可以包含消息参数以及将在内插期间解析的消息表达式。消息参数是包含在{}中的字符串文本,而消息表达式是包含在${}中的字符串文本。在算法插值过程中应用以下算法:

  • 1、通过使用它们作为资源束验证消息的键来解析任何消息参数。如果此包包含给定消息参数的条目,则该参数将在消息中用包中的相应值替换。如果替换的值再次包含消息参数,则递归地执行此步骤。资源包预期由应用程序开发人员提供,例如,通过向类路径添加名为ValidationMessages.properties的文件。您还可以通过提供此包的特定于地区的变体(如ValidationMessages_en_US.properties)来创建本地化错误消息。默认情况下,JVM的默认区域设置(Locale#getDefault())将在查找包中的消息时使用
  • 2、通过将它们用作包含Bean验证规范的附录B中定义的内置约束的标准错误消息的资源包的key,解决任何消息参数。对于Hibernate Validator,这个包名为org.hibernate.validator.ValidationMessages.如果该步骤触发替换,则再次执行步骤1,否则应用步骤3。
  • 3、通过将消息参数替换为同名的约束注释成员的值来解决任何消息参数。这允许在错误消息(例如“必须至少是${min}”)中引用约束的属性值(例如,Size#min())。
  • 4、通过将它们计算为统一表达式语言的表达式来解析任何消息表达式。请参阅第4.1.2节“用消息表达式进行插值”,以了解更多关于统一EL在错误消息中的使用的信息。

NOTE : 您可以在Bean验证规范的6.3.1.1节中找到插值算法的正式定义。

4.1.1. Special characters

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

  • { is considered as the literal {
  • } is considered as the literal }
  • \$ is considered as the literal $
  • \ is considered as the literal \

4.1.2. Interpolation with message expressions

从Hibernate Validator 5(Bean Validation 1.1)开始,可以在约束违反消息中使用统一表达式语言(由JSR 341定义)。这允许基于条件逻辑定义错误消息,并且还支持高级格式选项。验证引擎使下列对象在EL上下文中可用:

  • 映射到属性名称的约束的属性值(the attribute values of the constraint mapped to the attribute names)
  • 当前验证的值(属性、bean、方法参数等)在名称验证值下(the currently validated value (property, bean, method parameter etc.) under the name validatedValue
  • 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).
    下面的部分提供了在错误消息中使用EL表达式的几个示例

4.1.3. Examples

示例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实例将产生对示例4.3“预期错误消息”中的断言所示的消息的约束违反:

  • manufacturer上的@NotNull约束导致错误消息“一定不为空”,因为这是Bean验证规范定义的默认消息,并且消息属性中没有给出特定的描述符
  • licensePlate字段上的@Size约束显示了消息参数的内插({min},{max})以及如何使用EL表达式${validatedValue}将验证值添加到错误消息。
  • 在seatCount上的@Min约束演示了如何使用带有三元表达式的EL表达式根据约束的属性动态选择单数或复数形式(“必须至少有一个座位”vs“必须至少有两个座位”)
  • topSpeed上的@DecimalMax约束的消息显示了如何使用formatter实例格式化验证值
  • 最后,@DecimalMax对价格的约束表明参数插值优先于表达式求值,导致$符号在最高价格前面出现

note: 只有实际的约束属性可以使用{attributeName}形式的消息参数进行插值。当引用添加到插值上下文中的已验证值或自定义表达式变量(参见第12.11.1节,“HibernateConstraintValidatorContext”)时,必须使用${attributeName}形式的EL表达式。

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实现。
自定义插值器必须实现接口javax.validation.MessageInterpolator。注意,实现必须是线程安全的。建议自定义消息插值器将最终实现委托给默认插值器,可以通过Configuration#getDefaultMessageInterpolator()获得。

NOTE 为了使用自定义消息插值器,必须通过在Bean Validation XML描述符META-INF/validation.xml中配置它(参见8.1节,“在validation.xml中配置验证器工厂”)或通过在引导ValidatorFactory或ValidatorFactory时传递它来注册它。(分别见第9.2.1节“MessageInterpolator”和第9.3节“配置验证器”)(In order to use a custom message interpolator it must be registered either by configuring it in the 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验证器的ResourceBundleLocator SPI可以帮助。

Hibernate Validator中的默认消息内插器ResourceBundleMessageInterpolator将资源包的检索委托给该SPI。使用替代包只需要在引导ValidatorFactory时传递一个具有包名的PlatformResourceBundleLocator实例,如示例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 Validator还提供了另一个开箱即用的资源包定位器实现,即AggregateResourceBundleLocator,它允许从多个资源包检索错误消息。例如,您可以在多模块应用程序中使用此实现,其中希望每个模块有一个消息包。示例4.5,“使用聚集资源BundleLocator”展示了如何使用聚集资源BundleLocator。

Example 4.5: Using AggregateResourceBundleLocator

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

注意,束是按照传递给构造函数的顺序处理的。这意味着,如果几个bundle包含给定消息密钥的条目,则该值将从包含该密钥的列表中的第一个bundle获取。