zoukankan      html  css  js  c++  java
  • JSR 303 自定义 Validator 无法初始化排查

    背景

    报错信息为 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 方法参数中。

    确实走到了有参构造器,接下来就是对比一下堆栈,看看有什么不同。

    先去干晚饭,晚饭还没吃,上来继续,明天周六哈哈哈哈哈哈哈哈哈哈哈哈哈哈。

    炸串加馍真好吃,我回来了。

    丢?这里的 ConstraintValidatorFactorySpringConstraintValidatorFactory,而有问题的则是 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。。。传送门

  • 相关阅读:
    开源项目cmdbuild-搭建
    开源项目cmdbuild-寄语
    四 上下文切换
    withRouter的作用和一个简单应用
    封装react组件:显示五星评价
    简单使用 Easy Mock-创建线上伪数据
    react中避免内存泄漏的方法
    react中constructor和super的使用
    使用swiper设计移动端轮播图(https://www.swiper.com.cn/)
    vuex模块化练习-购物车
  • 原文地址:https://www.cnblogs.com/seliote/p/15249557.html
Copyright © 2011-2022 走看看