前言
在一个日常搬砖的早晨,习惯性拉了下代码,准备启动项目调试个接口,却出现启动失败报错的情况,看控制台日志描述的是一个熟悉又不常见的循环依赖错误,如图
Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [c] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example
此篇记录下问题的分析和解决过程
问题分析
从报错来看,大致意思是bean【a】和bean【c】存在循环依赖,bean【a】最终被包装,其他依赖的c并不是最后c的最后版本。 看了下项目里,类A,B,C的依赖关系为A -> B -> C -> A,确实存在间接循环依赖,但三个类里 都使用的属性注入方式,也就是set注入,在spring三级缓存的加持下应该会自动解决才对。 于是按照提示点击报错位置进入AbstractAutowireCapableBeanFactory中,报错位置如下
往上找,到createBean方法里,打上条件断点
以debug模式启动项目,进入断点
往下执行,跳过前面类解析环节,到doCreateBean方法
进入doCreateBean方法,往下执行,到这个地方
可以看到已经将a的早期引用加入三级缓存当中,当前bean a为普通对象,为了便于观察,将exposedObject加入watch中,接下来进入bean填充,populateBean方法中, 通过内部的后置处理器填充属性依赖,由于使用的是@Resource注解,此处是由CommonAnnotationBeanPostProcessor处理a中注入的b属性依赖
开始进行bean 【b】的创建
流程和a一样,b中的c也是通过CommonAnnotationBeanPostProcessor处理b中注入的c属性依赖
随后执行c的创建流程,因为a已经存在于三级缓存当中,所以c依赖的是a的早期依赖,因为没有其他依赖,c执行完整个创建周期,创建完成之后注入到b中继续b的创建,b执行完创建后回到a中,进行后面的流程
直接跳到下一步
很明显能看出a暴露的对象从原始对象变成了代理对象,再往后执行
这里会判断最终暴露的对象和早期对象是否是同一个,如果相等,则流程执行完毕,此处明显不相等,所以进入后面的else语句。后面的代码中 hasDependentBean方法是判断是否被其他对象依赖,会有个map来记录a的所有引用对象,allowRawInjectionDespiteWrapping大概意思是是否允许引用原始对象,如果条件满足,进入后面的代码, removeSingletonIfCreatedForTypeCheckOnly方法作用是根据传入的bean名判断如果已经创建完毕,则清其相关的所有缓存并返回true
因为bean【c】的创建已经完成,所以此处返回为true,并把c加入actualDependentBeans中,最后actualDependentBeans集合肯定是不为空的,所以报错了。走完整个流程我们发现,最终报错的原因是bean 【c】依赖的对象【a】并不是a的最终版本,debug过程中也知道bean【a】的对象版本是在initializeBean中被改变,于是再次debug进入initializeBean方法,initializeBean中主要执行相关后置处理器在bean初始化前后做一些事情,包括applyBeanPostProcessorsBeforeInitialization和applyBeanPostProcessorsAfterInitialization两块,在前面的applyBeanPostProcessorsBeforeInitialization中返回的bean没有变化
而在后面的applyBeanPostProcessorsAfterInitialization方法中,当执行到AsyncAnnotationBeanPostProcessor处理器时,生成了代理对象,而实际生成代理的代码是在其父类AbstractAdvisingBeanPostProcessor中, 此时大概猜到是因为@Async异步注解引起,项目里正好开启了@EnableAsync(@EnableAsync开启时它会向容器内注入AsyncAnnotationBeanPostProcessor), 且a类中刚好有一个方法带有此注解。
首先会判断如果已创建过代理(被事务代理等),isFrozen为false且切入点适配则新增一个切面即可,此处的切面AsyncAnnotationAdvisor完成,因为a类中存在标注了@Async的方法,所以是匹配的。当前a类还没创建过代理,走到后续创建代理流程
一个全新的a对象代理创建完成
解决方案
⓵ 把带有@Async注解的bean剔除循环依赖
⓶ 把allowRawInjectionDespiteWrapping设置为true
⓷ 使用@Lazy或@ComponentScan(lazyInit = true)
总结
此处使用的SpringBoot版本为2.2.5, 其他版本待验证
如果bean存在以下关系并且A中含有标注@Async注解的方法:
A,B两bean直接循环依赖, 如A->B->A
A,B,C间接循环依赖, 如A->B->C->A
因为项目启动记载文件的顺序不固定,在某些情况下
先createBean(A), 此时把A的早期引用放入三级缓存
populateBean(A), 开始创建A的依赖B
createBean(B), populateBean(B), 开始创建B的依赖C
createBean(C), populateBean(C), 此时C将拿到的依赖A将是A存在三级缓存中的早期引用
完成C后续的创建,回到B的过程继续B的创建, B创建完成后回到A的过程
执行initializeBean(A), 在执行到applyBeanPostProcessorsAfterInitialization()阶段,循环到后置处理器AsyncAnnotationBeanPostProcessor时,由其父类对A进行类增强并生成新的代理对象暴露给spring容器
到最后检测阶段发现C依赖的A并不是最终版本,导致报错。
评论区