Spring如何解决解决循环依赖问题?
Spring如何解决解决循环依赖问题?
一、什么是循环依赖?
循环依赖是指两个或多个Bean之间相互持有引用,形成一个闭环。例如:
1 | |
当Spring尝试创建A时,发现需要注入B,于是去创建B;而创建B时又发现需要注入A,这就形成了死循环。如果不加处理,容器将陷入无限递归,最终导致栈溢出。
二、Spring解决循环依赖的前提
Spring解决循环依赖并非万能,它必须满足以下条件:
- 作用域为单例(singleton):Spring仅对单例模式的Bean提供循环依赖解决方案。原型(prototype)模式的Bean由于每次创建都是新实例,无法缓存早期暴露的对象,因此遇到循环依赖会直接抛出
BeanCurrentlyInCreationException。 - 通过setter注入(或字段注入):这种注入方式允许对象先通过无参构造器实例化,再设置属性,从而能够提前暴露对象。构造器注入无法解决循环依赖,因为构造器必须在实例化阶段就传入依赖,此时对象还未创建完成,无法提前暴露。
三、Spring解决循环依赖的核心:三级缓存
Spring在DefaultSingletonBeanRegistry中维护了三级缓存,正是这三张表化解了循环依赖的难题。
1 | |
- 一级缓存:最终存放已经完成属性填充、初始化等所有步骤的Bean,是给外部使用的完整Bean。
- 二级缓存:存放实例化完成但尚未属性填充和初始化的原始Bean(或者被AOP代理后的早期对象)。当Bean正在创建时,其他Bean可以从二级缓存中获取它的早期引用。
- 三级缓存:存放
ObjectFactory,这是一个函数式接口,调用其getObject()方法可以获取早期引用。它的作用是延迟代理对象的生成,避免在Bean实例化后立即进行AOP代理,从而破坏循环依赖。
四、源码流程剖析:以A和B互相依赖为例
让我们通过源码一步步跟踪A和B的创建过程。
1. 开始创建A
调用getBean("a"),进入doGetBean,由于A不在缓存中,开始创建流程。A被标记为正在创建(singletonsCurrentlyInCreation.add(beanName))。
2. 实例化A
执行createBeanInstance,通过反射调用A的无参构造器,实例化出一个原始的A对象(此时A的属性b还是null)。
1 | |
addSingletonFactory的代码如下:
1 | |
此时三级缓存中有了一个可以产生A早期引用的工厂。
3. 填充A的属性
继续执行populateBean,尝试为A注入属性b。发现b需要从容器获取,于是调用getBean("b")。
4. 创建B
getBean("b")同样进入创建流程。B实例化后,也将自己的ObjectFactory放入三级缓存。然后开始填充B的属性,发现需要注入A。
5. B获取A的引用
B调用getBean("a")。此时A已经在创建中(isSingletonCurrentlyInCreation("a")为true),于是进入getSingleton的缓存获取逻辑:
1 | |
关键步骤:
- 一级缓存
singletonObjects中还没有A(因为A还没创建完)。 - 二级缓存
earlySingletonObjects中也没有A。 - 三级缓存
singletonFactories中存在A的工厂,于是调用factory.getObject()。
getObject()执行的是我们之前放入的Lambda:() -> getEarlyBeanReference(beanName, mbd, bean)。getEarlyBeanReference会遍历所有SmartInstantiationAwareBeanPostProcessor,如果存在AOP代理的需求(例如A被@Transactional或@Async标记),则会在这里返回代理对象;否则返回原始对象。
1 | |
假设A不需要代理,则直接返回原始的A对象。B拿到了A的引用(虽然A此时还没完成属性注入,但已经是一个可用的对象了),然后将A的引用注入到自己的属性a中。
B拿到A后,继续完成自己的属性填充、初始化等步骤,最终成为一个完整的Bean。B创建完成后,会被放入一级缓存singletonObjects,并从三级/二级缓存中移除。
6. 回到A的创建
B创建完毕并返回给A的populateBean方法,A将自己的属性b设置为B的引用。随后A执行初始化方法等,也成为一个完整Bean,被放入一级缓存。
至此,循环依赖圆满解决。
五、为什么三级缓存不能简化成二级?
有的同学可能会问:既然二级缓存earlySingletonObjects可以直接存放早期对象,为什么还需要三级缓存singletonFactories?直接用二级缓存存储原始对象不行吗?
答案是:为了处理AOP代理。 如果Bean需要被AOP代理,那么我们希望在循环依赖中,其他Bean注入的应该是代理对象,而不是原始对象。但AOP代理的创建通常是在Bean初始化完成后,通过BeanPostProcessor(如AbstractAutoProxyCreator)的postProcessAfterInitialization生成的。如果等初始化完成再生成代理,那么早期暴露的对象就是原始对象,会导致后续注入的依赖不一致。
因此,Spring引入了三级缓存,通过ObjectFactory实现延迟代理:当其他Bean需要获取早期引用时,才通过工厂方法决定是否生成代理。如果不需要代理,工厂直接返回原始对象;如果需要代理,工厂返回代理对象。这样既保证了循环依赖的正确性,又保证了AOP语义的一致性。
AbstractAutoProxyCreator正是通过实现SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法,在早期暴露时就创建代理,并将代理放入二级缓存。这样后续获取到的都是同一个代理对象。
六、为什么构造器注入无法解决循环依赖?
因为构造器注入要求依赖对象在实例化时就必须传入,也就是说在createBeanInstance阶段就需要依赖对象。而此时对象尚未实例化完成,无法提前暴露自己的早期引用给其他Bean,因此无法打破循环。如果两个Bean通过构造器相互依赖,Spring将抛出BeanCurrentlyInCreationException。
七、其他注意事项
allowCircularReferences开关:Spring允许通过setAllowCircularReferences(false)关闭循环依赖支持,默认是true。- 原型模式下的循环依赖:由于原型Bean每次创建都是新对象,无法缓存,因此检测到循环依赖时会直接抛出异常。
- 不仅仅是字段注入:setter注入同样适用,因为setter注入也是先实例化再设值。
八、总结
Spring通过三级缓存巧妙地解决了单例模式下setter注入的循环依赖问题:
- 一级缓存:存放完整Bean。
- 二级缓存:存放早期暴露的对象(可能是原始对象或代理对象),作为过渡。
- 三级缓存:存放
ObjectFactory,延迟生成早期对象,以支持AOP代理。
当A依赖B,B依赖A时:
- A实例化后,将自身工厂放入三级缓存。
- A填充属性时发现需要B,转而去创建B。
- B实例化后,也将自身工厂放入三级缓存。
- B填充属性时发现需要A,从三级缓存拿到A的工厂,生成A的早期引用(原始或代理),注入给自己,B完成创建。
- A继续填充属性,得到B的完整引用,最终完成创建。
这一设计既保证了对象的正确创建,又兼顾了AOP等高级特性,是Spring IoC容器最精彩的内核之一。