本文翻譯自 Method Constraints with Bean Validation 2.0
概述
在本文中,我們會讨論如何使用Bean Validation 2.0(JSR-380)來定義和校驗方法限制。
這裡我們主要聚焦在如下幾種類型的方法限制:
- 單參數限制
- 跨多參數限制
- 傳回值限制
本文中的例子需要引入JSR-380的相關依賴:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.2.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.0.2.Final</version>
<scop>provided</scop>
</dependency>
對依賴的具體說明可以參考 ValidationApi.md
聲明方法限制
首先我們讨論如何聲明方法參數限制和傳回值限制。
我們可以使用
javax.validation.contraints
中的注解,也可以使用建立自定義的限制(例如,建立跨多個參數的限制)
在單個參數上定義限制是非常直接的,我們可以按需在每個參數上添加限制注解:
public void createReservation(
@NotNull @Future LocalDate begin,
@Min(1) int duration,
@NotNull Customer customer) {
// ...
}
同樣的,我們也可以使用在構造函數上使用同樣的方法定義限制:
public class Customer {
public Customer(
@Size(min = 5, max = 200) @NotNull String firstName,
@Size(min = 5, max = 200) @NotNull String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// properties, getters, and setters
}
使用多參數限制
在某些情況下,我們可能需要同時校驗多個值,例如,校驗兩個數字其中一個大于另外一個。
對于這些場景,我們可以定義多參數限制,同時校驗兩個或兩個以上的參數。
方法的多參數校驗,類似于基于多個屬性的類級别校驗。讓我們考慮一個簡單的例子:有一個方法
createReservation
具有開始日期(begin)和結束日期(end)兩個
LocateDate
類型的參數。
我們想確定begin是未來的某個時間,而end在begin之後。與之前的例子不同的是,這次我們無法通過單個注解來定義這個限制,而需要一個跨多參數的限制。
跟單參數限制不同,跨多參數的限制申明在方法上:
@ConsistentDateParameters
public void createReservation(
LocalDate begin,
LocalDate end, Customer customer) {
// ...
}
建立多參數限制
為了實作
@ConsistentDateParameters
限制,我們需要兩個步驟。
首先,我們需要定義限制注解:
@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {
String message() default
"End date must be after begin date and both must be in the future";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
對每個限制注解,強制必須要一下三個屬性:
- message - 預設錯誤消息的key,讓我們可以使用消息解析
- groups - 允許為限制指定校驗組
- payload - Bean Validation API的用戶端端可以使用這個屬性為限制附加自定義的負載對象
如何定義自定義限制的細節說明可以查閱
官方文檔。
接下來,我們就可以定義校驗器類了:
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
implements ConstraintValidator<ConsistentDateParameters, Object[]> {
@Override
public boolean isValid(
Object[] value,
ConstraintValidatorContext context) {
if (value[0] == null || value[1] == null) {
return true;
}
if (!(value[0] instanceof LocalDate)
|| !(value[1] instanceof LocalDate)) {
throw new IllegalArgumentException(
"Illegal method signature, expected two parameters of type LocalDate.");
}
return ((LocalDate) value[0]).isAfter(LocalDate.now())
&& ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
}
}
上述代碼中,isValid方法包含實際的校驗邏輯。首先我們确定拿到兩個LocalDate類型的參數。然後,檢查兩個參數都是未來的時間,且end在begin之後。
同時,注意
ConsistentDateParameterValidator
這個類上必須有這個注解
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
。因為@ConsistentDateParameter設定在方法級别,但是限制需要施加在方法參數上(且不是施加在方法的傳回值)。
注意:Bean校驗規範建議認為null值是有效的。如果null是個無效值,應當使用
@NotNull
注解進行限制。
有時我們需要校驗一個方法的傳回值對象。為此,我們可以使用傳回值限制。
接下來的例子使用内置限制:
public class ReservationManagement {
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
}
對于方法getAllCustomers(),施加了如下限制:
- 傳回值清單必須不能為null且必須有至少一個元素
- 清單中不能包含null元素
傳回值自定義限制
又是我們需要校驗複雜的對象:
public class ReservationManagement {
@ValidReservation
public Reservation getReservationsById(int id) {
return null;
}
}
在這個例子中,傳回的Reservation對象必須符合@ValidReservation注解定義的限制。
我們需要在定義一個限制注解@ValidReservation:
@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
String message() default "End date must be after begin date "
+ "and both must be in the future, room number must be bigger than 0";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
接下來,定義校驗器類:
public class ValidReservationValidator
implements ConstraintValidator<ValidReservation, Reservation> {
@Override
public boolean isValid(
Reservation reservation, ConstraintValidatorContext context) {
if (reservation == null) {
return true;
}
if (!(reservation instanceof Reservation)) {
throw new IllegalArgumentException("Illegal method signature, "
+ "expected parameter of type Reservation.");
}
if (reservation.getBegin() == null
|| reservation.getEnd() == null
|| reservation.getCustomer() == null) {
return false;
}
return (reservation.getBegin().isAfter(LocalDate.now())
&& reservation.getBegin().isBefore(reservation.getEnd())
&& reservation.getRoom() > 0);
}
}
構造方法的傳回值校驗
在上面我們定義的@ValidaReservation注解的目标為METHOD和CONTRUCTOR,是以也這個限制注解也可以施加在構造方法上來校驗方法構造出的執行個體。
public class Reservation {
@ValidReservation
public Reservation(
LocalDate begin,
LocalDate end,
Customer customer,
int room) {
this.begin = begin;
this.end = end;
this.customer = customer;
this.room = room;
}
// properties, getters, and setters
}
級聯校驗
最後,Bean Validation API不僅允許我們校驗單個對象,同僚也可以使用級聯校驗來校驗整個對象圖。
如果我們想校驗複雜的對象,可以通過@Valid注解使用級聯校驗,這個注解對方法參數和傳回值都有效。
我們假設有一個Customer類,這個類有一些被限制的屬性:
public class Customer {
@Size(min = 5, max = 200)
private String firstName;
@Size(min = 5, max = 200)
private String lastName;
// constructor, getters and setters
}
另外有個Reservation類,它有一個Customer類型的屬性,同時還有一些其他被限制的屬性:
public class Reservation {
@Valid
private Customer customer;
@Positive
private int room;
// further properties, constructor, getters and setters
}
我們現在引用Reservation作為一個方法參數,我們可以強制遞歸校驗所有的屬性:
public void createNewCustomer(@Valid Reservation reservation) {
// ...
}
上面代碼中,我們在兩個位址使用了@Valid注解:
- 在Reservation類型參數上:當createNewCustomer方法被調用時,它觸發了對Reservation對象的校驗。
- 在Reservation内嵌的Customer類型屬性上:是以會觸發對内嵌屬性的校驗。
對傳回值類型為Reservation的方法同樣有效:
@Valid
public Reservation getReservationById(int id) {
return null;
}
校驗方法限制
在前面的章節中,我們定義了很多限制,現在我們開始真正來執行這些限制的校驗。有多種執行校驗的途徑,下面來意義說明。
使用Spring提供的自動校驗機制
Spring內建了Hibernate Validator來提供校驗機制。
注意:Spring校驗機制基于AOP,且使用Spring AOP作為預設實作。是以校驗隻能在普通方法上工作,對構造方法無效。
我們現在想要Spring來自動校驗我們的限制,我們需要做兩件事:
首先,我們要為需要校驗的bean加上
@Validated
注解:
@Validated
public class ReservationManagement {
public void createReservation(
@NotNull @Future LocalDate begin,
@Min(1) int duration, @NotNull Customer customer){
// ...
}
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers(){
return null;
}
}
然後,我們要提供一個MethodValidationPostProcessor Bean:
@Configuration
@ComponentScan({ "org.baeldung.javaxval.methodvalidation.model" })
public class MethodValidationConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
現在Spring容器會在違反限制時抛出
javax.validation.ConstraintViolationException
異常。
如果我們使用Spring-Boot,隻要hibernate-validator出現在類路徑中,容器會自動注冊MethodValidationPostProcessor。
人工程式設計校驗
在單獨的Java應用中,我們可以使用
javax.validation.executable.ExecutableValidator
接口來進行校驗。
可以通過如下代碼獲得該接口的執行個體:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();
ExecutableValidator提供了4個方法:
- 用于普通方法校驗的 validateParameters() 和 validateReturnValue()
- 用于構造方法校驗的 validateConstructorParameters() 和 validateConstrctorReturnValue()
校驗我們定義的createReservation()方法的參數的代碼如下:
ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
.getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set<ConstraintViolation<ReservationManagement>> violations
= executableValidator.validateParameters(object, method, parameterValues);
注意:官方文檔不鼓勵在應用代碼中直接使用這個接口,而應該通過方法連接配接技術例如AOP或代理模式。
如果你對ExecutableValidator接口有興趣,你可以看一下
總結
在這個教程中,我們快速浏覽了如何通過Hibernate Validator使用方法限制,還讨論了JSR-380的一些新特性。
首先,我們讨論了如何聲明不同類型的限制:
- 多參數限制
我們還看到了如何進行人工程式設計校驗,以及使用Spring Validation進行自動校驗。
本文的完整示例代碼可以從
GitHub獲得