Spring源码分析之循环依赖解决策略
Spring解决循环依赖的原理分析
在这之前需要明白java中所谓的引用传递和值传递的区别。
注意:JAVA中没有引用传递, 都是值传递,这儿只是为了理解,如果你有更好的说明方式可以下面留言
Spring的循环依赖的理论依据是当获得对象的引用时,对象的属性是可以延后设置的。但是构造器必须是在获取引用之前。
Spring创建Bean的流程
首先需要了解是Spring它创建Bean的流程,我把它的大致调用栈绘图如下:
对Bean的创建最为核心三个方法解释如下:
createBeanInstance
:例化,其实也就是调用对象的构造方法实例化对象populateBean
:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired
)initializeBean
:回到一些形如initMethod
、InitializingBean
等方法
从对单例Bean
的初始化可以看出,循环依赖主要发生在第二步(populateBean),也就是field属性注入的处理。
Spring容器的三级缓存
在Spring容器的整个声明周期中,单例Bean有且仅有一个对象。这很容易让人想到可以用缓存来加速访问。 从源码中也可以看出Spring大量运用了Cache的手段,在循环依赖问题的解决过程中甚至不惜使用了“三级缓存”,这也便是它设计的精妙之处~
三级缓存其实它更像是Spring容器工厂的内的术语,采用三级缓存模式来解决循环依赖问题,这三级缓存分别指:
注:
AbstractBeanFactory继承自DefaultSingletonBeanRegistry
singletonObjects
:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用earlySingletonObjects
:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖singletonFactories
:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖
获取单例Bean的源码如下:
-
先从
一级缓存singletonObjects
中去获取。(如果获取到就直接return) -
如果获取不到或者对象正在创建中(
isSingletonCurrentlyInCreation()
),那就再从二级缓存earlySingletonObjects
中获取。(如果获取到就直接return) -
如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过
getObject()
获取。就从三级缓存singletonFactory
.getObject()获取。(如果获取到了就从singletonFactories
中移除,并且放进earlySingletonObjects
。其实也就是从三级缓存移动(剪切)到了二级缓存)
**加入singletonFactories
三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决 **
getSingleton()
从缓存里获取单例对象步骤分析可知,Spring解决循环依赖的诀窍:就在于singletonFactories这个三级缓存。这个Cache里面都是ObjectFactory
,它是解决问题的关键。
经过ObjectFactory.getObject()后,此时放进了二级缓存earlySingletonObjects
内。这个时候对象已经实例化了,虽然还不完美
,但是对象的引用已经可以被其它引用了。
此处说一下二级缓存earlySingletonObjects
它里面的数据什么时候添加什么移除???
添加:向里面添加数据只有一个地方,就是上面说的getSingleton()
里从三级缓存里挪过来
移除:addSingleton、addSingletonFactory、removeSingleton
从语义中可以看出添加单例、添加单例工厂ObjectFactory
的时候都会删除二级缓存里面对应的缓存值,是互斥的
源码解析
Spring容器会将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,而对于创建完毕的Bean将从当前创建Bean池中清除掉。 这个“当前创建Bean池”指的是上面提到的singletonsCurrentlyInCreation
那个集合。
这里举例:例如是field
属性依赖注入,在populateBean
时它就会先去完成它所依赖注入的那个bean的实例化、初始化过程,最终返回到本流程继续处理,因此Spring这样处理是不存在任何问题的。
这里有个小细节:
这一句如果exposedObject == bean
表示最终返回的对象就是原始对象,说明在populateBean
和initializeBean
没对他代理过,那就啥话都不说了exposedObject = earlySingletonReference
,最终把二级缓存里的引用返回即可~
流程总结(重要)
此处以如上的A、B类的互相依赖注入为例,在这里表达出关键代码的走势:
1、入口处即是实例化、初始化A这个单例Bean。AbstractBeanFactory.doGetBean("a")
2、下面进入到最为复杂的AbstractAutowireCapableBeanFactory.createBean/doCreateBean()
环节,创建A的实例
由于关键代码部分的步骤不太好拆分,为了更具象表达,那么使用下面一副图示表示:
图来源田小波博客:
最后再来个纯文字版的总结。 依旧以上面A
、B
类使用属性field
注入循环依赖的例子为例,对整个流程做文字步骤总结如下:
- 使用
context.getBean(A.class)
,旨在获取容器内的单例A(若A不存在,就会走A这个Bean的创建流程),显然初次获取A是不存在的,因此走A的创建之路~ - 实例化A(注意此处仅仅是实例化),并将它放进
缓存
(此时A已经实例化完成,已经可以被引用了) - 初始化A:
@Autowired
依赖注入B(此时需要去容器内获取B) - 为了完成依赖注入B,会通过
getBean(B)
去容器内找B。但此时B在容器内不存在,就走向B的创建之路~ - 实例化B,并将其放入缓存。(此时B也能够被引用了)
- 初始化B,
@Autowired
依赖注入A(此时需要去容器内获取A) - 此处重要:初始化B时会调用
getBean(A)
去容器内找到A,上面我们已经说过了此时候因为A已经实例化完成了并且放进了缓存里,所以这个时候去看缓存里是已经存在A的引用了的,所以getBean(A)
能够正常返回 - B初始化成功(此时已经注入A成功了,已成功持有A的引用了),return(注意此处return相当于是返回最上面的
getBean(B)
这句代码,回到了初始化A的流程中~)。 - 因为B实例已经成功返回了,因此最终A也初始化成功
- 到此,B持有的已经是初始化完成的A,A持有的也是初始化完成的B,完美~
站的角度高一点,宏观上看Spring处理循环依赖的整个流程就是如此。希望这个宏观层面的总结能更加有助于小伙伴们对Spring解决循环依赖的原理的了解,同时也顺便能解释为何构造器循环依赖就不好使的原因。
循环依赖对AOP代理对象创建流程和结果的影响
我们都知道Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,而非原始对象。
本文结合循环依赖
,回头再看AOP代理对象的创建过程,和最终放进容器内的动作,非常有意思。
此Service
类使用到了事务,所以最终会生成一个JDK动态代理对象Proxy
。刚好它又存在自己引用自己
的循环依赖。看看这个Bean的创建概要描述如下:
上演示的是代理对象+自己存在循环依赖
的case:Spring用三级缓存很巧妙的进行解决了。 若是这种case:代理对象,但是自己并不存在循环依赖,过程稍微有点不一样儿了,如下描述:
分析可知,即使自己只需要代理,并不被循环引用,最终存在Spring容器里的仍旧是代理对象。(so此时别人直接@Autowired
进去的也是代理对象呀~~~)
终极case:如果我关闭Spring容器的循环依赖能力,也就是把allowCircularReferences
设值为false,那么会不会造成什么问题呢?
若关闭了循环依赖后,还存在上面A、B的循环依赖现象,启动便会报错如下:
注意此处异常类型也是
BeanCurrentlyInCreationException
异常,但是文案内容和上面强调的有所区别 它报错位置在:DefaultSingletonBeanRegistry.beforeSingletonCreation
这个位置~
报错浅析
:在实例化A后给其属性赋值时,会去实例化B。B实例化完成后会继续给B属性赋值,这时由于此时我们关闭了循环依赖
,所以不存在提前暴露
引用这么一说来给实用。因此B无法直接拿到A的引用地址,因此只能又去创建A的实例。而此时我们知道A其实已经正在创建中了,不能再创建了。so,就报错了~
这样它的大致运行如下:
可以看到即使把这个开关给关了,最终放进容器了的仍旧是代理对象,显然@Autowired
给属性赋值的也一定是代理对象。
最后,以AbstractAutoProxyCreator
为例看看自动代理创建器是怎么配合实现:循环依赖+创建代理
AbstractAutoProxyCreator
是抽象类,它的三大实现子类InfrastructureAdvisorAutoProxyCreator
、AspectJAwareAdvisorAutoProxyCreator
、AnnotationAwareAspectJAutoProxyCreator
小伙伴们应该会更加的熟悉些
该抽象类实现了创建代理的动作:
由上可知,自动代理创建器它保证了代理对象只会被创建一次,而且支持循环依赖的自动注入的依旧是代理对象。
上面分析了三种case,现给出结论如下:
不管是自己被循环依赖了还是没有,甚至是把Spring容器的循环依赖给关了,它对AOP代理的创建流程有影响,但对结果是无影响的。 也就是说Spring很好的对调用者屏蔽了这些实现细节,使得使用者使用起来完全的无感知~
总结
解决此类问题的关键是要对SpringIOC
和DI
的整个流程做到心中有数,要理解好本文章,建议有【相关阅读】里文章的大量知识的铺垫,同时呢本文又能进一步的帮助小伙伴理解到Spring Bean的实例化、初始化流程。
本文还是花了我一番心思的,个人觉得对Spring这部分的处理流程描述得还是比较详细的,希望我的总结能够给大家带来帮助。 另外为了避免循环依赖导致启动问题而又不会解决,有如下建议:
- 业务代码中尽量不要使用构造器注入,即使它有很多优点。
- 业务代码中为了简洁,尽量使用field注入而非setter方法注入
- 若你注入的同时,立马需要处理一些逻辑(一般见于框架设计中,业务代码中不太可能出现),可以使用setter方法注入辅助完成
__EOF__

本文链接:https://www.cnblogs.com/Courage129/p/14494680.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端