Skip to content

Spring 如何解决循环依赖? ​

  • 标签:后端、Spring

🏛 业务架构师 ​

Spring 用三级缓存解循环依赖:

  • 一级 singletonObjects:成品 bean;
  • 二级 earlySingletonObjects:提前曝光的半成品;
  • 三级 singletonFactories:工厂,需要时生产早期引用(含代理逻辑)。

流程:A 实例化后先把 ObjectFactory 放三级缓存 → 填充属性发现依赖 B → 创建 B → B 需要 A → 从三级缓存的工厂拿到 A 的早期引用(如果 A 需要 AOP,工厂在这里提前生成代理),放入二级缓存 → B 完成 → A 继续完成,升级到一级。

为什么二级不够:核心是AOP 代理的时机。若只有二级缓存且直接存"半成品对象",那么 A 的代理要么提前到实例化后立即生成(违背 Spring 设计原则——代理应在初始化后按 BeanPostProcessor 正常生成),要么 B 拿到的是没代理的原始 A(事务、异步注解全部失效——运行期事故)。三级缓存的工厂让代理"按需提前":正常无循环时走标准生命周期,有循环时才通过工厂把代理提前生成——用一次特例处理换全场景的语义正确。

我的架构态度分两层:

  1. 技术上,理解三级缓存是为了理解 Spring 在"优雅设计原则 vs 实用容错"间的精妙平衡——框架替开发者兜了设计缺陷,降低了使用摩擦;
  2. 架构上,循环依赖本身应该被消灭:构造器注入的环 Spring 直接报错,这个报错我视为框架在帮你做架构治理。业务上用 @Lazy 糊过去不如重画依赖图——抽出第三个概念(事件、共享模型、下沉服务)。我的规约:新代码零循环依赖(ArchUnit 门禁),存量循环依赖立整改项。

一句话:三级缓存是框架的善意,别把善意当许可证。

🏗 系统架构师 ​

Spring 用三级缓存解决单例 setter/字段注入的循环依赖,机制:

  • 一级 singletonObjects:成品 Bean;
  • 二级 earlySingletonObjects:提前曝光的半成品(已实例化未注入完成);
  • 三级 singletonFactories:ObjectFactory 工厂,能按需生成早期引用(普通 Bean 直接返回 this,AOP 代理的 Bean 从工厂拿到代理对象作为早期引用)。

流程:A 实例化后先把 ObjectFactory 放入三级缓存 → A 注入属性发现要 B → 创建 B → B 注入时要 A → 从三级缓存取 A 的工厂,执行工厂拿到早期引用(若需 AOP 则此处生成代理),放入二级缓存,B 拿到 A 的引用完成初始化 → 回到 A 继续注入 B,A 完成后从二级升入一级。

为什么能解:核心是"实例化与属性注入分两步"——构造完成后引用已存在(内存地址定了),提前把这个引用给对方,双方都 init 完成后引用自然完整。

边界:构造器注入无法解(实例化都没完成,无引用可暴露);prototype 不缓存无法解;@Async 这类循环依赖中代理冲突会直接报错(Boo 2.6 后默认全面禁止,倒逼架构上解耦)。

💻 全栈开发专家 ​

核心是三级缓存 + 提前暴露早期引用:

java
singletonFactories      // 三级:ObjectFactory 工厂(能生产早期代理)
earlySingletonObjects   // 二级:提前暴露的早期 Bean(可能是代理)
singletonObjects        // 一级:成品 Bean

流程(A 依赖 B,B 依赖 A):

text
1. createBeanInstance(A)         实例化 A(半成品)
2. 把"能生成 A 早期引用的工厂"放入三级缓存 → 继续填充属性发现需要 B
3. createBean(B) → B 填充属性需要 A
4. getSingleton(A):三级缓存拿到工厂 → factory.getObject()
   (若 A 需要 AOP,这里提前生成代理)→ 放入二级缓存,删三级
5. B 拿到 A 的早期引用,完成初始化,放入一级缓存
6. 回到 A 继续初始化,完成,放入一级缓存

为什么三级而不是二级(题眼):关键在 AOP 代理的生成时机。Spring 设计上代理应在初始化之后(BeanPostProcessor 后置阶段)生成,生命周期才完整。如果只有二级缓存,就必须在实例化后立刻为所有可能代理的 Bean 生成代理放进二级——没发生循环依赖的 Bean 也被破坏了正常的生命周期顺序。三级缓存存的是工厂:只有真的发生循环依赖、别人来取早期引用时,才调用工厂"按需"生成代理——没循环依赖的 Bean 走正常路径,有循环的才提前暴露。

边界:只救 setter/字段注入;构造器注入循环直接失败;@Async Bean 循环会报错(代理生成器冲突)。