在Java數(shù)據(jù)校驗詳解中詳細(xì)介紹了Java數(shù)據(jù)校驗相關(guān)的功能(簡稱Bean Validation,涵蓋JSR-303、JSR-349、JSR-380),本文將在Bean Validation的基礎(chǔ)上介紹Spring框架提供的數(shù)據(jù)校驗功能。
Spring提供的數(shù)據(jù)校驗功能分為2個部分,一個是Spring自定義的數(shù)據(jù)校驗功能(以下稱為Spring Validation),一個是符合Bean Validation規(guī)范的數(shù)據(jù)校驗功能。
Spring Validation數(shù)據(jù)校驗
Spring的自行開發(fā)的數(shù)據(jù)校驗功能由3個部分組成:
校驗器——Validator,他會運(yùn)行校驗代碼。
校驗對象,實際上就是一個JavaBean,Validator會對其進(jìn)行校驗。
校驗結(jié)果——Errors,一次校驗的結(jié)果都存放在Errors實例中。
這是Spring在Bean Validation規(guī)范制定之前就實現(xiàn)的數(shù)據(jù)校驗功能,ValidationUtils的注釋中@since標(biāo)簽是2003年5月6號,而JSR-303定稿時間已經(jīng)是6年之后(2009年)的事了。
Spring的數(shù)據(jù)校驗功能就是實現(xiàn)檢驗器、校驗對象、校驗結(jié)果三個對象。先聲明個一個校驗對象(實體):
packagechkui.springcore.example.hybrid.springvalidation.entity;//車輛信息publicclassVehicle{privateString name;privateString type;privateString engine;privateString manufacturer;privateCalendar productionDate;/**Getter Setter*/}
然后針對這個實體聲明一個校驗器。校驗器要實現(xiàn)org.springframework.validation.Validator接口:
packagechkui.springcore.example.hybrid.springvalidation.validator;publicclassVehicleValidatorimplementsValidator{privateList _TYPE = Arrays.asList(newString[] {"CAR","SUV","MPV"});publicbooleansupports(Class<?> clazz){//將驗證器和實體類進(jìn)行綁定,如果這里返回false在驗證過程中會拋出類型不匹配的異常returnVehicle.class.isAssignableFrom(clazz);}publicvoidvalidate(Object target, Errors errors){//驗證數(shù)據(jù)Vehicle vehicle = Vehicle.class.cast(target);if(null== vehicle.getName()) {//使用驗證工具綁定結(jié)果ValidationUtils.rejectIfEmpty(errors,"name","name.empty","車輛名稱為空");}if(!_TYPE.contains(vehicle.getType())) {//向Error添加驗證錯誤信息<2> errors.rejectValue("type","type.error","汽車類型必須是"+ _TYPE);}//More validate ......}}
有了驗證對象(JavaBean)和對應(yīng)的驗證器(Validator)就完成了一組驗證功能。注意VehicleValidator::validate方法傳遞的errors參數(shù),驗證工具會將錯誤實例傳遞進(jìn)來交給開發(fā)者去組裝驗證結(jié)果。
代碼中的ValidationUtils就是數(shù)據(jù)校驗工具,他提供了2個功能:
執(zhí)行校驗(接下來會馬上介紹)。
提供錯誤信息綁定的功能,例如ValidationUtils.rejectIfEmpty這一行代碼。會將對應(yīng)的信息寫入到Errors中。
有了驗證對象和驗證器就可以執(zhí)行驗證:
publicclassSpringValidationApp{privatestaticvoidspringValidation(ApplicationContext ctx){VehicleValidator vehicleValidator =newVehicleValidator();//創(chuàng)建驗證器Vehicle vehicle =newVehicle();//創(chuàng)建驗證對象<1> ValidationError error =newValidationError("Vehicle");//創(chuàng)建錯誤信息ValidationUtils.invokeValidator(vehicleValidator, vehicle, error);//執(zhí)行驗證List list = error.getFieldErrors();intcount =1;//輸出驗證結(jié)果for(FieldError res : list) {print("Error Info ", count++ ,".");print("Entity:", res.getObjectName());print("Field:", res.getField());print("Code:", res.getCode());print("Message:", res.getDefaultMessage());print("-");}}}
執(zhí)行完畢后,ValidationError中記錄了所有校驗錯誤信息。錯誤信息分為4個部分:
驗證的對象的名稱:在執(zhí)行驗證器的代碼中<1>部分創(chuàng)建錯誤對象時指定。Vehicle就是驗證對象的名稱。
錯誤的域、錯誤code和錯誤信息:每一個錯誤都有對應(yīng)的域、錯誤編碼以及錯誤信息,在驗證器<2>位置的代碼就是指定錯誤信息。
以上錯誤信息可以通過error.getFieldErrors();來獲取。
如果JavaBean有嵌套的結(jié)構(gòu),可以在校驗器中調(diào)用其他的校驗器來實現(xiàn)嵌套檢驗。先為Vehicle類增加一個Gearbox(變速箱)域:
packagechkui.springcore.example.hybrid.springvalidation.entity;//車輛信息publicclassVehicle{privateString name;privateString type;privateString engine;privateString manufacturer;privateGearbox gearbox;//Gearbox是另外一個實例privateCalendar productionDate;/**Getter Setter*/}
//變速箱publicclassGearbox{privateString name;privateString manufacturer;/**Getter Setter*/}
在校驗器VehicleValidator::validate中增加對Gearbox驗證:
publicclassVehicleValidatorimplementsValidator{@AutowiredGearboxValidator gearboxValidator;//用于校驗Gearbox的校驗器@Overridepublicvoidvalidate(Object target, Errors errors){Vehicle vehicle = Vehicle.class.cast(target);//some code ......}if(null== vehicle.getGearbox()) {errors.rejectValue("gearbox","gearbox.error","變速箱信息為空");}else{//指定子實體的名稱errors.pushNestedPath("gearbox");//執(zhí)行對Gearbox的校驗ValidationUtils.invokeValidator(gearboxValidator, vehicle.getGearbox(), errors);}}}
Bean Validation數(shù)據(jù)校驗
Spring現(xiàn)在推薦使用Bean Validation來進(jìn)行數(shù)據(jù)校驗,而且已經(jīng)整合到Spring MVC框架中。
在Spring中使用Bean Validation和Java數(shù)據(jù)校驗詳解一文中介紹的內(nèi)容差不多——也是注解和校驗器組成一個約束,通過注解來控制校驗的過程。
Spring核心部分沒有提供Bean Validation相關(guān)的實現(xiàn)類,所以需要引入對應(yīng)的實現(xiàn)框架。本文引入的是Hibernate Validator,他包括驗證器和el,詳情可以看源碼根目錄的build.gradle文件。
首先我們向IoC容器中添加全局校驗器:
@ConfigurationpublicclassSpringValidationConfig{@Bean("validator")publicValidatorvalidator(){returnnewLocalValidatorFactoryBean();}
這一段添加Bean的代碼非常簡單,就是新建了一個LocalValidatorFactoryBean實例。LocalValidatorFactoryBean實現(xiàn)了javax.validation.Validator接口,并且會自動使用已經(jīng)引入的Bean Validation框架。
然后向Vehicle增加Bean Validation相關(guān)的注解:
publicclassVehicle{@NotBlankprivateString name;@NotBlank@VehicleTypeprivateString type;@NotBlankprivateString engine;@NotBlankprivateString manufacturer;<3>@Valid//@Valid的作用是對嵌套的解構(gòu)進(jìn)行校驗privateGearbox gearbox;@ValidprivateTyre tyre;@VehicleProductionDateprivateCalendar productionDate;/**Getter Setter*/}
在上面的代碼中,除了常規(guī)的@NotBlank等注解,還有@VehicleType這個自定義注解。在代碼<3>的位置@Valid是告訴校驗器還要對gearbox的實例進(jìn)行校驗,相當(dāng)于前面介紹的嵌套校驗功能。最后我們使用檢驗器來對Vehicle的實例進(jìn)行校驗:
publicclassSpringValidationApp{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringValidationConfig.class);BeanValidation(ctx);//JSR規(guī)范驗證}privatestaticvoidBeanValidation(ApplicationContext ctx){Validator validator = ctx.getBean(Validator.class);//獲取校驗器Vehicle vehicle =newVehicle();//新建要校驗的對象validator.validate(vehicle).forEach(err -> {//執(zhí)行校驗print("Field: ", err.getPropertyPath());print("Error: ", err.getMessage());});}}
關(guān)于Bean Validation的詳細(xì)使用方法已經(jīng)在Java數(shù)據(jù)校驗詳解介紹。
兼容Bean Validation和Spring Validation
一些相對比較久遠(yuǎn)的項目可能會遇見在Spring Validation的基礎(chǔ)上新增Bean Validation功能的情況??梢允褂肧pringValidatorAdapter適配器來解決這個問題:
publicclassSpringValidationApp{privatestaticvoidadapterValidation(ApplicationContext ctx){// 獲取校驗器// LocalValidatorFactoryBean繼承了SpringValidatorAdapter// 所以這里就是獲取LocalValidatorFactoryBeanSpringValidatorAdapter adapter = ctx.getBean(SpringValidatorAdapter.class);Vehicle vehicle =newVehicle();// 檢驗對象ValidationError error =newValidationError("Vehicle");// Spring ValidationValidationUtils.invokeValidator(adapter, vehicle, error);//執(zhí)行校驗List list = error.getFieldErrors();//檢驗信息// Bean Validation 校驗adapter.validate(vehicle).forEach(err -> {// 執(zhí)行檢驗&輸出校驗結(jié)果print("Field: ", err.getPropertyPath());print("Error: ", err.getMessage());});}}
上面的代碼使用SpringValidatorAdapter分別執(zhí)行了Bean Validation和Spring Validation??梢詫pringValidatorAdapter看作一個org.springframework.validation.Validator的實現(xiàn)類用ValidationUtils來執(zhí)行校驗,而驗證的過程完全是按照Bean Validation的規(guī)范來執(zhí)行的。
方法參數(shù)校驗
除了校驗一個實體類,Spring在Bean Validation的基礎(chǔ)上使用后置處理器和AOP實現(xiàn)了方法參數(shù)的檢驗。例如下面的方法:
publicinterfacePersonService{public@NotBlankStringexecute(@NotBlank(message ="必須設(shè)置人員名稱")String name,@Min(value =18, message ="年齡必須大于18")intage);}
他表示返回數(shù)據(jù)不能為空字符串,傳入的2個參數(shù)name不能為空字符串、age必須大于18。
要啟用方法參數(shù)校驗關(guān)鍵點是引入MethodValidationPostProcessor并在需要驗證的Bean上增加一個@Validated注解。
先通過@Configuration引入后置處理器:
@Configuration@ComponentScan("chkui.springcore.example.hybrid.springvalidation.service")publicclassSpringValidationConfig{@Bean("validator")publicValidatorvalidator(){returnnewLocalValidatorFactoryBean();}@BeanpublicMethodValidationPostProcessormethodValidationPostProcessor(Validator validator){MethodValidationPostProcessor postProcessor =newMethodValidationPostProcessor();postProcessor.setValidator(validator);returnpostProcessor;}}
然后實現(xiàn)上面的PersonService接口并標(biāo)記@Validated表示這個類中的方法要進(jìn)行參數(shù)校驗:
@Service@ValidatedpublicclassPersonServiceImplimplementsPersonService{@OverridepublicStringexecute(String name,intage){return"I'm "+ name +". "+ age +" years old.";}}
最后使用這個Service:
publicclassSpringValidationApp{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringValidationConfig.class);methodValidation(ctx);//方法參數(shù)校驗}privatestaticvoidmethodValidation(ApplicationContext ctx){//對方法進(jìn)行參數(shù)校驗try{PersonService personService = ctx.getBean(PersonService.class);personService.execute(null,1);//傳遞參數(shù)}catch(ConstraintViolationException error) {error.getConstraintViolations().forEach(err -> {//輸出校驗錯誤信息print("Field: ", err.getPropertyPath());print("Error: ", err.getMessage());});}}}
在運(yùn)行的過程中,如果參數(shù)或返回數(shù)據(jù)不符合驗證規(guī)則會拋出ConstraintViolationException異常,可以從中獲取校驗錯誤的信息。
歡迎工作一到五年的Java工程師朋友們加入Java技術(shù)交流群:659270626
群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構(gòu)資料)合理利用自己每一分每一秒的時間來學(xué)習(xí)提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
c 9????Wj