JSR-303 规范
在程序进行数据处理之前,对数据进行准确性校验是我们必须要考虑的事情。尽早发现数据错误,不仅可以防止错误向核心业务逻辑蔓延,而且这种错误非常明显,容易发现解决。
JSR303 规范(Bean Validation 规范)为 JavaBean 验证定义了相应的元数据模型和 API。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
关于 JSR 303 – Bean Validation 规范,可以参考官网
对于 JSR 303 规范,Hibernate Validator 对其进行了参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。如果想了解更多有关 Hibernate Validator 的信息,请查看官网。
validation-api 内置的 constraint 清单
Hibernate Validator 附加的 constraint
Hibernate Validator 不同版本附加的 Constraint 可能不太一样,具体还需要你自己查看你使用版本。Hibernate 提供的 Constraint在这个包下面。
一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。
有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制。可以通过两种方法去实现,一种是组合现有的 constraint 来生成一个更复杂的 constraint,另外一种是开发一个全新的 constraint。
使用Spring Boot进行数据校验
Spring Validation 对 hibernate validation 进行了二次封装,可以让我们更加方便地使用数据校验功能。这边我们通过 Spring Boot 来引用校验功能。
如果你用的 Spring Boot 版本小于 2.3.x,spring-boot-starter-web 会自动引入 hibernate-validator 的依赖。如果 Spring Boot 版本大于 2.3.x,则需要手动引入依赖:
直接参数校验
有时候接口的参数比较少,只有一个活着两个参数,这时候就没必要定义一个DTO来接收参数,可以直接接收参数。
下面是统一异常处理类
调用结果
实体类DTO校验
定义一个DTO
接收参数时使用@Validated进行校验
统一异常处理
调用结果
对Service层方法参数校验
个人不太喜欢这种校验方式,一半情况下调用service层方法的参数都需要在controller层校验好,不需要再校验一次。这边列举这个功能,只是想说 Spring 也支持这个。
分组校验
有时候对于不同的接口,需要对DTO进行不同的校验规则。还是以上面的UserDTO为列,另外一个接口可能不需要将age限制在18~50之间,只需要大于18就可以了。
这样上面的校验规则就不适用了。分组校验就是来解决这个问题的,同一个DTO,不同的分组采用不同的校验策略。
使用方式
使用Group1分组进行校验,因为DTO中,Group1分组对name属性没有校验,所以这个校验将不会生效。
分组校验的好处是可以对同一个DTO设置不同的校验规则,缺点就是对于每一个新的校验分组,都需要重新设置下这个分组下面每个属性的校验规则。
分组校验还有一个按顺序校验功能。
考虑一种场景:一个bean有1个属性(假如说是attrA),这个属性上添加了3个约束(假如说是@NotNull、@NotEmpty、@NotBlank)。默认情况下,validation-api对这3个约束的校验顺序是随机的。也就是说,可能先校验@NotNull,再校验@NotEmpty,最后校验@NotBlank,也有可能先校验@NotBlank,再校验@NotEmpty,最后校验@NotNull。
那么,如果我们的需求是先校验@NotNull,再校验@NotBlank,最后校验@NotEmpty。@GroupSequence注解可以实现这个功能。
使用方式
嵌套校验
前面的示例中,DTO类里面的字段都是基本数据类型和String等类型。
但是实际场景中,有可能某个字段也是一个对象,如果我们需要对这个对象里面的数据也进行校验,可以使用嵌套校验。
假如UserDTO中还用一个Job对象,比如下面的结构。需要注意的是,在job类的校验上面一定要加上@Valid注解。
使用方式
测试结果
嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List字段会对这个list里面的每一个Job对象都进行校验。这个点
在下面的@Valid和@Validated的区别章节有详细讲到。
集合校验
如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:
包装List类型,并声明@Valid注解
调用方法
调用结果
会抛出NotReadablePropertyException异常,需要对这个异常做统一处理。这边代码就不贴了。
自定义校验器
在Spring中自定义校验器非常简单,分两步走。
自定义约束注解
实现ConstraintValidator接口编写约束校验器
编程式校验
上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入
javax.validation.Validator对象,然后再调用其api。
快速失败(Fail Fast)配置
Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。
校验信息的国际化
Spring 的校验功能可以返回很友好的校验信息提示,而且这个信息支持国际化。
这块功能暂时暂时不常用,具体可以参考这篇文章
@Validated和@Valid的区别联系
首先,@Validated和@Valid都能实现基本的验证功能,也就是如果你是想验证一个参数是否为空,长度是否满足要求这些简单功能,使用哪个注解都可以。
但是这两个注解在分组、注解作用的地方、嵌套验证等功能上两个有所不同。下面列下这两个注解主要的不同点。
@Valid注解是JSR303规范的注解,@Validated注解是Spring框架自带的注解;
@Valid不具有分组校验功能,@Validate具有分组校验功能;
@Valid可以用在方法、构造函数、方法参数和成员属性(字段)上,@Validated可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上,两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能;
@Valid加在成员属性上可以对成员属性进行嵌套验证,而@Validate不能加在成员属性上,所以不具备这个功能。
这边说明下,什么叫嵌套验证。
我们现在有个实体叫做Item:
Item带有很多属性,属性里面有:pid、vid、pidName和vidName,如下所示:
属性这个实体也有自己的验证机制,比如pid和vid不能为空,pidName和vidName不能为空等。
现在我们有个ItemController接受一个Item的入参,想要对Item进行验证,如下所示:
在上图中,如果Item实体的props属性不额外加注释,只有@NotNull和@Size,无论入参采用@Validated还是@Valid验证,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证,也就是@Validated和@Valid加在方法参数前,都不会自动对参数进行嵌套验证。也就是说如果传的List中有Prop的pid为空或者是负数,入参验证不会检测出来。
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
我们修改Item类如下所示:
然后我们在ItemController的addItem函数上再使用@Validated或者@Valid,就能对Item的入参进行嵌套验证。此时Item里面的props如果含有Prop的相应字段为空的情况,Spring Validation框架就会检测出来,bindingResult就会记录相应的错误。
Spring Validation原理简析
现在我们来简单分析下Spring校验功能的原理。
方法级别的参数校验实现原理
所谓的方法级别的校验就是指将@NotNull和@NotEmpty这些约束直接加在方法的参数上的。
比如
或者
都属于方法级别的校验。这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。
其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。
接着看一下MethodValidationInterceptor:
DTO级别的校验
这种属于DTO级别的校验。在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中。
可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。
看到这里,大家应该能明白为什么这种场景下@Validated、@Valid两个注解可以混用。我们接下来继续看WebDataBinder.validate()实现。
最终发现底层最终还是调用了Hibernate Validator进行真正的校验处理。
404等错误的统一处理
参考博客
参考
领取专属 10元无门槛券
私享最新 技术干货