Spring 循环依赖问题深度解析
Spring 循环依赖
什么是循环依赖
循环依赖指的是两个类中的属性相互依赖对方:例如 A 类中有 B 属性,B 类中有 A 属性,从而形成了一个依赖闭环。
循环依赖的三种情况
循环依赖问题在 Spring 中主要有三种情况:
第一种:通过构造方法进行依赖注入时产生的循环依赖问题
构造器注入类似原因:创建对象必须传完整依赖,提前暴露也无法注入 → 无法解决。
第二种:通过 setter 方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题
原型 Bean 循环依赖无法解决,因为容器不会缓存半初始化对象,每次 getBean 都创建新实例,无法注入引用。容器不缓存 Bean,单例 Bean 会放入 singletonObjects(一级缓存),原型 Bean 不会放入一级缓存,更不会维护二级缓存(earlySingletonObjects)或三级缓存(singletonFactories),所以每次获取原型 Bean,Spring 都是 “全新创建 + 注入 + 初始化”,容器不会提前暴露半初始化对象。
第三种:通过 setter 方法进行依赖注入且是在单例模式下产生的循环依赖问题
只有【第三种方式】的循环依赖问题被 Spring 解决了,其他两种方式在遇到循环依赖问题时,Spring 都会产生异常。Spring通过三级缓存和提前暴露未完全初始化的对象引用的机制来解决单例作用域Bean的 setter 注入方式的循环依赖问题。
三级缓存机制
三级缓存的定义
Spring在 DefaultSingletonBeanRegistry 类中维护了三个重要的缓存(Map),称为”三级缓存”:
一级缓存:singletonObjects
存放的是完全初始化好的、可用的Bean实例,getBean()方法最终返回的就是这里面的Bean。此时Bean已实例化、属性已填充、初始化方法已执行、AOP代理(如果需要)也已生成。
二级缓存:earlySingletonObjects
存放的是提前暴露的Bean的原始对象引用或早期代理对象引用,专门用来处理循环依赖。当一个Bean还在创建过程中(尚未完成属性填充和初始化),但它的引用需要被注入到另一个Bean时,就暂时放在这里。此时Bean已实例化(调用了构造函数),但属性尚未填充,初始化方法尚未执行,它可能是一个原始对象,也可能是一个为了解决 AOP 代理问题而提前生成的代理对象。
三级缓存:singletonFactories
存放的是Bean的 ObjectFactory 工厂对象。这是解决循环依赖和AOP代理问题工作的关键。当Bean被实例化后(刚调用构造函数),Spring会创建一个ObjectFactory并将其放入三级缓存。这个工厂的getObject()方法负责返回该Bean的早期引用(可能是原始对象,也可能是提前生成的代理对象),当检测到循环依赖需要注入一个尚未完全初始化的Bean时,就会调用这个工厂来获取早期引用。
三级缓存解决循环依赖的流程
三级缓存指的是 Spring 在创建 Bean 的过程中,通过三级缓存来缓存正在创建的 Bean,以及已经创建完成的 Bean 实例。
假设存在两个相互依赖的单例Bean:BeanA依赖BeanB,同时BeanB也依赖BeanA。当Spring容器启动时,它会按照以下流程处理:
第一步:创建BeanA的实例并提前暴露工厂
Spring首先调用 BeanA 的构造函数进行实例化,此时得到一个原始对象(尚未填充属性)。紧接着,Spring会将一个特殊的 ObjectFactory 工厂对象存入第三级缓存(singletonFactories)。这个工厂的使命是:当其他Bean需要引用 BeanA 时,它能动态返回当前这个半成品的 BeanA (可能是原始对象,也可能是为应对AOP而提前生成的代理对象)。此时 BeanA 的状态是”已实例化但未初始化”,像一座刚搭好钢筋骨架的大楼。
第二步:填充 BeanA 的属性时触发 BeanB 的创建
Spring开始为 BeanA 注入属性,发现它依赖 BeanB。于是容器转向创建 BeanB,同样先调用其构造函数实例化,并将 BeanB 对应的 ObjectFactory 工厂存入三级缓存。至此,三级缓存中同时存在 BeanA 和 BeanB 的工厂,它们都代表未完成初始化的半成品。
第三步:BeanB 属性注入时发现循环依赖
当Spring试图填充 BeanB 的属性时,检测到它需要注入 BeanA。此时容器启动依赖查找:在一级缓存(存放完整Bean)中未找到 BeanA;在二级缓存(存放已暴露的早期引用)中同样未命中;最终在三级缓存中定位到 BeanA 的工厂。
Spring立即调用该工厂的 getObject() 方法。这个方法会执行关键决策:若 BeanA 需要AOP代理,则动态生成代理对象(即使 BeanA 还未初始化);若无需代理,则直接返回原始对象。得到的这个早期引用(可能是代理)被放入二级缓存(earlySingletonObjects),同时从三级缓存清理工厂条目。最后,Spring将这个早期引用注入到 BeanB 的属性中。至此,BeanB 成功持有 BeanA 的引用——尽管 BeanA 此时仍是个半成品。
第四步:完成 BeanB 的生命周期
BeanB 获得所有依赖后,Spring执行其初始化方法(如 @PostConstruct),将其转化为完整可用的 Bean。随后,BeanB 被提升至一级缓存(singletonObjects),二级和三级缓存中关于 BeanB 的临时条目均被清除。此时 BeanB 已准备就绪,可被其他对象使用。
第五步:回溯完成 BeanA 的构建
随着 BeanB 创建完毕,流程回溯到最初中断的 BeanA 属性注入环节。Spring 将已完备的 BeanB 实例注入 BeanA,接着执行 BeanA 的初始化方法。这里有个精妙细节:若之前为 BeanA 生成过早期代理,Spring 会直接复用二级缓存中的代理对象作为最终Bean,而非重复创建。最终,完全初始化的 BeanA(可能是原始对象或代理)入驻一级缓存,其早期引用从二级缓存移除。至此循环闭环完成,两个Bean皆可用。
为什么需要三级缓存
通过三级缓存的机制,Spring 能够在处理循环依赖时,确保及时暴露正在创建的 Bean 对象,并能够正确地注入已经初始化的 Bean 实例,从而解决循环依赖问题,保证应用程序的正常运行。
Spring 使用三级缓存解决单例 Bean 循环依赖,是为了在依赖注入阶段生成早期引用。三级缓存中的 ObjectFactory 可以根据 Bean 是否需要 AOP 代理动态生成代理对象,然后放入二级缓存 earlySingletonObjects,保证注入的依赖既是代理对象,又符合单例原则。如果只有二级缓存,B 注入 A 时拿到的就是原始对象,破坏单例和代理一致性。