Spring循环依赖:当两个Bean陷入鸡生蛋死循环时...

简介: Spring中循环依赖问题常见于Bean相互依赖时,尤其在单例模式下。文章深入解析了循环依赖的成因及Spring的三级缓存解决方案,帮助理解Bean生命周期与依赖管理。

一、鸡生蛋悖论:到底谁先谁后?

事情是这样的,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 创建完,两个都在等待对方创建完成 ,陷入"先有鸡还是先有蛋"的死循环。
e59b58d4df6e235e90a3a323d0138db7_MD5.jpeg

Spring 把这种情况叫做循环依赖

二、循环依赖的三种情况

我们发现Spring 出现循环依赖是实例化后在属性赋值时进行依赖注入发生的。之前在我们Spring 控制反转与依赖注入 提到过依赖注入有三种方式。

  • 通过构造方法进行依赖注入
  • Setter 注入
  • 字段注入

其中Setter注入字段注入本质上都是通过调用Bean 的 Setter 方法来设置属性的。Spring 在使用 Setter 方法注入依赖时,又会触发依赖Bean的实例化。我们在上篇说过Bean 的实例化又分为原型模式和单例模式。

总结下来,循环依赖出现有三种情况

  1. 通过构造方法进行依赖注入时产生的循环依赖问题。
  2. 通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题
  3. 通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题

首先是构造方式进行依赖注入时产生的循环依赖:此时对象创建时就要注入依赖,两者同时发生。此时两个Bean 只能一直互相等待。对于这种情况Spring无法解决。

然后是原型模式下通过setter方法 进行依赖注入,此时每次获取Bean时都是新建,也就是会产生一个新的Bean。再拿开头的例子来说,创建BeanA时,依赖BeanB,于是创建BeanB,在创建BeanB时,发现依赖BeanA,于是又开始创建BeanA2 .....,如此循环下去,很快就会将内存耗尽。出现OOM

只有这第三种情况,Bean的创建与注入是分开的,并且每个Bean 只创建一次。Spring 想到了解决的办法

三、Spring的破局之道

经过上面的分析,我们总结出了循环依赖的两个关键点:

  1. 两个Bean 都在创建,并且互相等待对方创建完成
  2. 创建完成前,无法获取到对方的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 三级缓存的完整流程:

  1. 创建 BeanA 的实例
    调用A的构造函数并实例化,此时获得一个A的原始对象(属性皆为空)。Spring 会将A的ObjectFactory工厂对象存入第三级缓存(singletonFactories)。当需要获取BeanA时,可以通过ObjectFactory工厂对象的getObject 方法获取BeanA
  2. 填充BeanA的属性时触发BeanB的创建
    beanA进行属性赋值,发现依赖BeanB ,在三个缓存池中查找BeanB,未查询到;开始执行BeanB的创建流程:BeanB实例化,同样将BeanB对应的ObjectFactory工厂存入三级缓存
  3. BeanB属性赋值
    BeanB 进行属性赋值,发现依赖BeanA依次在一、二、三级缓存中查找;在三级缓存中发现A的工厂对象,调用getObject 方法,获取BeanA,放入二级缓存中,并将A的工厂对象从三级缓存中移除。完成属性赋值
  4. BeanB 完成创建
    beanB属性赋值完成后,执行初始化等后续流程。完成创建后,放入一级缓存中,并从其他缓存中移除
  5. 完成BanA的创建
    BeanB创建完成后,复用二级缓存中的BeanA完成属性赋值,执行初始化等后续流程。BeanA完成创建后,放入一级缓存并从二级缓存中移除

具体流程如图所示:
f73202086c8312d01989d89bf03f5521_MD5.jpeg

为什么是三级缓存

在了解了Spring 的处理机制后,经常思考的同学就觉得,为什么要有三级缓存,我觉得两级缓存同样可以解决循环依赖的问题,并按照自己的思路画出了如下的流程图:
4195554431b84e54079fa4fced61de5d_MD5.jpeg

大家也可以先想一想,这样操作可行吗,是否有什么问题?

不知道大家有没有注意到,在两级缓存的流程图中,我标注了一下放入的是原始对象

不要忘了Spring另外一个非常重要的特性就是AOP,而AOP的核心就是会生成代理对象,从而增强原对象的功能。如果只使用二级缓存的话,BeanB创建时拿到的就是原始对象,但BeanA实际创建完成时,得到的是BeanA的代理对象。这样的话就会出现两个BeanA对象,一个是原始对象,一个是代理对象。

三级缓存中正是为了解决这个问题而提出的。它缓存的不是对象,而是创建对象的工厂ObjectFactory ;当需要获取Bean时,需要调用ObjectFactorygetObject()来获取,此时Spring会进行判断,根据上下文来决定返回的是代理对象还是原始对象,如果需要代理对象,就提前进行创建。

简单说,三级缓存的本质是 “按需延迟生成正确引用” 。它既维持了 Bean 生命周期的完整性(正常流程在初始化后生成代理),又在循环依赖时特殊处理,避免逻辑矛盾。而二级缓存缺乏这种动态决策能力,因此无法替代三级缓存。

关键流程对比

步骤 两级缓存问题 三级缓存解决方案
早期暴露 直接暴露原始对象 暴露ObjectFactory
AOP处理 无法处理代理 在getEarlyBeanReference中处理代理
多次获取 每次获取都可能创建新代理 保证每次获取都是同一个代理

终极总结

解决问题最好的方法是避免问题的发生,同样避免循环依赖最好的方式就是不要让其出现循环依赖的情况。循环依赖往往是设计问题的信号,不应仅靠技术手段解决

  1. 三级缓存是Spring解决循环依赖的核心机制
  2. 必须是单例Bean + 非构造器注入才能生效
  3. 三级缓存的存在主要是为了解决AOP代理问题
  4. 构造器注入的循环依赖基本无解

看懂了吗?没有看懂的建议将这篇文章转发给你的小伙伴,让他给你再讲一遍!

相关文章
|
17天前
|
XML Java 数据格式
Bean的生命周期:从Spring的子宫到坟墓
Spring 管理 Bean 的生命周期,从对象注册、实例化、属性注入、初始化、使用到销毁,全程可控。Bean 的创建基于配置或注解,Spring 在容器启动时扫描并生成 BeanDefinition,按需实例化并填充依赖。通过 Aware 回调、初始化方法、AOP 代理等机制,实现灵活扩展。了解 Bean 生命周期有助于更好地掌握 Spring 框架运行机制,提升开发效率与系统可维护性。
|
18天前
|
Java
Java的CAS机制深度解析
CAS(Compare-And-Swap)是并发编程中的原子操作,用于实现多线程环境下的无锁数据同步。它通过比较内存值与预期值,决定是否更新值,从而避免锁的使用。CAS广泛应用于Java的原子类和并发包中,如AtomicInteger和ConcurrentHashMap,提升了并发性能。尽管CAS具有高性能、无死锁等优点,但也存在ABA问题、循环开销大及仅支持单变量原子操作等缺点。合理使用CAS,结合实际场景选择同步机制,能有效提升程序性能。
|
20天前
|
前端开发
Promise.all()方法和Promise.race()方法有什么区别?
Promise.all()方法和Promise.race()方法有什么区别?
319 115
|
20天前
|
前端开发
Promise.allSettled()方法的语法是什么?
Promise.allSettled()方法的语法是什么?
226 117
|
20天前
|
XML JSON 数据库
大模型不听话?试试提示词微调
想象一下,你向大型语言模型抛出问题,满心期待精准回答,得到的却是答非所问,是不是让人抓狂?在复杂分类场景下,这种“大模型不听话”的情况更是常见。
123 9
|
11天前
|
机器学习/深度学习 数据采集 算法
【风电功率预测】【多变量输入单步预测】基于RVM-Adaboost的风电功率预测研究(Matlab代码实现)
【风电功率预测】【多变量输入单步预测】基于RVM-Adaboost的风电功率预测研究(Matlab代码实现)
198 129
|
20天前
|
前端开发 JavaScript
Promise.allSettled()方法和Promise.all()方法有什么区别?
Promise.allSettled()方法和Promise.all()方法有什么区别?
265 123
|
11天前
|
算法 Python
【轴承故障诊断】一种用于轴承故障诊断的稀疏贝叶斯学习(SBL),两种群稀疏学习算法来提取故障脉冲,第一种仅利用故障脉冲的群稀疏性,第二种则利用故障脉冲的额外周期性行为(Matlab代码实现)
【轴承故障诊断】一种用于轴承故障诊断的稀疏贝叶斯学习(SBL),两种群稀疏学习算法来提取故障脉冲,第一种仅利用故障脉冲的群稀疏性,第二种则利用故障脉冲的额外周期性行为(Matlab代码实现)
266 152
|
9天前
|
传感器 Python Perl
大气成分氨体积混合比 L3 (AIRSAC3MNH3 V3) 来自 NASA Aqua 上的 AIRS/AMSU,位于 GES DISC
本数据集由NASA Aqua卫星上的AIRS/AMSU传感器获取,提供2002年9月至2016年8月全球大气氨体积混合比。氨是氮循环关键成分,主要来源于农业活动,对气溶胶形成和地球辐射平衡有重要影响。
73 48
|
20天前
|
前端开发 JavaScript
HTML/CSS/JavaScript基础学习day01
阿铭学习HTML基础,包括VSCode快捷生成代码、常用标签如head、title、body、img、a、p等的使用,以及CSS选择器的优先级和基本样式设置,适合前端入门学习。