Spring核心——数据校验

Java数据校验详解中详细介绍了Java数据校验相关的功能(简称Bean Validation,涵盖JSR-303、JSR-349、JSR-380),本文将在Bean Validation的基础上介绍Spring框架提供的数据校验功能。

Spring提供的数据校验功能分为2个部分,一个是Spring自定义的数据校验功能(以下称为Spring Validation),一个是符合Bean Validation规范的数据校验功能。

Spring Validation数据校验

Spring的自行开发的数据校验功能由3个部分组成:

校验器——Validator,他会运行校验代码。

校验对象,实际上就是一个JavaBean,Validator会对其进行校验。

校验结果——Errors,一次校验的结果都存放在Errors实例中。

这是Spring在Bean Validation规范制定之前就实现的数据校验功能,ValidationUtils的注释中@since标签是2003年5月6号,而JSR-303定稿时间已经是6年之后(2009年)的事了。

Spring的数据校验功能就是实现检验器、校验对象、校验结果三个对象。先声明个一个校验对象(实体):

packagechkui.springcore.example.hybrid.springvalidation.entity;//车辆信息publicclassVehicle{privateString name;privateString type;privateString engine;privateString manufacturer;privateCalendar productionDate;/**Getter Setter*/}

然后针对这个实体声明一个校验器。校验器要实现org.springframework.validation.Validator接口:

packagechkui.springcore.example.hybrid.springvalidation.validator;publicclassVehicleValidatorimplementsValidator{privateList _TYPE = Arrays.asList(newString[] {"CAR","SUV","MPV"});publicbooleansupports(Class<?> clazz){//将验证器和实体类进行绑定,如果这里返回false在验证过程中会抛出类型不匹配的异常returnVehicle.class.isAssignableFrom(clazz);}publicvoidvalidate(Object target, Errors errors){//验证数据Vehicle vehicle = Vehicle.class.cast(target);if(null== vehicle.getName()) {//使用验证工具绑定结果ValidationUtils.rejectIfEmpty(errors,"name","name.empty","车辆名称为空");}if(!_TYPE.contains(vehicle.getType())) {//向Error添加验证错误信息<2> errors.rejectValue("type","type.error","汽车类型必须是"+ _TYPE);}//More validate ......}}

有了验证对象(JavaBean)和对应的验证器(Validator)就完成了一组验证功能。注意VehicleValidator::validate方法传递的errors参数,验证工具会将错误实例传递进来交给开发者去组装验证结果。

代码中的ValidationUtils就是数据校验工具,他提供了2个功能:

执行校验(接下来会马上介绍)。

提供错误信息绑定的功能,例如ValidationUtils.rejectIfEmpty这一行代码。会将对应的信息写入到Errors中。

有了验证对象和验证器就可以执行验证:

publicclassSpringValidationApp{privatestaticvoidspringValidation(ApplicationContext ctx){VehicleValidator vehicleValidator =newVehicleValidator();//创建验证器Vehicle vehicle =newVehicle();//创建验证对象<1> ValidationError error =newValidationError("Vehicle");//创建错误信息ValidationUtils.invokeValidator(vehicleValidator, vehicle, error);//执行验证List list = error.getFieldErrors();intcount =1;//输出验证结果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("-");}}}

执行完毕后,ValidationError中记录了所有校验错误信息。错误信息分为4个部分:

验证的对象的名称:在执行验证器的代码中<1>部分创建错误对象时指定。Vehicle就是验证对象的名称。

错误的域、错误code和错误信息:每一个错误都有对应的域、错误编码以及错误信息,在验证器<2>位置的代码就是指定错误信息。

以上错误信息可以通过error.getFieldErrors();来获取。

如果JavaBean有嵌套的结构,可以在校验器中调用其他的校验器来实现嵌套检验。先为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");//执行对Gearbox的校验ValidationUtils.invokeValidator(gearboxValidator, vehicle.getGearbox(), errors);}}}

Bean Validation数据校验

Spring现在推荐使用Bean Validation来进行数据校验,而且已经整合到Spring MVC框架中。

在Spring中使用Bean ValidationJava数据校验详解一文中介绍的内容差不多——也是注解和校验器组成一个约束,通过注解来控制校验的过程。

Spring核心部分没有提供Bean Validation相关的实现类,所以需要引入对应的实现框架。本文引入的是Hibernate Validator,他包括验证器和el,详情可以看源码根目录的build.gradle文件。

首先我们向IoC容器中添加全局校验器:

@ConfigurationpublicclassSpringValidationConfig{@Bean("validator")publicValidatorvalidator(){returnnewLocalValidatorFactoryBean();}

这一段添加Bean的代码非常简单,就是新建了一个LocalValidatorFactoryBean实例。LocalValidatorFactoryBean实现了javax.validation.Validator接口,并且会自动使用已经引入的Bean Validation框架。

然后向Vehicle增加Bean Validation相关的注解:

publicclassVehicle{@NotBlankprivateString name;@NotBlank@VehicleTypeprivateString type;@NotBlankprivateString engine;@NotBlankprivateString manufacturer;<3>@Valid//@Valid的作用是对嵌套的解构进行校验privateGearbox gearbox;@ValidprivateTyre tyre;@VehicleProductionDateprivateCalendar productionDate;/**Getter Setter*/}

在上面的代码中,除了常规的@NotBlank等注解,还有@VehicleType这个自定义注解。在代码<3>的位置@Valid是告诉校验器还要对gearbox的实例进行校验,相当于前面介绍的嵌套校验功能。最后我们使用检验器来对Vehicle的实例进行校验:

publicclassSpringValidationApp{publicstaticvoidmain(String[] args){ApplicationContext ctx =newAnnotationConfigApplicationContext(SpringValidationConfig.class);BeanValidation(ctx);//JSR规范验证}privatestaticvoidBeanValidation(ApplicationContext ctx){Validator validator = ctx.getBean(Validator.class);//获取校验器Vehicle vehicle =newVehicle();//新建要校验的对象validator.validate(vehicle).forEach(err -> {//执行校验print("Field: ", err.getPropertyPath());print("Error: ", err.getMessage());});}}

关于Bean Validation的详细使用方法已经在Java数据校验详解介绍。

兼容Bean Validation和Spring Validation

一些相对比较久远的项目可能会遇见在Spring Validation的基础上新增Bean Validation功能的情况。可以使用SpringValidatorAdapter适配器来解决这个问题:

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);//执行校验List list = error.getFieldErrors();//检验信息// Bean Validation 校验adapter.validate(vehicle).forEach(err -> {// 执行检验&输出校验结果print("Field: ", err.getPropertyPath());print("Error: ", err.getMessage());});}}

上面的代码使用SpringValidatorAdapter分别执行了Bean ValidationSpring Validation。可以将SpringValidatorAdapter看作一个org.springframework.validation.Validator的实现类用ValidationUtils来执行校验,而验证的过程完全是按照Bean Validation的规范来执行的。

方法参数校验

除了校验一个实体类,Spring在Bean Validation的基础上使用后置处理器和AOP实现了方法参数的检验。例如下面的方法:

publicinterfacePersonService{public@NotBlankStringexecute(@NotBlank(message ="必须设置人员名称")String name,@Min(value =18, message ="年龄必须大于18")intage);}

他表示返回数据不能为空字符串,传入的2个参数name不能为空字符串、age必须大于18。

要启用方法参数校验关键点是引入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;}}

然后实现上面的PersonService接口并标记@Validated表示这个类中的方法要进行参数校验:

@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);//方法参数校验}privatestaticvoidmethodValidation(ApplicationContext ctx){//对方法进行参数校验try{PersonService personService = ctx.getBean(PersonService.class);personService.execute(null,1);//传递参数}catch(ConstraintViolationException error) {error.getConstraintViolations().forEach(err -> {//输出校验错误信息print("Field: ", err.getPropertyPath());print("Error: ", err.getMessage());});}}}

在运行的过程中,如果参数或返回数据不符合验证规则会抛出ConstraintViolationException异常,可以从中获取校验错误的信息。

欢迎工作一到五年的Java工程师朋友们加入Java技术交流群:659270626

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

c 9����Wj

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容