Hibernate Validator 中文操作指南

6. Creating custom constraints

Jakarta Bean 验证 API 定义了一整套标准约束注释,例如@NotNull@Size等。在这些内置约束不充分的情况下,可以轻松地创建针对特定验证要求定制的自定义约束。

6.1. Creating a simple constraint

要创建自定义约束,需要以下三个步骤:

  1. Create a constraint annotation

  2. Implement a validator

  3. 定义默认错误消息

6.1.1. The constraint annotation

本部分展示如何编写约束注解,该注解可用于确保给定字符串完全为大写或小写。随后,此约束将应用到 Chapter 1, Getting startedCar 类的 licensePlate 字段,以确保该字段始终为大写字符串。

首先需要一种表达两种大小写模式的方法。虽然可以使用String常量,但更好的方法是为此目的使用枚举:

示例 6.1:枚举 CaseMode 以表达大写和小写
package org.hibernate.validator.referenceguide.chapter06;

public enum CaseMode {
    UPPER,
    LOWER;
}

下一步是定义实际的约束注释。如果您以前从未设计过注释,这可能看起来有点可怕,但实际上并不难:

示例 6.2:定义 @CheckCase 约束注解
package org.hibernate.validator.referenceguide.chapter06;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
@Repeatable(List.class)
public @interface CheckCase {

    String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase." +
            "message}";

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

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

    CaseMode value();

    @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CheckCase[] value();
    }
}

使用@interface关键字定义注释类型。注释类型的所有属性都以类似于方法的方式声明。Jakarta Bean 验证 API 的规范要求任何约束注释定义:

  1. 一个名为 message 的属性,它在约束被违反时返回用于创建错误消息的默认键

  2. 允许指定验证组的属性 groups ,该约束属于该组(请参阅 Chapter 5, Grouping constraints )。默认为 Class<?> 类型的一个空数组。

  3. 属性 payload ,Jakarta Bean Validation API 的客户端可以使用它为约束指定自定义负载对象。此属性不被 API 本身使用。自定义负载的一个示例可以是严重性的定义:

_public class Severity { public interface Info extends Payload { }

    public interface Error extends Payload {
    }
}__public class ContactDetails {
    @NotNull(message = "Name is mandatory", payload = Severity.Error.class)
    private String name;

@NotNull(message = "Phone number not specified, but not mandatory", payload = Severity.Info.class) private String phoneNumber;

    // ...
}_Now a client can after the validation of a _ContactDetails_ instance access the severity of a constraint using _ConstraintViolation.getConstraintDescriptor().getPayload()_ and adjust its behavior depending on the severity.

除了这三个必填属性外,还有另一个属性value,允许指定所需的大小写模式。名称value是一个特殊的名称,如果它是指定的唯一属性,则可以在使用注释时省略,例如在@CheckCase(CaseMode.UPPER)中。

此外,约束注释还装饰有一些元注释:

  1. @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) :为约束定义受支持的目标元素类型。可以在字段(元素类型 FIELD )、JavaBeans 属性以及方法返回值( METHOD )、方法/构造函数参数( PARAMETER )和参数化类型( TYPE_USE )的类型参数中使用 @CheckCase 。元素类型 ANNOTATION_TYPE 允许基于 @CheckCase 创建复合约束(请参阅 Section 6.4, “Constraint composition” )。

创建类级约束时(参见 Section 2.1.4, “Class-level constraints”),必须使用元素类型 TYPE。针对构造函数返回值的约束需要支持元素类型 CONSTRUCTOR。用于一起验证方法或构造函数的所有参数的跨参数约束(参见 Section 6.3, “Cross-parameter constraints”),必须分别支持 METHODCONSTRUCTOR

  1. @Retention(RUNTIME):指定这种类型的注释将通过反射手段在运行时可用

  2. @Constraint(validatedBy = CheckCaseValidator.class):将注释类型标记为约束注释,并指定用于验证使用 @CheckCase 注释的元素的验证器。如果某个约束可用于多个数据类型,则可以指定多个验证器,每个数据类型一个。

  3. @Documented:表示 @CheckCase 的使用将包含在用它注释的元素的 JavaDoc 中

  4. @Repeatable(List.class):表示可以在同一位置多次重复注释,通常具有不同的配置。List 是包含注释类型。

此名为List的包含注释类型也在示例中显示。它允许在同一元素上指定多个@CheckCase注释,例如具有不同的验证组和消息。虽然可以使用其他名称,但 Jakarta Bean 验证规范建议使用名称List,并使注释成为相应约束类型的内部注释。

6.1.2. The constraint validator

定义注释后,您需要创建能够使用@CheckCase注释验证元素的约束验证程序。为此,请按如下所示实现 Jakarta Bean 验证接口ConstraintValidator

示例 6.3:为约束 @CheckCase 实现约束验证器
package org.hibernate.validator.referenceguide.chapter06;

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

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

        if ( caseMode == CaseMode.UPPER ) {
            return object.equals( object.toUpperCase() );
        }
        else {
            return object.equals( object.toLowerCase() );
        }
    }
}

ConstraintValidator 接口定义两个类型参数,它们在实现中设置。第一个指定要验证的注释类型 (CheckCase),第二个指定验证器可以处理的元素类型 (String)。如果约束支持多种数据类型,则必须针对每个允许的类型实现一个 ConstraintValidator,并按上述方式在约束注释处进行注册。

验证器的实现很简单。initialize() 方法使您可以访问已验证约束的属性值,并允许您将它们存储在验证器的字段中,如示例所示。

isValid() 方法包含实际的验证逻辑。对于 @CheckCase,这是在取决于 initialize() 中检索的大小写模式的情况下,检查给定字符串是完全小写还是完全大写的。请注意,Jakarta Bean Validation 规范建议将 null 值视为有效值。如果 null 不是元素的有效值,则应明确用 @NotNull 对其进行注释。

6.1.2.1. The ConstraintValidatorContext

Example 6.3, “Implementing a constraint validator for the constraint @CheckCase 仅从 isValid() 方法返回 truefalse ,依靠默认的错误消息生成。使用传递的 ConstraintValidatorContext 对象,可以添加其他错误消息或完全禁用默认错误消息生成,而仅定义自定义错误消息。 ConstraintValidatorContext API 被建模为流式接口,最好通过一个示例加以说明:

示例 6.4:使用 ConstraintValidatorContext 定义自定义错误消息
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorcontext;

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

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

        boolean isValid;
        if ( caseMode == CaseMode.UPPER ) {
            isValid = object.equals( object.toUpperCase() );
        }
        else {
            isValid = object.equals( object.toLowerCase() );
        }

        if ( !isValid ) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate(
                    "{org.hibernate.validator.referenceguide.chapter06." +
                    "constraintvalidatorcontext.CheckCase.message}"
            )
            .addConstraintViolation();
        }

        return isValid;
    }
}

Example 6.4, “Using ConstraintValidatorContext to define custom error messages” 说明如何禁用默认错误消息生成并使用指定的模板添加自定义错误消息。在本示例中,使用 ConstraintValidatorContext 会生成与默认错误消息生成相同错误消息。

通过调用 addConstraintViolation() 添加每个已配置约束冲突非常重要。只有在调用后,才会创建新的约束冲突。

默认情况下,对于 ConstraintValidatorContext 中创建的自定义违法,表达式语言未启用。

但是,对于某些高级要求,可能需要使用 Expression Language。

在这种情况下,您需要取消包装 HibernateConstraintValidatorContext 并明确启用表达式语言。有关详细信息,请参阅 Section 12.13.1, “HibernateConstraintValidatorContext

请参阅 Section 6.2.1, “Custom property paths”,了解如何使用 ConstraintValidatorContext API 控制类级约束的约束违规行为的属性路径。

6.1.2.2. The HibernateConstraintValidator extension

Hibernate Validator 为 ConstraintValidator 合约提供了一个扩展:HibernateConstraintValidator

此扩展的目的是向 initialize() 方法提供更多上下文信息,因为在当前 ConstraintValidator 合约中,仅将注释作为参数传递。

initialize()HibernateConstraintValidator 方法有两个参数:

  1. 此约束的 ConstraintDescriptor。可以使用 ConstraintDescriptor#getAnnotation() 访问注释。

  2. HibernateConstraintValidatorInitializationContext 提供有用的帮助程序和上下文信息,例如时钟提供器或时间验证容差。

此扩展被标记为孵化版,因此可能会发生更改。计划将来对其进行标准化并将其包含在 Jakarta Bean Validation 中。

以下示例演示了如何基于 HibernateConstraintValidator 来验证:

示例 6.5: 使用 HibernateConstraintValidator 合约
package org.hibernate.validator.referenceguide.chapter06;

public class MyFutureValidator implements HibernateConstraintValidator<MyFuture, Instant> {

    private Clock clock;

    private boolean orPresent;

    @Override
    public void initialize(ConstraintDescriptor<MyFuture> constraintDescriptor,
            HibernateConstraintValidatorInitializationContext initializationContext) {
        this.orPresent = constraintDescriptor.getAnnotation().orPresent();
        this.clock = initializationContext.getClockProvider().getClock();
    }

    @Override
    public boolean isValid(Instant instant, ConstraintValidatorContext constraintContext) {
        //...

        return false;
    }
}

你应该只实现一个 initialize() 方法。请注意,在初始化验证程序时将调用这两个方法。

6.1.2.3. Passing a payload to the constraint validator

有时,你可能希望基于一些外部参数来设定约束验证器行为。

例如,如果您为每个国家/地区有一个实例,则您的邮政编码验证器可能会根据应用程序实例的语言环境而有所不同。另一个要求可能是对特定环境有不同的行为:暂存环境可能无法访问验证器的正确功能所需的一些外部生产资源。

约束验证器负载的概念是针对所有这些用例引出的。它是通过 HibernateConstraintValidatorContextValidator 实例传递给每个约束验证器的对象。

以下示例演示了如何在 ValidatorFactory 初始化期间设置约束验证器负载。除非您覆盖此默认值,否则所有 Validator_s created by this _ValidatorFactory 都会设置此约束验证器负载值。

示例 6.6:在 ValidatorFactory 初始化期间定义约束验证器有效负载
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .constraintValidatorPayload( "US" )
        .buildValidatorFactory();

Validator validator = validatorFactory.getValidator();

另一个选项是使用上下文为每个 Validator 设置约束验证器负载:

示例 6.7:使用 Validator 上下文定义约束验证器有效负载
HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
        .configure()
        .buildValidatorFactory()
        .unwrap( HibernateValidatorFactory.class );

Validator validator = hibernateValidatorFactory.usingContext()
        .constraintValidatorPayload( "US" )
        .getValidator();

// [...] US specific validation checks

validator = hibernateValidatorFactory.usingContext()
        .constraintValidatorPayload( "FR" )
        .getValidator();

// [...] France specific validation checks

设置约束验证器负载后,即可在约束验证器中使用它,如下例所示:

示例 6.8:在约束验证器中使用约束验证器有效负载
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorpayload;

public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> {

    public String countryCode;

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

        boolean isValid = false;

        String countryCode = constraintContext
                .unwrap( HibernateConstraintValidatorContext.class )
                .getConstraintValidatorPayload( String.class );

        if ( "US".equals( countryCode ) ) {
            // checks specific to the United States
        }
        else if ( "FR".equals( countryCode ) ) {
            // checks specific to France
        }
        else {
            // ...
        }

        return isValid;
    }
}

HibernateConstraintValidatorContext#getConstraintValidatorPayload() 有一个类型参数,并且仅当负载属于给定类型时才返回负载。

值得注意的是,约束验证器有效负载不同于在所提出的约束违反中可以包括的动态有效负载。

该约束验证器有效负载的全部目的是用于限制约束验证器的行为。除非特定 ConstraintValidator 实现使用 constraint violation dynamic payload mechanism 将有效负载传递给发出的约束违反,否则不包含在约束违反中。

6.1.3. The error message

最后缺少的一块是一个错误消息,当一个 @CheckCase 约束被违反时,应该使用这个错误消息。要定义此消息,请使用以下内容创建一个文件 ValidationMessages.properties(另请参见 Section 4.1, “Default message interpolation”):

示例 6.9:定义使用 CheckCase 约束的自定义错误消息

如果发生验证错误,验证运行时会使用您为 @CheckCase 注释的 message 属性指定的默认值来查找此资源包中的错误消息。

6.1.4. Using the constraint

现在,您可以使用 Car 章节中的 Chapter 1, Getting started 类中的约束来指定 licensePlate 仅应包含大写字符:

示例 6.10:应用 @CheckCase 约束
package org.hibernate.validator.referenceguide.chapter06;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    @CheckCase(CaseMode.UPPER)
    private String licensePlate;

    @Min(2)
    private int seatCount;

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    //getters and setters ...
}

最后, Example 6.11, “Validating objects with the @CheckCase constraint” 示出了如何使用无效车牌验证 Car 实例致使 @CheckCase 约束违反。

示例 6.11:使用 @CheckCase 约束验证对象
//invalid license plate
Car car = new Car( "Morris", "dd-ab-123", 4 );
Set<ConstraintViolation<Car>> constraintViolations =
        validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals(
        "Case mode must be UPPER.",
        constraintViolations.iterator().next().getMessage()
);

//valid license plate
car = new Car( "Morris", "DD-AB-123", 4 );

constraintViolations = validator.validate( car );

assertEquals( 0, constraintViolations.size() );

6.2. Class-level constraints

如前文所述,还可以在类级别施加约束来验证整个对象的某些状态。类级别约束的定义方式与属性约束相同。 Example 6.12, “Implementing a class-level constraint”显示约束注解和您在 Example 2.9, “Class-level constraint”中已见的_@ValidPassengerCount_约束的验证器。

示例 6.12:实施类级别约束
package org.hibernate.validator.referenceguide.chapter06.classlevel;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { ValidPassengerCountValidator.class })
@Documented
public @interface ValidPassengerCount {

    String message() default "{org.hibernate.validator.referenceguide.chapter06.classlevel." +
            "ValidPassengerCount.message}";

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

    Class<? extends Payload>[] payload() default { };
}
package org.hibernate.validator.referenceguide.chapter06.classlevel;

public class ValidPassengerCountValidator
        implements ConstraintValidator<ValidPassengerCount, Car> {

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {
    }

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

        return car.getPassengers().size() <= car.getSeatCount();
    }
}

如示例所示,您需要在 @Target 注释中使用 element type TYPE。这允许将约束放在类型定义上。示例中约束的验证器在 isValid() 方法中接收 Car,并且可以访问完整对象状态,以判定给定的实例是否有效。

6.2.1. Custom property paths

默认情况下,类级别约束的约束破坏会报告在注释类型级别,例如 Car

但在某些情况下,最好使破坏的属性路径引用涉及的属性之一。例如,您可能希望针对 passengers 属性而不是 Car bean 报告 @ValidPassengerCount 约束。

Example 6.13, “Adding a new ConstraintViolation with custom property path” 展示了如何通过使用传递给 isValid() 的约束验证器上下文对乘客属性建立一个自定义约束违反而完成。请注意,您也可以添加若干个属性节点,指向验证 bean 的子实体。

示例 6.13:添加一个带有自定义属性路径的 ConstraintViolation
package org.hibernate.validator.referenceguide.chapter06.custompath;

public class ValidPassengerCountValidator
        implements ConstraintValidator<ValidPassengerCount, Car> {

    @Override
    public void initialize(ValidPassengerCount constraintAnnotation) {
    }

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

        boolean isValid = car.getPassengers().size() <= car.getSeatCount();

        if ( !isValid ) {
            constraintValidatorContext.disableDefaultConstraintViolation();
            constraintValidatorContext
                    .buildConstraintViolationWithTemplate( "{my.custom.template}" )
                    .addPropertyNode( "passengers" ).addConstraintViolation();
        }

        return isValid;
    }
}

6.3. Cross-parameter constraints

Jakarta Bean 验证区分两个不同类型的约束。

通用约束(迄今已讨论过)适用于注释元素,例如类型、字段、容器元素、方法参数或返回值等。而交叉参数约束适用于方法或构造函数的参数数组,并且可以用来说明基于多个参数值的验证逻辑。

为了定义跨参数约束,其验证器类必须使用 @SupportedValidationTarget(ValidationTarget.PARAMETERS) 注释。来自 ConstraintValidator 接口的类型参数 T 必须解析为 ObjectObject[],以便在 isValid() 方法中接收方法/构造函数参数的数组。

以下示例显示了一个交叉参数约束的定义,它可以用来检查方法的两个 Date 参数是否按正确的顺序排列:

示例 6.14:跨参数约束
package org.hibernate.validator.referenceguide.chapter06.crossparameter;

@Constraint(validatedBy = ConsistentDateParametersValidator.class)
@Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {

    String message() default "{org.hibernate.validator.referenceguide.chapter04." +
            "crossparameter.ConsistentDateParameters.message}";

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

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

跨参数约束的定义与定义通用约束没有任何区别,即它必须指定成员_message()、_groups()_和_payload(),并用_@Constraint_注解。此元注解还指定对应的验证器,如 Example 6.15, “Generic and cross-parameter constraint”中所示。请注意,除了元素类型_METHOD_和_CONSTRUCTOR_之外,还指定_ANNOTATION_TYPE_作为注解的目标,以基于_@ConsistentDateParameters_创建复合约束(请参见 Section 6.4, “Constraint composition”)。

跨参数约束直接在方法或构造函数的声明中指定,返回值约束也是如此。为了提高代码可读性,因此建议选择约束名称(例如 @ConsistentDateParameters),它使约束目标显而易见。

示例 6.15:通用的跨参数约束
package org.hibernate.validator.referenceguide.chapter06.crossparameter;

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParametersValidator implements
        ConstraintValidator<ConsistentDateParameters, Object[]> {

    @Override
    public void initialize(ConsistentDateParameters constraintAnnotation) {
    }

    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        if ( value.length != 2 ) {
            throw new IllegalArgumentException( "Illegal method signature" );
        }

        //leave null-checking to @NotNull on individual parameters
        if ( value[0] == null || value[1] == null ) {
            return true;
        }

        if ( !( value[0] instanceof Date ) || !( value[1] instanceof Date ) ) {
            throw new IllegalArgumentException(
                    "Illegal method signature, expected two " +
                            "parameters of type Date."
            );
        }

        return ( (Date) value[0] ).before( (Date) value[1] );
    }
}

如上所述,验证目标 PARAMETERS 必须使用 @SupportedValidationTarget 注释为交叉参数验证器配置。由于交叉参数约束可以应用于任何方法或构造函数,因此在验证器实现中检查预期的参数数量和类型被认为是一种最佳实践。

与通用约束一样,null 参数应该被视为有效的,而且应该在各个参数上使用 @NotNull 来确保参数不是 null

与类级约束类似,可以在验证跨参数约束时在单个参数(而非所有参数)上创建自定义约束冲突。只要从传递给 isValid()ConstraintValidatorContext 中获取一个节点构建器,并通过调用 addParameterNode() 添加一个参数节点。在该示例中,你可以使用它来针对已验证方法的结束日期参数创建约束冲突。

在罕见的情况下,约束既是通用的又是跨参数的。如果约束具有用 @SupportedValidationTarget({ValidationTarget.PARAMETERS, ValidationTarget.ANNOTATED_ELEMENT}) 注释的验证器类,或者如果它具有通用和跨参数验证器类,则会出现这种情况。

当对具有参数和返回值的方法声明此类约束时,无法确定预期约束目标。因此,通用和跨参数的约束必须定义允许约束用户指定约束目标的成员_validationAppliesTo()_,如 Example 6.16, “Generic and cross-parameter constraint”所示。

示例 6.16:通用的跨参数约束
package org.hibernate.validator.referenceguide.chapter06.crossparameter;

@Constraint(validatedBy = {
        ScriptAssertObjectValidator.class,
        ScriptAssertParametersValidator.class
})
@Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ScriptAssert {

    String message() default "{org.hibernate.validator.referenceguide.chapter04." +
            "crossparameter.ScriptAssert.message}";

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

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

    String script();

    ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
}

@ScriptAssert 约束有两个验证器(未显示),分别是通用和交叉参数验证器,因此定义了成员 validationAppliesTo()。默认值 IMPLICIT 允许在可能的情况下自动推导目标(例如,如果约束是在字段上声明的,或者是在具有参数但不具有返回值的方法上声明的)。

如果无法隐式确定目标,则必须由用户将其设置为_PARAMETERS_或_RETURN_VALUE_,如 Example 6.17, “Specifying the target for a generic and cross-parameter constraint”所示。

示例 6.17:指定通用和跨参数约束的目标
@ScriptAssert(script = "arg1.size() <= arg0", validationAppliesTo = ConstraintTarget.PARAMETERS)
public Car buildCar(int seatCount, List<Passenger> passengers) {
    //...
    return null;
}

6.4. Constraint composition

查看 Example 6.10, “Applying the @CheckCase constraint”Car 类的 licensePlate 字段,您会看到三个约束注解。在更加复杂的情况下,更加多的约束可以应用于一个元素,这可能会容易引起一点困惑。此外,如果在另一个类中存在一个 licensePlate 字段,那么您将必须将所有约束声明复制到另一个类,违反 DRY 原则。

您可以通过创造更高级别的约束来解决此类问题,该约束由若干基本约束组成。 Example 6.18, “Creating a composing constraint @ValidLicensePlate 显示了一个包含约束 @NotNull@Size@CheckCase 的组合约束注解:

示例 6.18:创建组合约束 @ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;

@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
@Documented
public @interface ValidLicensePlate {

    String message() default "{org.hibernate.validator.referenceguide.chapter06." +
            "constraintcomposition.ValidLicensePlate.message}";

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

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

要创建组合约束,只需使用其组成约束对约束声明进行注释即可。如果组合约束本身需要验证器,则此验证器应在 @Constraint 注释中指定。对于不需要附加验证器的组合约束,例如 @ValidLicensePlate,只需将 validatedBy() 设置为一个空数组。

licensePlate 字段中使用新的组合约束与以前的版本完全等效,其中这三个约束直接在字段本身声明:

示例 6.19:组合约束 ValidLicensePlate 的应用
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;

public class Car {

    @ValidLicensePlate
    private String licensePlate;

    //...
}

ConstraintViolation_s retrieved when validating a _Car 实例的集合将包含 @ValidLicensePlate 约束的每个违反组合约束的条目。如果在任何组合约束被违反的情况下,您更喜欢单个 ConstraintViolation,可以使用 @ReportAsSingleViolation 元约束,如下所示:

示例 6.20:使用 @ReportAsSingleViolation
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition.reportassingle;

//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {

    String message() default "{org.hibernate.validator.referenceguide.chapter06." +
            "constraintcomposition.reportassingle.ValidLicensePlate.message}";

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

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