一、鸡生蛋悖论:到底谁先谁后?
事情是这样的,Spring 在创建Bean 时,遇到了这样的代码
@Service
public class BeanA {
@Autowired
private BeanB beanB;
}
@Service
public class BeanB {
@Autowired
private BeanA beanA;
}
这时 Spring 如果想要创建BeanA,需要依赖BeanB ,此时BeanB 还没有被创建,
于是先去创建BeanB,在创建BeanB时,又发现依赖BeanA,
这时BeanA 还等着BeanB 创建完,两个都在等待对方创建完成 ,陷入"先有鸡还是先有蛋"的死循环。
Spring 把这种情况叫做循环依赖
二、循环依赖的三种情况
我们发现Spring 出现循环依赖是实例化后在属性赋值时进行依赖注入发生的。之前在我们Spring 控制反转与依赖注入 提到过依赖注入有三种方式。
- 通过构造方法进行依赖注入
- Setter 注入
- 字段注入
其中Setter注入
和字段注入
本质上都是通过调用Bean 的 Setter 方法来设置属性的。Spring 在使用 Setter 方法注入依赖时,又会触发依赖Bean的实例化。我们在上篇说过Bean 的实例化又分为原型模式和单例模式。
总结下来,循环依赖出现有三种情况
- 通过构造方法进行依赖注入时产生的循环依赖问题。
- 通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题
- 通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题
首先是构造方式进行依赖注入时产生的循环依赖:此时对象创建时就要注入依赖,两者同时发生。此时两个Bean 只能一直互相等待。对于这种情况Spring无法解决。
然后是原型模式下通过setter方法 进行依赖注入,此时每次获取Bean时都是新建,也就是会产生一个新的Bean。再拿开头的例子来说,创建BeanA时,依赖BeanB,于是创建BeanB,在创建BeanB时,发现依赖BeanA,于是又开始创建BeanA2 .....,如此循环下去,很快就会将内存耗尽。出现OOM
只有这第三种情况,Bean的创建与注入是分开的,并且每个Bean 只创建一次。Spring 想到了解决的办法
三、Spring的破局之道
经过上面的分析,我们总结出了循环依赖的两个关键点:
- 两个Bean 都在创建,并且互相等待对方创建完成
- 创建完成前,无法获取到对方的Bean 对象
使用@Lazy延迟初始化
Spring 解决循环依赖的方法就是从这两个问题下手的,既然双方一块创建会导致互相等待,那么让其中一个晚点创建或者等使用时再创建,不就可以避免互相等待了,也就不会出现循环依赖了。
但是不创建的话就拿不到对应的对象,此时我们可以先生成一个代理对象,等到真正使用时再创建。
Spring 为了实现这种功能设计了一个注解@Lazy
@Lazy
注解可以让依赖的Bean延迟到第一次使用时才初始化,而非在当前Bean创建时立即初始化。例如:
@Service
public class AService {
@Lazy
@Autowired
private BService bService;
}
此时,Spring会为B创建一个代理对象注入A,当A第一次使用B时才会真正创建B实例,打破了初始化时的闭环。可以理解为去图书馆时,先在座位上放了一本书占座。
"破圈"三板斧:三级缓存
Spring 通过@Lazy
注解避免了一部分出现循环依赖的情况。但是还有一些就是要在一开始创建,这时候从第二个关键点上破局:创建完成前,无法获取到对方的Bean 对象。我们想办法在 Bean 创建完成前就拿到Bean对象。
就像你去坐火车,需要用身份证;但是你还未成年,无法办理身份证,这时你就可以办理一个临时身份证,功能没有身份证完整,但是已经可以去坐火车了
Spring 把这种方法叫做提前暴露,为了区分提前暴露的Bean、真正的Bean、以及之后被AOP增强的Bean 这三种不同的状态,Spring 将其放在了不同的地方中。也就是我们常听说过的三级缓存
Spring在DefaultSingletonBeanRegistry
类中维护了三个核心Map:
/** 一级缓存:存放完全初始化好的单例Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 二级缓存:存放早期暴露的Bean(已经实例化但未完全初始化)*/
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
/** 三级缓存:存放Bean工厂,用于创建早期引用 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
三级缓存的关系:
三级缓存 → (调用getObject) → 二级缓存 → (完成初始化) → 一级缓存
我们先看一下Spring 三级缓存的完整流程:
- 创建 BeanA 的实例
调用A的构造函数并实例化,此时获得一个A的原始对象(属性皆为空)。Spring 会将A的ObjectFactory
工厂对象存入第三级缓存(singletonFactories
)。当需要获取BeanA时,可以通过ObjectFactory工厂对象的getObject
方法获取BeanA - 填充
BeanA
的属性时触发BeanB
的创建
beanA进行属性赋值,发现依赖BeanB ,在三个缓存池中查找BeanB,未查询到;开始执行BeanB的创建流程:BeanB实例化,同样将BeanB
对应的ObjectFactory
工厂存入三级缓存 - BeanB属性赋值
BeanB 进行属性赋值,发现依赖BeanA依次在一、二、三级缓存中查找;在三级缓存中发现A的工厂对象,调用getObject
方法,获取BeanA,放入二级缓存中,并将A的工厂对象从三级缓存中移除。完成属性赋值 - BeanB 完成创建
beanB属性赋值完成后,执行初始化等后续流程。完成创建后,放入一级缓存中,并从其他缓存中移除 - 完成BanA的创建
BeanB创建完成后,复用二级缓存中的BeanA完成属性赋值,执行初始化等后续流程。BeanA完成创建后,放入一级缓存并从二级缓存中移除
具体流程如图所示:
为什么是三级缓存
在了解了Spring 的处理机制后,经常思考的同学就觉得,为什么要有三级缓存,我觉得两级缓存同样可以解决循环依赖的问题,并按照自己的思路画出了如下的流程图:
大家也可以先想一想,这样操作可行吗,是否有什么问题?
不知道大家有没有注意到,在两级缓存的流程图中,我标注了一下放入的是原始对象。
不要忘了Spring另外一个非常重要的特性就是AOP,而AOP的核心就是会生成代理对象,从而增强原对象的功能。如果只使用二级缓存的话,BeanB创建时拿到的就是原始对象,但BeanA实际创建完成时,得到的是BeanA的代理对象。这样的话就会出现两个BeanA对象,一个是原始对象,一个是代理对象。
三级缓存中正是为了解决这个问题而提出的。它缓存的不是对象,而是创建对象的工厂ObjectFactory
;当需要获取Bean时,需要调用ObjectFactory
的getObject()
来获取,此时Spring会进行判断,根据上下文来决定返回的是代理对象还是原始对象,如果需要代理对象,就提前进行创建。
简单说,三级缓存的本质是 “按需延迟生成正确引用” 。它既维持了 Bean 生命周期的完整性(正常流程在初始化后生成代理),又在循环依赖时特殊处理,避免逻辑矛盾。而二级缓存缺乏这种动态决策能力,因此无法替代三级缓存。
关键流程对比
步骤 | 两级缓存问题 | 三级缓存解决方案 |
---|---|---|
早期暴露 | 直接暴露原始对象 | 暴露ObjectFactory |
AOP处理 | 无法处理代理 | 在getEarlyBeanReference中处理代理 |
多次获取 | 每次获取都可能创建新代理 | 保证每次获取都是同一个代理 |
终极总结
解决问题最好的方法是避免问题的发生,同样避免循环依赖最好的方式就是不要让其出现循环依赖的情况。循环依赖往往是设计问题的信号,不应仅靠技术手段解决
- 三级缓存是Spring解决循环依赖的核心机制
- 必须是单例Bean + 非构造器注入才能生效
- 三级缓存的存在主要是为了解决AOP代理问题
- 构造器注入的循环依赖基本无解
看懂了吗?没有看懂的建议将这篇文章转发给你的小伙伴,让他给你再讲一遍!