循环依赖——为什么是三级缓存?
针对循环依赖,应该我们都不陌生,就是beanA依赖于beanB,注入时候发现beanB里面又依赖beanA,导致双方都不能进行DI。那我们就来认真考虑并拆解一下Spring为什么使用了三级缓存来解决这个问题。
首先我们自己先想一想这个问题应该要怎么解决。要破除循环,肯定某一方要先给出一个可以先注入的实例吧,这样才能让另一方去正常走完bean的生命周期,然后回头先给出的那一方就可以在单例池里面找到需要的成熟bean,也可以进行完整的bean生命周期了。
我们可能会担心给出实例的一方还并未成熟,就注入到另一方去。没有关系,因为是引用传递,只要后面给出实例的一方也走完了生命周期,自然另一方里面的也就成熟了。
那我们要在什么时候去给出一个暂时可以用来注入的实例呢?其实实例化完了之后就可以了,我们完全可以用一个Map去存储这个类型的一个暂时不成熟的bean,然后在DI另一方,碰到循环依赖的时候就把这个Map里面的暂不成熟bean先注入,就打破了循环。
这样就万无一失了吗?这个方法只是打破了循环而已,还存在不少问题。其中最大的问题就是:如果这个暂不成熟的bean后续是要通过动态代理的,那另一方注入的岂不是变为原生实例,而不是动态代理的实例?
为了解决这个问题,我们似乎要把AOP动态代理的创建提前一下,在用Map存储之前就创建动态代理,然后把代理对象放入Map,这样另一方拿到的就是代理对象了,也就是提前AOP。似乎这样就可以了,但是在实例化的时候,Spring还不能判断是否出现了循环依赖,得在DI的时候才会知道,所以AOP的提前不能提前到实例化之后马上进行。
那好,那我们就先不用这个Map了,也就是先暂时不打破这个循环,我们得判断出是否出现了循环依赖。我们用一个CreationSet在bean生命周期的最开始保存有哪些bean还在生命周期中,在放入单例池后把他去掉。这样在DI注入的时候,另一方一查CreationSet,就知道有了循环依赖。这个时候,我们再创建AOP动态代理,把动态代理对象注入给另一方,似乎就可以了。
那现在就有两个问题:
1、我的AOP代理都创建好了,什么时候放入单例池里面呢?要知道,现在里面的被代理对象还是不成熟的,直接放进去会有很大的问题,那怎么办?
2、我在创建AOP代理对象的时候,要怎么拿到原生对象?我们知道代理是要有原生的target的,用什么方法拿到呢?
我们先再来考虑一个问题。如果A和B循环依赖;A和C也同时循环依赖,那么,在A和B之间注入的时候,A的动态代理被创建了一次(假设是A先执行DI);但是,在A和C之间注入的时候,A的动态代理又被创建了一次,这就造成了代理对象不一致的问题。
为了解决这个问题,我们考虑又用一个earlyMap,存储生成的代理对象,这样就可以保证唯一性。那我们有了这个Map,再回头看一下的那个放入单例池问题,我们就有思路了:在A原生实例生命周期执行完成,马上要加入单例池的时候,我把这个earlyMap里面的代理对象拿出来放入单例池,不就可以了吗?
现在就剩下怎么拿到原生对象的问题了。我们发现,现在的做法还并没有打破循环,也就是原生对象还并没有提供方法让AOP创建的时候拿到。
那就再来一个factoriesMap!我直接往里面丢实例化完了的对象不就好了吗?
但是值得注意的是,Spring没有之间放一个实例化对象作为value。他是放了一个函数式接口也就是lambda表达式进去;当DI循环依赖需要动态代理的时候,从这个factoriesMap一取,就是这个lambda表达式。这个lambda表达式会决定是否执行动态代理的创建。
现在,一个完美的循环依赖解决方案就出来了!现在我们分析一下这个解决方法对应于Spring里面的哪些内容:
正在创建集合singletonsCurrentlyInCreation:保存有哪些bean还在生命周期中,对应讲到的CreationSet。
三级缓存singletonFactories:实例化后,放入beanName作为键,lambda作为值的Map,对应讲到的factoriesMap。
二级缓存earlySingletonObjects:存储生成的代理对象,避免在多个循环依赖中创建多个不同的代理对象,对应讲到的earlyMap。
一级缓存singletonObjects:单例池。
注意
Spring本身并不关心循环不循环的问题。他只是想能否通过暴露早期对象,解决同时创建两个互相需要的bean的问题。
完整流程:
beanA进入生命周期,标记为正在创建,进入singletonsCurrentlyInCreation集合。A被实例化,A的ObjectFactory放入三级缓存。进行DI,发现了依赖beanB。会先进入一级缓存查找是否有成熟beanB。没有的话,开始进行B的创建(getBean()),由于singletonsCurrentlyInCreation集合中没有B,不提前暴露早期对象。
beanB进入生命周期,和A一样的流程。在DI时,发现依赖A。Spring通过查询singletonsCurrentlyInCreation发现A也在创建,决定提前暴露早期对象。
查询一级缓存单例池,发现没有,加锁读二三级缓存,如果支持循环依赖,就会在其中一个缓存读到A。取出,就是A的代理对象(如果需要动态代理增强)因为二级缓存之中就是经过三级缓存决定是否创建代理之后产生的对象,三级缓存就是lambda,决定是否创建代理对象。然后A在同步块中被同步放入二级缓存,删除三级缓存内容,避免错误。
B打破了循环,有了出口。这样A也就可以走完他的生命周期,循环依赖解决。
那有的问题就来了:在循环依赖中,我们做到了代理对象的生成,那后面的初始化之后,Spring是怎么知道不要再创建一次动态代理的呢?
其实在循环依赖提前AOP的时候,Spring把他放入了一个earlyProxyReference的一个Map中。这样就知道哪些bean是提前AOP的了;在后面的初始化之后,一调用这个Map的remove方法,返回的就是这个bean对象,一作比对就知道已经提前AOP过了,就不再进行动态代理的创建。
注意
即使没有循环依赖,这个三级缓存依然会在正常流程中使用到,比如每一次实例化都会加入三级缓存。
不过二级缓存就基本不会使用了,因为二级缓存暴露了早期的对象,平常正常生命周期是不需要暴露的。
我们可以想想:为什么是三级缓存,两级缓存不行吗?
如果bean无需AOP,自然也就不用这么多级缓存了;直接把实例化后的对象丢到二级缓存里面就行了,这样另一方拿到的也是正确的原生实例对象。
但是问题就出在某些bean需要AOP代理创建!如果只有二级缓存,要么你拿到的就是原生的实例,而不是代理对象;要么就是直接创建代理对象,但是这就违反了生命周期原则。因为我们知道直接创建代理对象,会导致被代理的target没有进行依赖注入而不是成熟的bean。所以,只有在真正需要早期引用时才决定是否创建代理对象,否则就应该遵守正常的生命周期流程。
到这里,循环依赖的本质和解决核心我们都了解了。
那我们再来考虑一个东西:@Async注解。
假设我有这么一段代码:
1 |
|
并且这个test方法写在一个A类中,A类本身无需AOP创建动态代理,而且A类和B类构成了循环依赖。那我们现在考虑一下,Spring是否会正常解决循环依赖问题呢?
在得出答案之前,我们需要知道一个前提:@Async不是基于AOP创建的动态代理。如@Transactional注解就是基于AOP创建的动态代理,但@Async不是。他是另一个bean的后置处理器进行的动态代理创建。
那有了这个前提,答案也就很清晰了:Spring会直接报错,因为用循环依赖解决方案出现了问题。具体出现在哪里呢?
刚才说到,@Async不是基于AOP创建的动态代理。那回头看看我们的循环依赖解决流程中,如果正常走下来,二级缓存应该放的是A的原生实例,并且已经赋值给了B。但是,当走到初始化后的逻辑,由于@Async的动态代理还没有创建,就需要创建一个不是基于AOP的动态代理。创建完后,Spring把这个创建出的动态代理对象和二级缓存里面的原生对象一比较,发现不一样,直接就报错了,因为已经出现了错误。
这个时候只能请另一个老祖出山了:@Lazy。@Lazy又是一个不是基于AOP创建的动态代理,实际上,他也不是跟@Async一样bean后置处理器创建的动态代理。那这个@Lazy是怎么破除这个问题的呢?
实际上,@Lazy不止能破除这个问题,他还能本质上解决循环依赖问题。我们再来看一个场景:
假设某类A的构造函数注入的时候依赖的必须bean就已经和某类B形成了循环依赖,那岂不是连实例化都实例化不了?更何谈起三级缓存的早期对象存储,这个情况连Spring都无法解决。
但是@Lazy老祖就可以。举个例子:
1 |
|
他的原理是:先把一个假的对象,实际上是一个他创建出的代理对象先塞到A里面的b中,这样A就有了出口,就可以正常创建bean。那什么时候把假的换成真的呢?当真正要使用到这个beanB的时候。这个时候去创建B的bean,也不会出现循环依赖,因为单例池里面已经有了A的bean。正如其名:@Lazy,也就是懒加载。
好了,到这里,循环依赖的本质就被我们摸清了。
我们还可以想想一个新问题:多例bean循环依赖问题能解决吗?
当然不行啦,因为Spring不会去保存多例bean的早期对象。而且,如果两方允许创建,会直接导致死锁,因为多例Bean只有在被请求时才创建。
那怎么办?多例bean就失去了循环依赖的解决方案吗?
@Lazy老祖:又要我出山了吗?
- 标题: 循环依赖——为什么是三级缓存?
- 作者: ylnxwlp
- 创建于 : 2025-10-04 01:17:26
- 更新于 : 2025-10-04 15:20:04
- 链接: https://www.swissroll.today/2025/10/04/循环依赖——为什么是三级缓存?/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。