在寫程序的時候經(jīng)常需要進行數(shù)據(jù)校驗,比如服務(wù)端對http請求參數(shù)校驗,數(shù)據(jù)入庫時對字段長度進行校驗,接口參數(shù)校驗,可以說數(shù)據(jù)校驗遍布應(yīng)用程序代碼中,就像下圖所示:

為了減少代碼重復(fù)率,開發(fā)人員將數(shù)據(jù)校驗的邏輯直接加載數(shù)據(jù)模型中。JSR380定義了一套bean校驗的元數(shù)據(jù)模型,將數(shù)據(jù)的約束定義在數(shù)據(jù)模型中。

而hibernate validator則實現(xiàn)了這樣一套規(guī)范。
下面我們來看看如何使用hibernate進行bean校驗已經(jīng)方法參數(shù)校驗。
準(zhǔn)備
下載hibernate validator的依賴包,這里全部使用maven管理依賴。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.1-b08</version>
</dependency>
校驗bean的屬性
- 先定義一個需要校驗的類,這里采用hibernate validator官方的例子
public class Car {
@NotNull @Size(min = 2, max = 14)
private String manufacturer;
public Car(String manufacturer) {
this.manufacturer = manufacturer;
}
public void drive(@Max(50) int speedInMph) {
// ...
}
// getter setter
}
- 獲取Validator
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
.buildValidatorFactory();
Validator validator = factory.getValidator();
- 使用Validator校驗Car
Car car = new Car(null);
Set<ConstraintViolation<Car>> violations = validator.validate(car);
assertEquals(1, violations.size());
對于bean的校驗首先需要確定要校驗?zāi)男╊悾谶@些類的屬性添加各種約束(比如@NotNull),通過java.validation.Validation獲取Validator,通過validator校驗對象,獲取校驗的結(jié)果,其校驗結(jié)果都是返回一個包含ConstraintViolation對象的集合。
校驗方法中的參數(shù)
比如要校驗Car中drive方法中的參數(shù)。
- 首先獲取ExecutableValidator
ExecutableValidator executableValidator = validator.forExecutables();
通過之前獲取的Validator得到ExecutableValidater.
- 通過ExecutableValidator校驗drive方法中的參數(shù)
Car object = new Car("Morris");
Method method = RacingCar.class.getMethod( "drive", int.class );
Object[] parameterValues = { 90 };
Set<ConstraintViolation> violations = executableValidator.validateParameters(
object,
method,
parameterValues
);
// assertEquals(1, violations.size() );
通過這個例子能夠最bean的屬性和方法進行簡單的校驗,但是我們經(jīng)常遇到需要校驗的場景有:
- 對象的級聯(lián)校驗;
比如Car中引用一個對象Driver,在校驗Car的同時也需要校驗Driver中的屬性。
class Car {
@NotNull
private String manufacturer;
private Driver driver;
}
- 對象中關(guān)聯(lián)參數(shù)聯(lián)合校驗;
比如Car有兩個屬性,座位數(shù)和乘客數(shù),要求乘客數(shù)量不能大于座位數(shù)。
class Car {
@Max(20)
private int seatCount;
// passengers.size() <= seatCount
private List<Passenger> passengers;
}
- 方法中參數(shù)關(guān)聯(lián)校驗;
比如Car對象的方法buildCar的簽名如下:
public Car buildCar(int seatCount, List<Passenger> passengers) {
// ...
return null;
}
要求乘客數(shù)量小于座位數(shù)。
- 默認(rèn)情況下,validator會對被校驗對象的所有屬性進行校驗,能否只校驗一部分?
- 如何自定義約束呢?
- 如何自定義校驗結(jié)果中的message呢?
下面一起來回答前面提到的幾個問題。
級聯(lián)校驗
通過在屬性上添加@Valid注解就可以進行級聯(lián)校驗了。如下:
class Car {
@NotNull
private String manufacturer;
@Valid
private Driver driver;
// getter setter
}
這樣在校驗Car的時候,也同時會校驗Driver中的屬性。
自定義約束
1.首先定義一個約束,約束是一個注解形式,相當(dāng)于定義個注解,我們需要確定這個注解是用于屬性、類還是方法上等,其次約束注解需要提供幾個固定的方法,最后確定這個約束需要的自定義方法。例如要驗證汽車的載客人數(shù)不能超過座位數(shù)的約束。
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
//必須添加下面這個注解,實際校驗的時候?qū)⑹褂弥付ǖ腣alidator進行校驗
@Constraint(validateBy = {ValidPassengerCount.Validator.class})
public @interface ValidPassengerCount {
// 固定需要添加的方法
String message() default 'validatePassengerCount.message';
// 固定需要添加的方法
Class<?>[] groups() default {};
// 固定需要添加的方法
Class<? extends Payload> payload() default {};
class Validator implements ConstraintValidator<ValidPassengerCount, Car> {
@Override
public void initialize(ValidPassengerCount constraintAnnotation) {
}
@Override
public boolean isValid(Car value, ConstraintValidatorContext context) {
if (value.getPassengers().size() > value.getPassengers().size()) {
return false;
}
return true;
}
}
上面自定義的約束沒有添加自定義的業(yè)務(wù)屬性,但可以添加任何自定義的方法,然后在Validator的initialize方法中,通過ValidatorPassengerCount獲取自定義方法放回的結(jié)果保存在Validator中,然后在isValid方法使用;
對于isValid方法的返回類型是boolean型,校驗通過返回true,校驗失敗返回false。
2.在需要約束的類定義中添加自定義的約束,已Car為例,如下:
@ValidPassengerCount
class Car {
@Min(4)
private int seatCount;
@NotNull
private List<Passenger> passengers;
// getter setter
}
約束分組(GROUP)
約束分組用來實現(xiàn)部分校驗的功能,例如我們在Car的fields上添加了較多約束,但是在有些場景中我們只需要驗證car的部分屬性,雖然這種場景的使用應(yīng)不多,但我們?nèi)绾螌崿F(xiàn)這種功能呢?
通過前面的例子我們可以看到,在每一個約束中都包含一個groups的屬性,返回class數(shù)組,Validator的validate方法也提供一個輸入groups的參數(shù),我想大家都明白groups是怎么用的了,對,我們就是可以使用groups實現(xiàn)之校驗該跟分組的約束。示例如下:
public class Driver {
@NotNull
public String name;
@Min(
value = 18,
message = "You have to be 18 to drive a car",
groups = DriverChecks.class
)
public int age;
@AssertTrue(
message = "You first have to pass the driving test",
groups = DriverChecks.class
)
public boolean hasDrivingLicense;
public Driver(String name) {
this.name = name;
}
public void passedDrivingTest(boolean b) {
hasDrivingLicense = b;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public interface DriverChecks {
}
上面的示例代碼中使用了兩種group,DEFAULT和DriverChecks,Driver.name屬于DEFAULT 組,Driver.age和Driver.hasDrivingLicense屬于DriverChecks分組,如果只想校驗Driver.name,只需要參照如下示例:
validator.validate(driver, DEFAULT.class);
如果只想校驗Driver.age和Driver.hasDrivingLicense,參考如下示例:
validator.validate(driver, DriverChecks.class);
如果想同時Driver.name、Driver.age和Driver.hasDrivingLicense,參考如下示例:
validator.validate(driver, DEFAULT.class, DriverChecks.class);
校驗的順序與group的先后順序一致。
自定義message
自定義message可以通過多種方式來實現(xiàn)。
- 在屬性或者方法參數(shù)上添加約束注解時,可以在約束的message屬性上設(shè)置自定義的message。如下:
class Car {
@NotNull("car.menufacturer can't be null")
private String manufacturer;
}
- 通過在構(gòu)建Validator實例的過程中,配置ValidatorFactory時,設(shè)置一個MessageInterpolater,如下:
validator = Validation.byProvider(HibernateValidator.class)
.configure()
.messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("message/validate_message")))
.buildValidatorFactory()
.getValidator();
然后在classpath的message目錄下添加validate_message.properties文件,將要翻譯的信息添加到該文件中:

這個是hibernate-validator jar中ValidationMessages.properties文件中的內(nèi)容,默認(rèn)key的規(guī)則都是以定義的約束的class的全限量名加".message"組成。
- 自定義約束設(shè)置message
自定義約束時,在約束的message屬性上設(shè)置default值,如下:
String message() default "{javax.validation.constraints.NotNull.message}";
- 在指定約束的校驗器中,覆蓋默認(rèn)message,如下:
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate( "{com.mycompany.constraints.CheckCase.message}" ).addConstraintViolation();
}
hibernate-validator還包含一些其他的特性,就不細(xì)說了。下面我們看看如何將hibernate-validator應(yīng)用到實際的開發(fā)工作中去。
應(yīng)用例子
對于使用springMvc框架的,要在Controller中校驗方法參數(shù)只需要在參數(shù)上注解@Valid和一些約束注解就可以了。
在使用一些rpc通訊框架時,一般這些rpc框架都不會集成一些參數(shù)校驗的組件,需要我們自己寫,這個時候我們就可以采用hibernate-validator組件了,這個相比自己去寫校驗組件真是快多了。
一般采用rpc做服務(wù)實現(xiàn)時,在服務(wù)實現(xiàn)的第一層,我們通過配置aop的方式對服務(wù)實現(xiàn)類進行代理,在代理中添加校驗的邏輯。如下:
- 寫一個通過hibernate-validator進行校驗的類,如下:
public class HibernateValidateService implements ValidateService {
private static HibernateValidateService INSTANCE;
private static ExecutableValidator validator;
static {
validator = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.ignoreXmlConfiguration()
.parameterNameProvider(new ParanamerParameterNameProvider())
.messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("message/validate_message")))
.buildValidatorFactory()
.getValidator()
.forExecutables();
}
public <T> void validate(T obj, Method method, Object[] parameters) throws OspException {
Set<ConstraintViolation<T>> violations = validator.validateParameters(obj, method, parameters);
if (!violations.isEmpty()) {
ConstraintViolation<T> violation = violations.iterator().next();
throw new IllegalArgumentException(buildErrorMsg(violation));
}
}
private <T> String buildErrorMsg(ConstraintViolation<T> violation) {
Iterator<Path.Node> propertyNodes = violation.getPropertyPath().iterator();
// skip method name
propertyNodes.next();
StringBuilder sb = new StringBuilder();
while (propertyNodes.hasNext()) {
sb.append(propertyNodes.next().getName());
if (propertyNodes.hasNext()) {
sb.append(".");
} else {
sb.append(" ");
}
}
return sb.append(violation.getMessage()).toString();
}
public static ValidateService getInstance() {
if (INSTANCE == null) {
synchronized (HibernateValidateService.class) {
if (INSTANCE == null) {
INSTANCE = new HibernateValidateService();
}
}
}
return INSTANCE;
}
}
- 編寫aop advice
public class ValidateBeforeAdvice implements MethodBeforeAdvice {
private ValidateService validateService = HibernateValidateService.getInstance();
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
validateService.validate(target, method, args);
}
}
- 配置服務(wù)實現(xiàn)層(api層)代理
<bean id="validateBeforeAdvice" class="advice.ValidateBeforeAdvice" />
<aop:config proxy-target-class="false">
<aop:pointcut id="pointcut" expression="execution(* api.validate..*(..))" />
<aop:advisor advice-ref="validateBeforeAdvice" pointcut-ref="pointcut" />
</aop:config>
這里有個注意點就是api層拋出了什么類型的異常,在寫校驗的時候也應(yīng)該拋出什么類型的異常。