背景
报错信息为 HV000064: Unable to instantiate ConstraintValidator
以及 NoSuchMethodException: com.seliote.mlb.common.jsr303.minio.validator.PathValidator.<init>()
单纯看报错问题提示的就已经听明显的,ConstraintValidator
实例会被 Spring 自动识别并为其注入依赖 Bean,但是在 Validator 中构造器 @Autowired
注入的时候并没有找到相应的 MlbService
Bean,所以 Spring 框架又试图去找默认无参构造器,然而并没有找到,所以就抛出了无法实例化,无该方法的报错。
但是,但是,这种写法讲道理是没有问题的,为什么这里 Bean 容器里找不到要注入的 Bean 呢?其他的 Validator 都是正常工作的,为什么这个就存在问题?
排查
两天连着遇到 Hibernate 的诡异问题,让我有点怀疑 Hibernate 了。
一开始先去查了一下 StackOverFlow,但是一堆评论说是让改成 setter 注入或者域注入的,甚至还有让加个默认无参构造器的,excuse me? 掩耳盗铃吗?Spring 因为找不到 Bean 转去使用上面的方法,把问题变成空指针的问题,这个问题就解决了,可真是妙娃种子吃妙脆角,妙到家了。
尝试将其他自定义注解放在实体上,同样的报错,可以推出不是这个注解定义的问题,而是自定义注解放在 Entity 层上时 Bean 注入就会有问题。
跟一下源码吧。
创建一个 NoSuchMethodException
异常断点,然后查看堆栈
可以看到这里的栈还是比较深的。
根据堆栈走到 BeanValidationEventListener
里,二十分钟过去了,终于走到了核心代码
这就奇怪了,直接调用用的默认构造器???难道实体类的构造的时候走的不是同一个逻辑???
OK,放生吧,逻辑到这已经比较清楚了,至少对于注解在 JPA 实体类上的 Path
注解,Hibernate 是先查找缓存,没有的话就进行构造,构造时查找默认无参构造器。
让我们来看看个正常构造的,直接在构造器里面打断点,证明一下确实调用的是有参数的构造器,把 Path
注解随意注释在一个 Service 方法参数中。
确实走到了有参构造器,接下来就是对比一下堆栈,看看有什么不同。
先去干晚饭,晚饭还没吃,上来继续,明天周六哈哈哈哈哈哈哈哈哈哈哈哈哈哈。
炸串加馍真好吃,我回来了。
丢?这里的 ConstraintValidatorFactory
是 SpringConstraintValidatorFactory
,而有问题的则是 ConstraintValidatorFactoryImpl
。
这个就是导致两者构造存在差异的点。
通过上面代码跟踪也可以看出来,即使将自定义的 ConstraintValidator
使用 @Component
标记为 Spring 管理的 Bean,也是无效的,因为 Bean 初始化时没问题,会使用 Spring Bean 容器来管理,但是,在涉及到 JSR 303 校验时,Hibernate Validator 会去自己的缓存里查找 Bean 而非在 Spring 容器中,缓存里没有还是会调用 ConstraintValidatorFactoryImpl
来执行构造,就又回到了上面的问题。
其实这里已经有想到一个解决方案了,就是不使用 Bean 注入,之前之所以需要 Bean 是因为 MlbService
抽取出了重复代码,其中使用了 @ConfigurationProperties
的 YAML 配置属性,既然这里已经知道该方法是后置初始化的,所以将 @ConfigurationProperties
配置改为 static
的并且不再将 MlbService
注入为 Bean 而直接使用 static
方法去读取配置的 static
属性,理论上也是可行的,但是,这种解法只是在规避问题,并没有解决问题。所以还是决定继续向下看看,深究一下原因。
现在的问题点就在于,为什么在 JPA 实体上使用校验时会使用 Hibernate 默认的 ConstraintValidatorFactoryImpl
,而非在方法入参或者返回值上使用的 Spring 默认的 SpringConstraintValidatorFactory
。
对于这种差异,我第一反应是会不会是因为项目用的 JDK 代理导致的无法为实体类创建代理所导致的?或许可以试着强制采用 CGLIB 代理试一下,很遗憾,问题依旧。
其次,我想到的是会不会是应用上下文中的 ValidatorFacotry
Bean 与根应用上下文中的不一致,导致存在问题。
异常情况下校验上下文是 BeanValidationContext,而正常场景下是 ParameterValidationContext 或者 ReturnValidationContext,不一样的上下文导致获取到了不一样的 ConstraintValidator
。
其实这里怀疑是上下文覆盖导致的获取到不同的实现,尝试着在 Spring Context 中重新定义 ValidatorFactory
获取到的仍然是 BeanValidationContext
,这里其实比较疑惑,既然只有一个 Spring 上下文,为什么使用了不同的 ValidatorFacotry
呢。
如果没记错的话,SpringBoot 默认是只有一个上下文的。
继续跟踪堆栈,发现创建 ValidatorImpl
时就使用的 ConstraintValidatorFactoryImpl
。
跟代码跟到这里已经放弃继续跟踪堆栈的想法了,堆栈调用实在是太深了,大概思路是已经有了,在创建 ValidatorImpl
时就已经出了问题,转而直接跟踪报错的 save
代码,逐行调试。
累了,跟不下去了,这一个问题已经断断续续卡了两天,框架中大量使用反射导致代码跟踪比较麻烦。。。
到这里其实想的是用上面提到的改代码获取方式,不再用配置的方式了,这就避免了注入,但是想想还是不行,有其他的 ConstraintValidator
避免不了,就算一时避免了也难保证线上不会出现什么问题,加之确实很想搞明白这个问题,只能硬着头皮继续往下跟了。
先建个 bug 让 Hibernate 的人一起帮忙研究一下吧,传送门
唉,又遇到一 @Min 产生的 Bug。。。传送门