6.创建自定义约束
Bean Validation API定义了一整套标准注解约束像等@NotNull, @Size等等。在这种情况下,这些内置的约束是不够的,你可以轻松地创建自定义约束根据特定的验证需求。在某些情况下,内置的约束不能满足需求,可以根据特定的验证需求创建自定义约束。
6.1.创建一个简单的约束
要创建一个自定义约束,需要以下三个步骤:
创建一个约束注解
实现一个验证器
定义一个默认的错误消息
6.1.1.约束注解
本节展示了如何编写一个约束注解,用于确保给定字符串是完全大写或小写。这个约束将被应用到 Car类的licensePlate字段,以确保字段一直是一个大写字母的字符串。
首先需要的是一个表达两种模式的方法。虽然可以使用String常量,但更好的方法是使用Java 5的枚举类型来达到这个目的:
package org.hibernate.validator.referenceguide.chapter06;
public enum CaseMode {
UPPER,
LOWER;
}
下一步是定义实际的约束注解。
package org.hibernate.validator.referenceguide.chapter06;
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
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关键字定义的。注解类型的所有属性都以类似方法的方式声明。Bean Validation API的规范要求,即任何约束注解的定义需要:
- message属性,当违反约束产生错误消息的时候,返回默认键(请参阅约束的错误信息)。
- groups属性,允许指定此约束所属的验证组的属性(请参阅分组约束)。这必须默认为一个类型为
Class<?>
的空数组。 - payload属性,Bean Validation API的客户端程序使用该属性去给约束指派自定义的payload,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;
// ...
}
现在,客户端程序可以在验证ContactDetails实例时通过ConstraintViolation.getConstraintDescriptor().getPayload()获得约束的Severity以根据该Severity判断程序的行为。
除了这三个必须的属性外,还有一个value属性,在这里使用它去指定CheckCase约束使用何种模式。value是唯一一个特殊的属性,在使用时可以省略属性名称,例如@CheckCase(CaseMode.UPPER)。
还有几个用来约束注解的元注解:
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE}):定义注解支持的元素类型。@CheckCase可能是用于字段(元素类型 FIELD),javabean属性以及方法的返回值(METHOD)和方法或构造函数的参数(PARAMETER)。ANNOTATION_TYPE允许用在注解上构成组合约束(见6.4.组合约束)。
在创建类级别的约束时(请参阅2.1.4.类级约束),@Targe必须使用TYPE。以构造函数的返回值为目标的约束需要使用CONSTRUCTOR。验证用于方法或构造函数参数的交叉约束时(请参阅6.3.交叉参数约束),必须分别支持METHOD或CONSTRUCTOR。@Retention(RUNTIME):指定这种类型的注解在运行时可以通过反射来获得。
@Constraint(validatedBy = CheckCaseValidator.class):将注解标记为约束注解,并指定在验证@CheckCase时使用的验证器。如果一个约束可以使用在多种数据类型上时,那么可以指定几个验证器,每个数据类型一个。
@Documented:@CheckCase将被包含在被它标记的元素的JavaDoc中。
最后,有内部注解类型List。这个注释允许在同一个元素上指定几个@CheckCase注释,例如,具有不同的验证组或错误消息。虽然也可以使用其他名称,但Bean Validation规范建议使用名称List,并使注解成为相应约束类型的内部注解。
6.1.2.约束验证器
定义了注解之后,需要创建一个约束验证器,该验证器能够使用@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)。
验证器的实现很简单。该initialize()方法可以访问约束的属性值,并允许将它们存储在验证器的字段中,如示例中所示。
该isValid()方法包含实际的验证逻辑。对于@CheckCase,根据从initialize()中获取的CaseMode,检查给定的字符串是完全小写还是大写。请注意,Bean Validation规范建议将空值视为有效。如果null不是元素的有效值,则应该@NotNull 明确标注。
ConstraintValidatorContext
使用传递的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;
}
}
上面的例子显示了如何禁用默认的错误消息和添加自定义的错误消息。
6.2.1. Custom property paths章节描述如何使用ConstraintValidatorContext API去控制类级别约束违反约束时的property paths。
6.1.3.错误消息
最后需要定义的是违反@CheckCase时的错误消息,新建一个ValidationMessages.properties,在其中添加以下文本(见4.1.默认消息插值):
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.
如果违反了约束,在验证时将使用指定@CheckCase注解的message属性去资源包中需找错误消息。
6.1.4. 使用约束
现在可以使用该约束去指定Car类的licensePlate字段应该只包含大写字母的字符串:
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 ...
}
验证Car实例我违反@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.类级别的约束
如之前所说,约束也可以用在类上,下面定义了一个类级别的约束:
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的值包含TYPE,这使约束可以放在类定义上。在其后的验证器中,isValid()方法可以访问完整的对象状态以决定给定的实例是否通过验证。
6.2.1. 自定义property paths
默认情况下,违反类级别约束被报告在被标记的类上。例如Car。
在某些情况下,违反约束指的是违反涉及类的属性之一,例如要报告的是违反了类的某个属性而不是这个类。
下面例子展示了如何通过传递给isValid()方法的ConstraintValidatorContext参数去构建自定义的违反约束的属性结点。也可以添加多个属性节点,指向已验证bean。
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. 交叉参数约束
要定义交叉参数约束,其验证器类必须使用@SupportedValidationTarget(ValidationTarget.PARAMETERS)注解。为了使isValid()方法接受方法或构造器的参数数组,ConstraintValidator接口的泛型T必须使用Object或者Object[]替换,
下面的例子定义了一个用于检查两个Date类型参数的正确顺序的交叉参数约束。
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注解标注,@Constraint注解还指定了相应的验证器。注意,注解支持的元素类型除了METHOD和CONSTRUCTOR,还有ANNOTATION_TYPE。这样可以基于ConsistentDateParameters约束创建组合约束(见6.4.约束组成)。
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] );
}
}
必须在交叉参数的验证器上使用@SupportedValidationTarget(ValidationTarget.PARAMETERS)。
与通用约束一样,null参数应该被认为是有效的,并且应该使用@NotNull来确保参数单独的参数不是null。
在极少数情况下,约束既是通用约束又是交叉参数约束。当一个约束的验证器类被标注了@SupportedValidationTarget({ValidationTarget.PARAMETERS,ValidationTarget.ANNOTATED_ELEMENT}),或者它有一个通用的证器类和一个交叉参数验证器类,此时该约束既是通用约束又是交叉参数约束。
在具有参数和返回值的方法上声明这样的约束时,无法确定预期的约束目标。因此,同时具有通用和交叉参数的约束必须定义一个成员validationAppliesTo(),该成员允许约束用户指定约束的目标,如下所示:
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()。默认值ConstraintTarget.IMPLICIT允许在可能的情况下自动确定目标(例如,如果约束是在一个字段或一个有参数但没有返回值的方法上声明的话)。
如果目标不能被隐式地确定,它必须由用户设置为PARAMETERS或 RETURN_VALUE显示地指定目标。
@ScriptAssert(script = "arg1.size() <= arg0", validationAppliesTo = ConstraintTarget.PARAMETERS)
public Car buildCar(int seatCount, List<Passenger> passengers) {
//...
return null;
}
6.4.组合约束
在复杂的情况下,可以将多个约束应用于一个元素,这可能容易变得有点混乱。此外,如果在另一个类中有一个字段也需要这些约束,则必须将所有约束声明复制到另一个类,违反了DRY原则。
可以通过创建组合约束来解决这类问题,这些约束由几个基本约束组成。看下面的例子:
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;
@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@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()为空数组。
@ValidLicensePlate组合约束的应用:
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition;
public class Car {
@ValidLicensePlate
private String licensePlate;
//...
}
当在验证Car实例违反组合约束@ValidLicensePlate时,返回的ConstraintViolation是违反了组合约束中的某个约束的信息,如:ConstraintViolationImpl{interpolatedMessage=’不能为null’, propertyPath=licensePlate, rootBeanClass=class test.Car, messageTemplate=’{javax.validation.constraints.NotNull.message}’},如果想返回组合约束的信息,可以使用@ReportAsSingleViolation注解,如下所示:
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition.reportassingle;
//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {
String message() default "{org.hibernate.validator.referenceguide.chapter06." +
"constraintcomposition.ValidLicensePlate.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
加了@ReportAsSingleViolation注解后,信息如下:
ConstraintViolationImpl{interpolatedMessage=’{org.hibernate.validator.referenceguide.chapter06.constraintcomposition.ValidLicensePlate.message}’, propertyPath=licensePlate,rootBeanClass=class test.Car,messageTemplate=’{org.hibernate.validator.referenceguide.chapter06.constraintcomposition.ValidLicensePlate.message}’}