Hibernate Validator 中文操作指南

3. Declaring and validating method constraints

從 Bean Validation 1.1 開始,約束不只能應用於 JavaBean 及其屬性,還能應用於任何 Java 類型的方法和建構函式的參數和返回值。這樣就可以使用 Jakarta Bean Validation 約束來指定

  1. 在调用方法或构造函数之前,调用者必须满足的先决条件(通过对可执行文件的参数应用约束来实现)

  2. 在方法或构造函数调用返回之后对调用者保证的后置条件(通过对可执行文件返回值应用约束来实现)

为了本参考指南的目的,除非另有说明,method constraint 一词既表示方法,又表示构造函数约束。有时,executable 一词用于指代方法和构造函数。

与检查参数和返回值正确性的传统方法相比,这种方法有以下几个优点:

  1. 不必手动执行检查(如抛出 IllegalArgumentException 或类似异常),因而减少了编写和维护代码量

  2. 可执行文件的前置条件和后置条件不必在其文档中再次表达,因为约束注释将自动包括在生成的 JavaDoc 中。这可以避免冗余,还可以减少实现和文档之间不一致的可能性

为了让注释显示在注释元素的 JavaDoc 中,注释类型本身必须使用元注释 @Documented 注释。对于所有内置约束都需如此,并被认为对任何自定义约束都是最佳实践。

在本篇剩余内容中,您将了解如何声明参数和返回值约束,以及如何使用 ExecutableValidator API 验证它们。

3.1. Declaring method constraints

3.1.1. Parameter constraints

通过向其参数添加约束注解来指定方法或构造函数的先决条件,如 Example 3.1, “Declaring method and constructor parameter constraints”中所示。

示例 3.1:声明方法和构造方法参数约束
package org.hibernate.validator.referenceguide.chapter03.parameter;

public class RentalStation {

    public RentalStation(@NotNull String name) {
        //...
    }

    public void rentCar(
            @NotNull Customer customer,
            @NotNull @Future Date startDate,
            @Min(1) int durationInDays) {
        //...
    }
}

此处声明了以下先决条件:

  1. 传递给 RentalCar 构造函数的 name 不得为 null

  2. 在调用 rentCar() 方法时,给定的 customer 不得为 null,租赁的开始日期也不得为 null,并且必须在未来,最后,租赁期限必须至少为一天

请注意,声明方法或构造函数约束本身不会在执行调用时自动导致其验证。相反,必须使用 _ExecutableValidator_API(参见 Section 3.2, “Validating method constraints”)执行验证,这通常使用 AOP 等方法拦截工具或代理对象来完成。

约束仅可应用于实例方法,即,不支持声明静态方法的约束。根据您用于触发方法验证的拦截机制,可能存在其他限制,例如,相对于作为拦截目标支持的方法的可见性。请参阅拦截技术的文档,以了解是否存在此类限制。

3.1.1.1. Cross-parameter constraints

有时,验证不仅仅依赖于单个参数,而是依赖于方法或构造器的多个参数,甚至所有参数。可借助跨参数约束来满足这类要求。

跨参数约束可视为类级别约束中方法验证的等效部分。两者都可用于实现基于多个元素的验证要求。类级别约束适用于 Bean 的多个属性,而跨参数约束适用于可执行文件的多个参数。

与单参数约束不同,跨参数约束在方法或构造函数上声明,如你可以在 Example 3.2, “Declaring a cross-parameter constraint”中看到。在这里,针对 _load()_方法声明的跨参数约束 _@LuggageCountMatchesPassengerCount_用于确保任何乘客的行李件数不超过两件。

示例 3.2:声明跨参数约束
package org.hibernate.validator.referenceguide.chapter03.crossparameter;

public class Car {

    @LuggageCountMatchesPassengerCount(piecesOfLuggagePerPassenger = 2)
    public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
        //...
    }
}

正如你将在下一节中了解到,返回值约束也在方法级别上声明。要从返回值约束中区分跨参数约束,则在 _ConstraintValidator_实现中使用 _@SupportedValidationTarget_注解配置约束目标。你可以在 Section 6.3, “Cross-parameter constraints”中找到详细信息,它展示了如何实现你自己的跨参数约束。

在某些情况下,可将一个约束应用于可执行文件的参数(即它是一个跨参数约束),还可应用于返回值。一个示例是允许使用表达式或脚本语言指定验证规则的自定义约束。

此类约束必须定义一个成员 validationAppliesTo(),可在声明时用于指定约束目标。如 Example 3.3, “Specifying a constraint’s target”中所示,通过指定 _validationAppliesTo = ConstraintTarget.PARAMETERS_向执行对象的的参数应用约束,而 _ConstraintTarget.RETURN_VALUE_用于向执行的返回值应用约束。

示例 3.3:指定约束的目标
package org.hibernate.validator.referenceguide.chapter03.crossparameter.constrainttarget;

public class Garage {

    @ELAssert(expression = "...", validationAppliesTo = ConstraintTarget.PARAMETERS)
    public Car buildCar(List<Part> parts) {
        //...
        return null;
    }

    @ELAssert(expression = "...", validationAppliesTo = ConstraintTarget.RETURN_VALUE)
    public Car paintCar(int color) {
        //...
        return null;
    }
}

尽管此类约束可应用于可执行文件的参数和返回值,但通常可以自动推断出目标。在以下情况下会这样,如果约束是在以下内容上声明的:

  1. 带参数的 void 方法(该约束应用于参数)

  2. 带有返回值但没有参数的可执行文件(该约束应用于返回值)

  3. 既不是方法又不是构造函数,而是字段、参数等(该约束应用于带注释的元素)

在这些情况下,您不必指定约束目标。如果这对提升源代码的可读性有所帮助,则仍然建议您这样做。如果没有指定约束目标,而在不能自动推断目标的情况下,则会引发 ConstraintDeclarationException

3.1.2. Return value constraints

方法或构造函数的后置条件通过向执行对象添加约束注解来声明,如 Example 3.4, “Declaring method and constructor return value constraints”中所示。

示例 3.4:声明方法和构造方法返回值约束
package org.hibernate.validator.referenceguide.chapter03.returnvalue;

public class RentalStation {

    @ValidRentalStation
    public RentalStation() {
        //...
    }

    @NotNull
    @Size(min = 1)
    public List<@NotNull Customer> getCustomers() {
        //...
        return null;
    }
}

以下约束适用于 RentalStation 的可执行文件:

  1. 任何新创建的 RentalStation 对象必须满足 @ValidRentalStation 约束

  2. getCustomers() 返回的客户列表不得为 null,并且必须至少包含一个元素

  3. getCustomers() 返回的客户列表不能包含 null 对象

如你所见,在上述示例中,方法返回值支持容器元素约束。它们还支持方法参数。

3.1.3. Cascaded validation

类似于 JavaBeans 属性的级联验证(参见 Section 2.1.6, “Object graphs”),_@Valid_注解可用于标记执行参数和返回值以进行级联验证。在验证用 _@Valid_注解的参数或返回值时,同时验证在参数或返回值对象中声明的约束。

Example 3.5, “Marking executable parameters and return values for cascaded validation”中,方法 _Garage#checkCar()_的 _car_参数以及 _Garage_构造函数的返回值被标记为级联验证。

示例 3.5:标记可执行参数和返回值以进行级联验证
package org.hibernate.validator.referenceguide.chapter03.cascaded;

public class Garage {

    @NotNull
    private String name;

    @Valid
    public Garage(String name) {
        this.name = name;
    }

    public boolean checkCar(@Valid @NotNull Car car) {
        //...
        return false;
    }
}
package org.hibernate.validator.referenceguide.chapter03.cascaded;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

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

    //getters and setters ...
}

在验证 checkCar() 方法的参数时,还将评估传入的 Car 对象属性的约束。同样,在验证 Garage 构造器的返回值时,将检查 Garage 的名称字段上的 @NotNull 约束。

一般来说,级联验证适用于可执行文件的方式与适用于 JavaBean 属性的方式完全相同。

特别是,null 值在级联验证期间将被忽略(自然地,这不会在构造器返回值验证期间发生),并且级联验证是递归执行的,即,如果标记为进行级联验证的参数或返回值对象本身具有用 @Valid 标记的属性,则对所引用元素声明的约束也将进行验证。

与字段和属性相同,也可以对容器元素(例如,返回值和参数的集合、映射或自定义容器元素)声明级联验证。

在这种情况下,容器中包含的每个元素都得到验证。因此,在验证 Example 3.6, “Container elements of method parameter marked for cascaded validation”checkCars()_方法的参数时,传递的列表中的每个元素实例都将被验证,当任何包含的 _Car_实例无效时都会创建一个 _ConstraintViolation

示例 3.6:标记为级联验证的方法参数的容器元素
package org.hibernate.validator.referenceguide.chapter03.cascaded.containerelement;

public class Garage {

    public boolean checkCars(@NotNull List<@Valid Car> cars) {
        //...
        return false;
    }
}

3.1.4. Method constraints in inheritance hierarchies

在继承层次结构中声明方法约束时,需要注意以下规则:

  1. 方法的调用者须遵守的先决条件不能强化在子类型中

  2. 保证方法的调用者的后置条件不能弱化在子类型中

这些规则源于 behavioral subtyping 的概念,该概念要求无论在何处使用类型 T,都可以使用 T 的子类型 S,而不会更改程序的行为。

例如,考虑一个类使用该对象上的一个方法调用,具有静态类型 T。如果该对象的运行时类型是 S,且 S 施加了其他先决条件,则客户端类可能无法满足这些先决条件(因为它不知道这些条件)。行为子类型的规则也称为 Liskov substitution principle

Jakarta Bean Validation 规范通过禁止对重写或实现超类(超类或接口)中声明的方法的方法上的参数约束来实现第一条规则。 Example 3.7, “Illegal method parameter constraint in subtype”显示了此规则的违规行为。

示例 3.7:在子类型中的非法方法参数约束
package org.hibernate.validator.referenceguide.chapter03.inheritance.parameter;

public interface Vehicle {

    void drive(@Max(75) int speedInMph);
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.parameter;

public class Car implements Vehicle {

    @Override
    public void drive(@Max(55) int speedInMph) {
        //...
    }
}

Car#drive()@Max 约束是非法的,因为此方法实现了接口方法 Vehicle#drive()。请注意,如果超类型方法本身未声明任何参数约束,则重写方法的参数约束也会被禁止。

此外,如果一个方法重写或实现多个并行超类(例如两个不相互扩展的接口或一个类和一个该类未实现的接口)中声明的方法,则不能为涉及类型中的任何方法指定参数约束。 Example 3.8, “Illegal method parameter constraint in parallel types of a hierarchy”中的类型演示了该规则的违规情况。方法 RacingCar#drive()_重写 _Vehicle#drive()_和 _Car#drive()。因此,_Vehicle#drive()_上的约束是非法的。

示例 3.8:在层次结构的并行类型中的非法方法参数约束
package org.hibernate.validator.referenceguide.chapter03.inheritance.parallel;

public interface Vehicle {

    void drive(@Max(75) int speedInMph);
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.parallel;

public interface Car {

    void drive(int speedInMph);
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.parallel;

public class RacingCar implements Car, Vehicle {

    @Override
    public void drive(int speedInMph) {
        //...
    }
}

前面描述的限制仅适用于参数约束。相比之下,可以在重写或实现任何超类型方法的方法中添加返回值约束。

在这种情况下,所有方法的返回值约束都适用于子类型方法,即在子类型方法本身声明的约束以及重写/实现的超类型方法的任何返回值约束。这是合法的,因为实施额外的返回值约束永远不会减弱对方法调用者的后置条件的保证。

因此,在验证 Example 3.9, “Return value constraints on supertype and subtype method”中所示方法 _Car#getPassengers()_的返回值时,方法本身上的 _@Size_约束以及实现的接口方法 _Vehicle#getPassengers()_上的 _@NotNull_约束适用。

示例 3.9:超类型和子类型方法上的返回值约束
package org.hibernate.validator.referenceguide.chapter03.inheritance.returnvalue;

public interface Vehicle {

    @NotNull
    List<Person> getPassengers();
}
package org.hibernate.validator.referenceguide.chapter03.inheritance.returnvalue;

public class Car implements Vehicle {

    @Override
    @Size(min = 1)
    public List<Person> getPassengers() {
        //...
        return null;
    }
}

如果验证引擎检测到违反上述任何规则,则将引发 ConstraintDeclarationException

本节中描述的规则仅适用于方法,不适用于构造函数。根据定义,构造函数永远不会覆盖超类构造函数。因此,在验证构造函数调用的参数或返回值时,只应用在构造函数本身上声明的约束,而永远不会应用在超类构造函数上声明的任何约束。

在创建 Validator 实例之前,可以通过设置 HibernateValidatorConfigurationMethodValidationConfiguration 属性中包含的配置参数来放宽这些规则的执行。另请参阅 Section 12.3, “Relaxation of requirements for method validation in class hierarchies”

3.2. Validating method constraints

使用 ExecutableValidator 接口对方法约束进行验证。

Section 3.2.1, “Obtaining an ExecutableValidator instance” 中,您将了解如何获取 ExecutableValidator 实例,而 Section 3.2.2, “ExecutableValidator methods” 介绍如何使用此接口提供的不同方法。

通常不直接在应用程序代码中调用 ExecutableValidator 方法,而是通过 AOP、代理对象等方法拦截技术来调用它们。这会导致在调用方法或构造函数时自动透明地验证可执行约束。通常当任何约束被违反时,集成层会引发 ConstraintViolationException

3.2.1. Obtaining an ExecutableValidator instance

您可以通过 Validator#forExecutables() 检索 ExecutableValidator 实例,如 Example 3.10, “Obtaining an ExecutableValidator instance” 中所示。

示例 3.10:获取 ExecutableValidator 实例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();

在示例中,可执行验证器从默认验证器工厂中检索,但如果需要,您还可以引导一个专门配置的工厂,如 Chapter 9, Bootstrapping 中所述,例如为了使用特定的参数名称提供程序(请参阅 Section 9.2.4, “ParameterNameProvider )。

3.2.2. ExecutableValidator methods

ExecutableValidator 接口总共提供四种方法:

  1. validateParameters()validateReturnValue() 用于方法验证

  2. validateConstructorParameters()validateConstructorReturnValue() 用于构造函数验证

Validator 上的方法一样,所有这些方法都返回 Set<ConstraintViolation>,其中包含一个 ConstraintViolation 实例(每个违反的约束一个),如果验证成功,则为空。此外,所有这些方法都有 var-args group 参数,您可以通过它传递验证中要考虑的验证组。

以下部分中的示例基于 CarExample 3.11, “Class Car with constrained methods and constructors” 类构造方法中的方法。

示例 3.11:带有约束方法和构造方法的类 Car
package org.hibernate.validator.referenceguide.chapter03.validation;

public class Car {

    public Car(@NotNull String manufacturer) {
        //...
    }

    @ValidRacingCar
    public Car(String manufacturer, String team) {
        //...
    }

    public void drive(@Max(75) int speedInMph) {
        //...
    }

    @Size(min = 1)
    public List<Passenger> getPassengers() {
        //...
        return Collections.emptyList();
    }
}
3.2.2.1. ExecutableValidator#validateParameters()

validateParameters() 方法用于验证方法调用的参数。 Example 3.12, “Using ExecutableValidator#validateParameters() 展示了一个示例。该验证导致对 drive() 方法参数的 @Max 约束违规。

示例 3.12:使用 ExecutableValidator#validateParameters()
Car object = new Car( "Morris" );
Method method = Car.class.getMethod( "drive", int.class );
Object[] parameterValues = { 80 };
Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters(
        object,
        method,
        parameterValues
);

assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
        .next()
        .getConstraintDescriptor()
        .getAnnotation()
        .annotationType();
assertEquals( Max.class, constraintType );

请注意,validateParameters() 验证方法的所有参数约束,即各个参数的约束以及跨参数约束。

3.2.2.2. ExecutableValidator#validateReturnValue()

使用 validateReturnValue() 可以验证方法的返回值。 Example 3.13, “Using ExecutableValidator#validateReturnValue() 中的验证导致一次约束违规,因为 getPassengers() 方法预期至少返回一个 Passenger 实例。

示例 3.13:使用 ExecutableValidator#validateReturnValue()
Car object = new Car( "Morris" );
Method method = Car.class.getMethod( "getPassengers" );
Object returnValue = Collections.<Passenger>emptyList();
Set<ConstraintViolation<Car>> violations = executableValidator.validateReturnValue(
        object,
        method,
        returnValue
);

assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
        .next()
        .getConstraintDescriptor()
        .getAnnotation()
        .annotationType();
assertEquals( Size.class, constraintType );
3.2.2.3. ExecutableValidator#validateConstructorParameters()

构造函数调用的参数可以用 validateConstructorParameters() 进行验证,如方法 Example 3.14, “Using ExecutableValidator#validateConstructorParameters() 中所示。由于 manufacturer 参数上的 @NotNull 约束,验证调用返回一次约束违规。

示例 3.14:使用 ExecutableValidator#validateConstructorParameters()
Constructor<Car> constructor = Car.class.getConstructor( String.class );
Object[] parameterValues = { null };
Set<ConstraintViolation<Car>> violations = executableValidator.validateConstructorParameters(
        constructor,
        parameterValues
);

assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
        .next()
        .getConstraintDescriptor()
        .getAnnotation()
        .annotationType();
assertEquals( NotNull.class, constraintType );
3.2.2.4. ExecutableValidator#validateConstructorReturnValue()

最后,通过使用 validateConstructorReturnValue() 您可以验证构造函数的返回值。在 Example 3.15, “Using ExecutableValidator#validateConstructorReturnValue() 中, validateConstructorReturnValue() 返回一次约束违规,因为构造函数返回的 Car 实例不满足 @ValidRacingCar 约束(未显示)。

示例 3.15:使用 ExecutableValidator#validateConstructorReturnValue()
//constructor for creating racing cars
Constructor<Car> constructor = Car.class.getConstructor( String.class, String.class );
Car createdObject = new Car( "Morris", null );
Set<ConstraintViolation<Car>> violations = executableValidator.validateConstructorReturnValue(
        constructor,
        createdObject
);

assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
        .next()
        .getConstraintDescriptor()
        .getAnnotation()
        .annotationType();
assertEquals( ValidRacingCar.class, constraintType );

3.2.3. ConstraintViolation methods for method validation

除了 Section 2.2.3, “ConstraintViolation 中引入的方法, ConstraintViolation 还提供了两个更具体的方法,用于验证可执行参数和返回值。

对于方法或构造函数参数验证,ConstraintViolation#getExecutableParameters() 返回经过验证的参数数组,而 ConstraintViolation#getExecutableReturnValue() 在返回值验证时提供对已验证对象的访问。

所有其他 ConstraintViolation 方法一般来说适用于方法验证,方式与验证 Bean 相同。请参阅 JavaDoc 了解在 Bean 和方法验证期间各个方法的行为及其返回值的更多信息。

请注意,_getPropertyPath()_在获取有关已验证参数或返回值的详细信息(例如用于日志记录目的)时非常有用。具体而言,你可以从路径节点中检索相关方法的名称和参数类型以及相关参数的索引。如何在 Example 3.16, “Retrieving method and parameter information”中完成此操作。

示例 3.16:检索方法和参数信息
Car object = new Car( "Morris" );
Method method = Car.class.getMethod( "drive", int.class );
Object[] parameterValues = { 80 };
Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters(
        object,
        method,
        parameterValues
);

assertEquals( 1, violations.size() );
Iterator<Node> propertyPath = violations.iterator()
        .next()
        .getPropertyPath()
        .iterator();

MethodNode methodNode = propertyPath.next().as( MethodNode.class );
assertEquals( "drive", methodNode.getName() );
assertEquals( Arrays.<Class<?>>asList( int.class ), methodNode.getParameterTypes() );

ParameterNode parameterNode = propertyPath.next().as( ParameterNode.class );
assertEquals( "speedInMph", parameterNode.getName() );
assertEquals( 0, parameterNode.getParameterIndex() );

参数名称使用当前 ParameterNameProvider 确定(参见 Section 9.2.4, “ParameterNameProvider )。

3.3. Built-in method constraints

除了 Section 2.3, “Built-in constraints”中讨论的内置 bean 和属性级约束外,Hibernate Validator 目前提供一个方法级约束 @ParameterScriptAssert。这是一个通用的跨参数约束,它允许使用任何 JSR 223 兼容的(“JavaTM 平台的脚本化”)脚本语言实现验证例程,前提是在类路径上提供了此语言的引擎。

要从表达式中引用可执行的参数,请使用从活动参数名称提供程序获得的名称(参见 Section 9.2.4, “ParameterNameProvider )。 Example 3.17, “Using @ParameterScriptAssert 展示了如何借助 @ParameterScriptAssert 表达 Example 3.2, “Declaring a cross-parameter constraint”@LuggageCountMatchesPassengerCount 约束的验证逻辑。

示例 3.17:使用 @ParameterScriptAssert
package org.hibernate.validator.referenceguide.chapter03.parameterscriptassert;

public class Car {

    @ParameterScriptAssert(lang = "groovy", script = "luggage.size() <= passengers.size() * 2")
    public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
        //...
    }
}