侧边栏壁纸
博主头像
博主等级

  • 累计撰写 19 篇文章
  • 累计创建 34 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

SpringBoot项目中使用@Async导致循环依赖问题

前尘一梦
2022-05-29 / 0 评论 / 6 点赞 / 82 阅读 / 8633 字

前言

在一个日常搬砖的早晨,习惯性拉了下代码,准备启动项目调试个接口,却出现启动失败报错的情况,看控制台日志描述的是一个熟悉又不常见的循环依赖错误,如图

b5419ef8efbc4578897fb3ba28f7d942~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

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中,报错位置如下

ba929b07df914f059e0c1ce921249501~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

往上找,到createBean方法里,打上条件断点

70e5ff0fee6e4635bcbc3ac7cba1f3be~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

以debug模式启动项目,进入断点

60c61bf6e3a546f4870c41e4e4b689a3~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

往下执行,跳过前面类解析环节,到doCreateBean方法

3ee645c357d2424083322b3302969d43~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

进入doCreateBean方法,往下执行,到这个地方

6155e1eae8e34b7c8cc2807a6798a5ec~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

可以看到已经将a的早期引用加入三级缓存当中,当前bean a为普通对象,为了便于观察,将exposedObject加入watch中,接下来进入bean填充,populateBean方法中, 通过内部的后置处理器填充属性依赖,由于使用的是@Resource注解,此处是由CommonAnnotationBeanPostProcessor处理a中注入的b属性依赖

b3e93b3332d04413bd392422eeb95dc5~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

开始进行bean 【b】的创建

1fa68c4c068446ee84bdd9f359e0bfab~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

流程和a一样,b中的c也是通过CommonAnnotationBeanPostProcessor处理b中注入的c属性依赖
fb27eb3e1b2546558220f28b431387bf~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

随后执行c的创建流程,因为a已经存在于三级缓存当中,所以c依赖的是a的早期依赖,因为没有其他依赖,c执行完整个创建周期,创建完成之后注入到b中继续b的创建,b执行完创建后回到a中,进行后面的流程

c23dbf2d46f8403583558ee044758606~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

直接跳到下一步

be087dcf9586482dbeae862059135fa2~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

很明显能看出a暴露的对象从原始对象变成了代理对象,再往后执行

d76e6216ffc549478ce9dd6a3ccde0b4~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

这里会判断最终暴露的对象和早期对象是否是同一个,如果相等,则流程执行完毕,此处明显不相等,所以进入后面的else语句。后面的代码中 hasDependentBean方法是判断是否被其他对象依赖,会有个map来记录a的所有引用对象,allowRawInjectionDespiteWrapping大概意思是是否允许引用原始对象,如果条件满足,进入后面的代码, removeSingletonIfCreatedForTypeCheckOnly方法作用是根据传入的bean名判断如果已经创建完毕,则清其相关的所有缓存并返回true

5fa35c9cb0d54ee1af33bc510bf9a49d~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

因为bean【c】的创建已经完成,所以此处返回为true,并把c加入actualDependentBeans中,最后actualDependentBeans集合肯定是不为空的,所以报错了。走完整个流程我们发现,最终报错的原因是bean 【c】依赖的对象【a】并不是a的最终版本,debug过程中也知道bean【a】的对象版本是在initializeBean中被改变,于是再次debug进入initializeBean方法,initializeBean中主要执行相关后置处理器在bean初始化前后做一些事情,包括applyBeanPostProcessorsBeforeInitialization和applyBeanPostProcessorsAfterInitialization两块,在前面的applyBeanPostProcessorsBeforeInitialization中返回的bean没有变化

8ece6b76f0ea44089321c490f8ab6e9e~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

而在后面的applyBeanPostProcessorsAfterInitialization方法中,当执行到AsyncAnnotationBeanPostProcessor处理器时,生成了代理对象,而实际生成代理的代码是在其父类AbstractAdvisingBeanPostProcessor中, 此时大概猜到是因为@Async异步注解引起,项目里正好开启了@EnableAsync(@EnableAsync开启时它会向容器内注入AsyncAnnotationBeanPostProcessor), 且a类中刚好有一个方法带有此注解。

da9485df999e4b67a4bdf8a1beceef00~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg
首先会判断如果已创建过代理(被事务代理等),isFrozen为false且切入点适配则新增一个切面即可,此处的切面AsyncAnnotationAdvisor完成,因为a类中存在标注了@Async的方法,所以是匹配的。当前a类还没创建过代理,走到后续创建代理流程

c7c7b3a6f60f4ca4bc796bdf0ce04ec7~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.jpg

一个全新的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并不是最终版本,导致报错。

6

评论区