Hibernate Validator5.4.2--宣告和驗證方法約束
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) {
//...
}
}