前言

对这次的过程排查如果要形容的话,我觉得更像是悬疑剧,bean not found 这种错误,已经看腻了,甚至有时候都看不起这种错误,但是似乎这个想法被springboot听见了,所以这几天他就给我来了一记大耳刮子。。。

现象

版本(抛开版本就是耍流氓~)
jdk8
sprintboot 2.3.12

现象
首先我们的项目存在一个名叫common的项目,另一个叫fusionXXX的微服务依赖了这个common的项目,并且common中存在一个beandefination注册的过程(具体可以参考mybatis的mapper),因为要实现动态代理。
ok,当前idea同时打开了common项目和fusionXXX的项目,并且在一个window下,无需install到maven本地就可以直接运行,运行成功没有任何问题。

但是,现在我们将common项目install进了自己的maven仓库之后,用idea直接打开fusionXXX项目,这个时候开始运行fusionXXX项目,使用@Autowire来获取我们自己的bean,结果发现bean注入不进来,报错 bean not found

这是什么原因???

分析原因

没啥好说的,直接开debug模式找到bean not found的堆栈处,自己去logback.xml或者log4j.xml 文件中设置日志级别,设置之后springboot会将初始化的过程打印出来,报错之后会直接打印堆栈日志,这样直接就可以点进来看了,我这边直接运行二合一和单fusionXXX来比较哪里出现了不一致导致了bean没有注入,最终定位到这里两个项目出现了不一致的问题!

【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP
因为是Autowire注解,该注解先通过Type来获取bean,获取不到之后再使用name的方式获取bean,二合一在这里返回的String[] 存在一个bean的名称就是我给自己的bean起的名字,但是单fusionXXX就没有,所以我们继续看503行,因为cache刚开始是没有值的,与此同时我们也了解到springboot的bean查找存在一层缓存(不虚此行!)

【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP

这里我们看到如果type匹配到了才会添加这个beanName(这里的beanName是循环的临时变量,这里使用了循环的方式进行type的匹配,循环的就是spring实例化bean前的beanDefination列表,只有spring第一次实例化bean的情况才会这样循环,目前我们代码中有1222个定义,所以启动起来最耗费时间的应该就是这个过程了。),二合一的536行已经匹配到了,但是fusionXXX没有匹配到那么我么继续追踪下去。
【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP

通过层层的比对,最终发现不一致的地方在348行,单fusionXXX在这里直接进入了349行,我们看下最终返回false的条件为什么是false
【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP
终于到了重要的地方,这里我们发现单fusionXXX的两个Class不是一个对象,但是二合一的都是一个对象

【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP
【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP
上面两个图如果不注意正常来讲ClassName都是一样的,但是确实无法相互赋值,为什么呢?ClassLoader不一样。。。这种真的很难排查。。。那么现在分析一下为什么不一样

首先,如果两个类的ClassLoader不一样,那么jvm会认为两个类不是同一个类,即使两个类的reference一模一样,就如上面所看

ChatGPT说RestartClassLoader是springboot中的devtools的一个类加载器,其功能是为了热加载类文件设计的,当二合一运行时,在idea中,除了jar包以外的所有class都是可编辑的,所以他们都统一被RestartClassLoader加载,因此在二合一中的两个类的加载器都是RestartClassLoader
如果不是二合一,也就是单fusionXXX的时候,common模块以jar包形式进来,在beandefination加载的时候,jar包中的类提前手动(我自己写的)被java中的应用加载器加载(AppClassLoader ,java经典三层类加载器 :bootstrap -> ext->app),而在运行过程中,比对前spring确实找到了对应的类,但是却使用了devtools中的classloader进行了加载,从而导致了在匹配Type的时候无法匹配而最终找不到bean。

被restartclassloader加载的类可以在class文件修改的时候重新加载从而起到热修改的作用。

解决方法

将devtools注释掉即可,所有类都通过java的appclassloader加载就好了,但是不支持热修改了
【工作技术栈】【源码解读】一次springboot注入bean失败问题的排查过程-LMLPHP

思考感悟

这次的排查使用了不少的时间才排查到,一个很常见的错误本以为很快就能解决,没想到下面藏了一个冰山。。。
整个项目仅仅在本地运行不起来,打包成功后都能够运行,总的来说不会影响到业务,但是后面引入jar包的时候也应该对jar有一定的了解。

09-08 13:41