collection集合 最新_关于Spring集合非空校验无效的问题分析

数据校验在处理应用业务逻辑中是我们必须考虑和面对的问题,我们需要确保输入在一定条件下是正确的,在Spring应用中我们通常使用Bean Validation做数据校验。本文主要介绍在使用Bean Validation过程中遇到的一个奇怪的问题并进行了一定程度上的源码分析。

Bean Validation

首先介绍一下Bean Validation规范,该规范主要有3个发展历程:

  • 2009-11-16初始的1.0版本主要基于注解定义了JavaBean验证的元数据模型和API(这个版本的规范为JSR303)
  • 2013年标准化了Java平台的约束定义、声明和验证,是1.1版本(对应的规范为JSR349)
  • 随着Java8的发行,2017年基于Java8做了改进优化,是2.0版本(对应的规范为JSR380)

具体演进流程可以参考下图:

6249f555f2c4d0984e9d808c78d1e2c1.png
图片来源:jcp.org

一般我们使用Hibernate Validator作为Bean Validation的规范实现。规范与实现的最新Maven依赖如下:

<dependency>    <groupId>javax.validationgroupId>    <artifactId>validation-apiartifactId>    <version>2.0.1.Finalversion>dependency><dependency>    <groupId>org.hibernate.validatorgroupId>    <artifactId>hibernate-validatorartifactId>    <version>6.1.5.Finalversion>dependency>

通常在SpringBoot项目中都是直接引入spring-boot-stater-web,由SpringBoot自动引入校验依赖。

各个不同版本的Bean Validation规范对应不同的Hibernate Validator实现,确保实现与规范版本匹配。

2.0提供的常用约束

javax.validation:validation-api:2.0.1.Final提供的Constraint约束主要有:

注解约束说明
@Null注解标注的元素必须为null
@NotNull注解标注的元素必须非null
@AssertFalse注解标注的元素必须为false【只能标注在boolean及其包装类上】
@AssertTrue注解标注的元素必须为true【只能标注在boolean及其包装类上】
@Digits(integer, fraction)注解标注的元素整数部分必须不大于integer位,小数部分位数必须不大于fraction位【只能标注在BigInteger、BigDecimal、CharSequence或者byte、short、int、long及其包装类上】
@Email(regexp)注解标注的元素必须符合邮箱命名格式,可以额外指定必须满足指定正则规则【只能标注在CharSequence】
@Size(min,max)注解标注的元素长度必须在指定范围内【只能标注在CharSequence、Collection、Map或者Array上】
@Pattern(regexp)注解标注的元素必须满足指定的正则规则【只能标注在CharSequence上】
@NotBlank注解标注的字符序列必须非null并且至少包含一个非空字符【只能标注在CharSequence上】
@NotEmpty注解标注的元素必须非null并且长度非0【只能标注在CharSequence、Collection、Map或者Array上】
@Min(value)注解标注的元素必须是数字并且大于等于指定值【只能标注在BigInteger、BigDecimal或者byte、short、int、long及其包装类上】
@Max(value)注解标注的元素必须是数字并且小于等于指定值【只能标注在BigInteger、BigDecimal或者byte、short、int、long及其包装类上】
@Negative注解标注的元素必须是负数【只能标注在BigInteger、BigDecimal或者byte、short、int、long、float、double及其包装类上】
@NegativeOrZero注解标注的元素必须是非正数(0或者负数)【只能标注在BigInteger、BigDecimal或者byte、short、int、long、float、double及其包装类上】
@Positive注解标注的元素必须是正数【只能标注在BigInteger、BigDecimal或者byte、short、int、long、float、double及其包装类上】
@PositiveOrZero注解标注的元素必须是非负数(0或者正数)【只能标注在BigInteger、BigDecimal或者byte、short、int、long、float、double及其包装类上】
@Past注解标注的元素必须表示过去的时间【常用于Date、JSR310规范的Instant、LocalDateTime、LocalDate、LocalTime、ZonedDateTime】
@PastOrPresent注解标注的元素必须表示当前时间或者过去的时间【常用于Date、JSR310规范的Instant、LocalDateTime、LocalDate、LocalTime、ZonedDateTime】
@Future注解标注的元素必须表示未来的时间【常用于Date、JSR310规范的Instant、LocalDateTime、LocalDate、LocalTime、ZonedDateTime】
@FutureOrPresent注解标注的元素必须表示当前时间或者未来的时间【常用于Date、JSR310规范的Instant、LocalDateTime、LocalDate、LocalTime、ZonedDateTime】

Bean Validation提供的数据校验支持级联校验、分组校验、自定义校验等功能,具体如何使用在这里就不展开篇幅了。下面让我们看看具体的问题是什么。

方法入参为集合容器对象的非空校验

这里的集合容器指的是ListSetMap以及Array,即必须符合@NotEmpty的注解标注元素约束并且是容器类型。这里以List为例,先看下面这段代码:

@Slf4j@RestController@AllArgsConstructor@RequestMapping("/valid")public class ValidController {    @PostMapping("/list")    public boolean list(@RequestBody @Valid @NotEmpty List items) {        return true;    }}

当以[]空请求体请求/valid/list时,你认为@NotEmpty会不会生效,是否会抛出MethodArgumentNotValidException

直接公布答案:不会

实际上看这里的代码,应该是有两层校验的,一层是@RequestBody的请求体必须存在校验,另一层才是我们使用@NotEmpty约束的集合成员数必须非空校验。现在这个请求没有抛出任何异常,但是给这个类标注@Validated注解后,再次执行却抛出了ConstraintViolationException异常。这是为什么呢?

源码分析

通过断点调试一步步追踪,我们发现,当为类添加了@Validated注解后,请求最终会被Springorg.springframework.validation.beanvalidation.MethodValidationInterceptor拦截下来,该拦截器会对方法入参和出参做相应的校验,如果校验失败,那么就抛出ConstraintViolationException。如下图所示,List的非空校验失败了。

5ab3d3fe7da65daa688910864d255c63.png
MethodValidationInterceptor参数校验

那么为什么添加了@Validated注解后,MethodValidationInterceptor就会拦截并对方法进行参数校验呢?

继续分析源码,发现原来在ValidationAutoConfiguration自动配置类中会启动一个MethodValidationPostProcessor方法校验后置处理器。而MethodValidationPostProcessor在自身实例化后会创建一个匹配@Validated注解的切入点AnnotationMatchingPointcut(注解匹配时支持继承获得的@Validated注解),同时会创建一个方法校验通知/增强MethodValidationInterceptor,并将两者构建成一个切点切面DefaultPointcutAdvisor

ff820d296299818365327f945273c9b8.png
MVPP实例化切点切面

同时,由于MethodValidationPostProcessor是一个BeanPostProcessor,当SpringBean实例化后,在postProcessAfterInitialization方法中,会对符合条件(自身或者父类/接口标注了@Validated注解)的SpringBean进行切面增强然后返回代理类。

cd8b74a0a3a5662891ce5812761e234c.png
MVPP为符合条件的SpringBean添加方法校验增强通知

这样,当代理对象被调用时,在经过RequestResponseBodyMethodProcessor#resolveArgument方法进行的参数校验后(校验失败将抛出MethodArgumentNotValidException异常,但这一步识别不了List的非空校验)后,由MethodValidationInterceptor进行校验并最终抛出ConstraintViolationException异常。

RequestResponseBodyMethodProcessor#resolveArgument里的校验走ValidatorImpl#validate(由于入参List被当做Bean校验,而List并没有需要校验的成员,所以认为不需要校验),而MethodValidationInterceptor里的校验走ValidatorImpl#validateParameters(直接将List当做参数进行校验)。

543de6bffbb0338faef03605c71a7592.png
两者的校验方法不同

到此,我们基本清楚了为什么添加了@Validated之后,List上的@NotEmpty校验会生效。这个问题不太容易发现,而且挺有意思的。

延伸扩展

这里扩展一个问题,在使用OpenFeign的时候也是可以使用数据校验的。我的问题是:如果将@Validated@FeignClient一起标注在接口上(该服务为B),那么当服务调用方(该服务为A)进行远程方法调用时会触发MethodValidationInterceptor的方法校验吗?还是说只在服务提供方会校验。这个问题的答案在文本有提及,细心的朋友应该知道答案了。

- END -
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值