Hibernate Validator實戰(zhàn)篇

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


application-layers.png

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

Paste_Image.png

而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的屬性

  1. 先定義一個需要校驗的類,這里采用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
}
  1. 獲取Validator
  ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                           .configure()
                           .buildValidatorFactory();
   Validator validator = factory.getValidator();
  1. 使用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ù)。

  1. 首先獲取ExecutableValidator
ExecutableValidator executableValidator = validator.forExecutables();

通過之前獲取的Validator得到ExecutableValidater.

  1. 通過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)常遇到需要校驗的場景有:

  1. 對象的級聯(lián)校驗;
    比如Car中引用一個對象Driver,在校驗Car的同時也需要校驗Driver中的屬性。
class Car {
  @NotNull
   private String manufacturer;
   private Driver driver;
}
  1. 對象中關(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;
}
  1. 方法中參數(shù)關(guān)聯(lián)校驗;
    比如Car對象的方法buildCar的簽名如下:
public Car buildCar(int seatCount, List<Passenger> passengers) {
  // ...
  return null;
}

要求乘客數(shù)量小于座位數(shù)。

  1. 默認(rèn)情況下,validator會對被校驗對象的所有屬性進行校驗,能否只校驗一部分?
  2. 如何自定義約束呢?
  3. 如何自定義校驗結(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)。

  1. 在屬性或者方法參數(shù)上添加約束注解時,可以在約束的message屬性上設(shè)置自定義的message。如下:
class Car {
  @NotNull("car.menufacturer can't be null")
  private String manufacturer;
}
  1. 通過在構(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文件,將要翻譯的信息添加到該文件中:

image.png

這個是hibernate-validator jar中ValidationMessages.properties文件中的內(nèi)容,默認(rèn)key的規(guī)則都是以定義的約束的class的全限量名加".message"組成。

  1. 自定義約束設(shè)置message
    自定義約束時,在約束的message屬性上設(shè)置default值,如下:
String message() default "{javax.validation.constraints.NotNull.message}";
  1. 在指定約束的校驗器中,覆蓋默認(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)類進行代理,在代理中添加校驗的邏輯。如下:

  1. 寫一個通過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;
    }

}
  1. 編寫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);
    }
}
  1. 配置服務(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)該拋出什么類型的異常。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容