3.声明和验证方法约束
从Bean Validation 1.1开始,约束不仅可以应用于JavaBeans及其属性,还可以应用于任何Java类型的方法和构造函数的参数和返回值。这种方式可以使用Bean Validation约束来指定
- 在调用方法或构造函数之前调用方必须满足的前提条件(通过对方法或构造函数的参数应用约束)
- 在方法或构造函数调用返回后(通过对的返回值应用约束)保证调用者的后置条件
这种方法比检查参数和返回值的正确性的传统方法有几个优点:
- 不必手动执行检查(例如通过抛IllegalArgumentException或类似的方式),导致编写和维护的代码更少。
- 方法或构造函数的约束条件不必在其文档中再次表述,因为约束注解将自动包含在生成的JavaDoc中。这避免了冗余,并减少了实现和文档之间不一致的几率。
为了使注解显示在注释元素的JavaDoc中,注释类型本身必须使用元注释@Documented注释。
3.1声明方法约束
3.1.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) {
//...
}
}
该例子指定了以下条件:
传入构造方法RentalStation的参数name不能为null
如果要调用rentCar方法,参数customer不能为null,参数startDate也不能为null且在时间上要比当前时间要晚,参数durationInDay最小为1。
注意,声明方法或者构造函数约束本身并不会导致他们在调用时自动被验证。相反, 需要使用ExecutableValidator API(见3.2节验证方法的约束)来执行验证,这通常是通过使用一个方法拦截工具如AOP,代理对象等来实现。
方法约束只适用于实例方法,即对不支持静态方法进行约束。可能会有额外的限制,取决于使用什么拦截工具来触发方法验证,如拦截器支持的目标方法的访问权限。请参阅拦截技术的文档,以确定是否存在任何此类限制。
交叉参数约束
有时,验证不仅取决于单个参数,还取决于方法或构造函数的几个甚至全部参数。这种需求可以通过交叉参数约束来实现。
交叉参数约束可以被认为是相当于类级约束的方法验证。两者都可以用来实现基于几个要素的验证要求。不过类级约束适用于Bean的多个属性,但交叉参数约束适用于方法的多个参数。
声明一个交叉参数约束:
package org.hibernate.validator.referenceguide.chapter03.crossparameter;
public class Car {
@LuggageCountMatchesPassengerCount(piecesOfLuggagePerPassenger = 2)
public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
//...
}
}
为了区分交叉参数约束和返回值约束,验证器要实现ConstraintValidator接口且用@SupportedValidationTarget注解。可以在6.3节交叉参数约束中找到有关详细信息, 其中显示了如何实现自己的交叉参数约束。
在某些情况下,可以将一个约束应用于方法或构造函数的多个参数(即它是一个交叉参数约束),也可以应用于返回值。其中一个例子是自定义约束,它允许使用表达式或脚本语言指定验证规则。
这些约束必须定义一个validationAppliesTo(),可以在声明时指定约束目标。如通过validationAppliesTo = ConstraintTarget.PARAMETERS可以指定将约束应用于方法的参数,而ConstraintTarget.RETURN_VALUE将约束应用于返回值。
指定一个约束的目标:
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;
}
}
虽然这样的约束适用于方法的参数和返回值,但通常可以自动推断目标,如:
- 带有参数的void方法(约束用于参数)
- 一个带有返回值但没有参数的方法(约束用于返回值)
- 既不是方法也不是构造函数,而是字段,参数等(约束用于被标注的元素)
在这些情况下,不必指定约束目标。如果在不能自动确定的情况下没有指定约束目标,则会引发ConstraintDeclarationException异常。
3.1.2.返回值约束
通过向方法添加约束来声明方法或构造函数的后置条件
声明方法和构造函数的返回值约束:
package org.hibernate.validator.referenceguide.chapter03.returnvalue;
public class RentalStation {
@ValidRentalStation
public RentalStation() {
//...
}
@NotNull
@Size(min = 1)
public List<Customer> getCustomers() {
//...
return null;
}
}
- 任何新创建的RentalStation对象都必须满足@ValidRentalStation约束
- getCustomers()方法返回的
List<Customer>
不能是null且必须包含至少一个元素
3.1.3.级联验证
与JavaBeans属性的级联验证(请参阅2.1.6. Object graphs章节)类似 ,@Valid注解可用于标记方法的参数和返回值的级联验证。验证标注了@Valid的参数或返回值时,也会验证在参数或返回值对象上声明的约束。
在标记方法的参数和返回值的级联验证时,Garage的checkCar()方法的Car参数和Garage构造函数的返回值被标记为级联验证。
标记参数和返回值为级联验证:
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也会验证对象的属性约束。同样,在验证构造函数的返回值时,也会验证name字段的@NotNull约束。
通常,级联验证对于方法约束的处理方式与对于JavaBeans属性的方式完全相同。
特别是,在级联验证过程中null值会被忽略(当然这在构造函数返回值验证过程中不会发生),级联验证是递归执行的,即如果标记为级联验证的参数或返回值对象本身具有带有标记的属性@Valid,则在引用的元素上声明的约束也将被验证。
级联验证不仅适用于简单对象引用,还适用于集合类型参数和返回值。这意味着当把@Valid标注在一个参数或返回值上时,如果是:
- 数组
- 实现了java.lang.Iterable
- 实现了java.util.Map
每个包含的元素都得到验证。下面的例子中,checkCars()方法的cars参数被@Valid标注,会级联验证List<Car>
中所有的的car对象。
3.1.4.继承层次结构中的方法约束
在继承层次结构中声明方法约束时,注意以下规则是很重要的:
- 子方法不能加强方法调用者所满足的前提条件
- 保证调用方法的后置条件在子类型中不能被削弱
在子类型中非法的方法参数约束:
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(55)上约束是非法的,因为这方法实现了接口方法 Vehicle的drive()。请注意,如果超类方法本身没有声明任何参数约束,则声明覆盖方法的参数约束也是不允许的。
此外,如果方法重写或实现了在几个同一结构层次超类或接口中声明的方法(例如,两个接口不是彼此扩展的,或者一个接口没有被这个类实现),那么在这个方法中不能有参数约束。该方法RacingCar的drive()覆盖Vehicle的drive()以及Car的drive()。所以约束Vehicle的drive()是非法的。
同一层次结构的类型中的非法方法参数约束:
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) {
//...
}
}
先前描述的限制仅适用于参数约束。相比之下,返回值约束可以被添加到覆盖或实现任何超类型方法的方法中。
在这种情况下,所有方法的返回值约束都适用于子类型方法,即在子类型方法本身上声明的约束以及在重载或实现的超类型方法上的声明任何返回值约束,是合法的。因为将额外的返回值约束放在适当的位置可能永远不会减弱向方法的调用者保证的后置条件。
超类型和子类型方法的返回值约束:
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异常。
本节中描述的规则只适用于方法,不适用于构造函数。根据定义,构造函数不会覆盖超类型的构造函数。因此,在验证构造函数调用的参数或返回值时,只有在构造函数本身上声明的约束被应用,其他任何在超类型构造函数中声明的约束不起作用。
通过在创建实例之前设置HibernateValidatorConfiguration的MethodValidationConfiguration属性中包含的配置参数,可以放宽这些规则的执行。另请参阅11.3.在类层次结构中放宽方法验证的要求章节。
3.2.验证方法约束
方法约束的验证是使用ExecutableValidator接口完成的。
3.2.1.获得一个ExecutableValidator实例
获取一个ExecutableValidator实例:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();
在这个例子中,验证器是从默认的验证器工厂中获取,但是如果需要,例如需要使用特定的ParameterNameProvider(参阅8.2.4. ParameterNameProvider章节),可以引导一个特定配置的工厂,如8.Bootstrapping章节所描述的那样。
3.2.2.ExecutableValidator的方法
ExecutableValidator接口提供了共四种方法:
- validateParameters()和validateReturnValue()方法验证
- validateConstructorParameters()和validateConstructorReturnValue()构造函数验证
就像上面的Validator方法一样,所有这些方法都返回一个Set<ConstraintViolation>
,其包含ConstraintViolation每个违反约束的实例ConstraintViolation,如果验证成功则返回空。同样所有的方法都有一个var-args参数,通过这个参数你可以指定验证组来进行验证。
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();
}
}
ExecutableValidatord的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()验证方法的所有参数约束,即对各个参数约束以及交叉参数约束进行验证。
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 );
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 );
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中的方法
除了第二章介绍的ConstraintViolation方法, ConstraintViolation还提供了两个方法验证的具体可执行的参数和返回值。
- ConstraintViolation的getExecutableParameters()返在方法或构造函数参数验证的情况下返回验证的参数数组。
- ConstraintViolation的getExecutableReturnValue()在返回值验证的情况下提供对验证的对象的访问。
请注意,getPropertyPath()可以获取被验证的参数或返回值的详细信息。可以用来日志记录,特别是,可以检索有关方法的名称和参数类型以及有关参数的索引节点的路径。
获取方法和参数的信息:
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( "arg0", parameterNode.getName() );
assertEquals( 0, parameterNode.getParameterIndex() );
这里使用了当前的ParameterNameProvider(参阅8.2.4. ParameterNameProvider章节),参数名默认为arg0, arg1…。
3.3.内置的方法约束
Hibernate Validator目前提供了一个方法级别约束 @ParameterScriptAssert,这是一个通用的交叉参数约束,它允许使用任何兼容JSR 223(”Scripting for the JavaTM Platform”)的脚本语言来实现验证,前提是此类语言的引擎在类路径中可用。
要从表达式中引用方法的参数,请根据名字从ParameterNameProvider获取。(参阅8.2.4. ParameterNameProvider章节)。 下面的例子展示了如何用@ParameterScriptAssert完成前面通过声明一个交叉参数约束@LuggageCountMatchesPassengerCount 完成的验证逻辑。
使用@LuggageCountMatchesPassengerCount:
package org.hibernate.validator.referenceguide.chapter03.crossparameter;
public class Car {
@LuggageCountMatchesPassengerCount(piecesOfLuggagePerPassenger = 2)
public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
//...
}
}
使用@ParameterScriptAssert:
package org.hibernate.validator.referenceguide.chapter03.parameterscriptassert;
public class Car {
@ParameterScriptAssert(lang = "javascript", script = "arg1.size() <= arg0.size() * 2")
public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
//...
}
}