Spring循环依赖与三级缓存详解

Spring 循环依赖与三级缓存详解

目标:把 "Spring 为啥能解决一部分循环依赖" 讲清楚;把 三级缓存(singletonObjects / earlySingletonObjects / singletonFactories) 的作用讲透;以及 什么时候一定解决不了


1. 什么是循环依赖

循环依赖(Circular Dependency)本质是:Bean A 的创建过程依赖 Bean B,而 Bean B 的创建过程又依赖 Bean A(直接或间接)

常见形式:

  • 直接循环:A → B → A
  • 间接循环:A → B → C → A

依赖发生的位置也很关键:

  • 构造器注入(constructor injection)
  • Setter/字段注入(property/field injection)

Spring 能解决的,主要是 单例 + Setter/字段注入 这类。


2. Spring 解决循环依赖的核心思路:先"露个面",后"补齐"

Spring 创建 Bean 的过程可以粗略拆成两段:

  1. 实例化(Instantiation)new 出对象(此时属性还没注入)
  2. 属性填充(Populate Properties):注入依赖(A 需要 B)
  3. 初始化(Initialize):执行 Aware、BPP、init-method、@PostConstruct 等
  4. 注册成单例(Add to Singleton Cache)

循环依赖卡住的点一般在第 2 步:

A 还在创建中,A 注入 B → 创建 B → B 又要注入 A(但 A 还没创建完)。

解决办法

当 A "刚实例化完但还没初始化完" 时,Spring 先把 A 的一个"早期引用"暴露出去,让别人(比如 B)先拿到 A 的引用把依赖链打通,后面再把 A 的初始化补齐。

这就是三级缓存要做的事。


3. 三级缓存分别是什么(DefaultSingletonBeanRegistry)

Spring 单例相关的缓存(简化理解):

3.1 一级缓存:singletonObjects(成品池)

  • Map<String, Object> singletonObjects
  • 存的是 完整创建完成 的单例 bean(成品)。
  • 你平时从容器 getBean 拿到的,大多来自这里。

3.2 二级缓存:earlySingletonObjects(半成品引用池)

  • Map<String, Object> earlySingletonObjects
  • 存的是 早期暴露的 bean 引用(半成品引用)。
  • 只有在发生循环依赖时,才会被用到。

3.3 三级缓存:singletonFactories(工厂池)

  • Map<String, ObjectFactory<?>> singletonFactories
  • 存的是一个 ObjectFactory ,可以在"确实需要"时,创建 bean 的 早期引用
  • 关键点:它不是直接存 bean,而是存 "如何生成早期引用 的方法"。

直觉类比:

  • 一级:成品仓库
  • 二级:临时借出去的半成品
  • 三级:半成品的"出库申请单/生成器"------只有别人真来借,才生成并借出

4. 为什么一定要三级?二级不够吗?

很多人卡在这里:如果只要"早期引用",二级缓存不就够了?

三级缓存的存在,核心是为了支持 AOP 代理等"需要在早期就决定返回什么对象" 的场景:

  • 早期引用有可能不是原始对象,而是 代理对象
  • 是否需要代理,要看 BeanPostProcessor(例如 AOP 的 AutoProxyCreator)逻辑
  • Spring 不希望"所有 bean 都提前生成早期引用",只有在循环依赖真正发生时才生成

因此使用三级缓存:

  1. A 刚实例化时,先往三级缓存塞一个 factory
  2. 只有当 B 真的来要 A 的引用(且 A 正在创建中)时,才调用 factory
  3. factory 内部会走 getEarlyBeanReference,给你一个"正确的早期引用"(可能是代理)

这能避免:

  • 过早创建代理导致不必要的开销
  • 代理创建时机不对,出现 双重代理 / 代理丢失 / 代理对象不一致 等坑

5. 经典流程拆解:A → B → A(Setter 注入,单例)

假设:

java 复制代码
@Component
class A { @Autowired B b; }

@Component
class B { @Autowired A a; }

5.1 关键流程(简化版)

  1. 创建 A
    • 实例化 A:new A()
    • 把 A 的 ObjectFactory 放入三级缓存(此时 A 还没注入 B)
  2. A 需要注入 B → 创建 B
    • 实例化 B:new B()
    • 把 B 的 ObjectFactory 放入三级缓存
  3. B 需要注入 A → 再次获取 A
    • 发现 A "正在创建中"
    • 一级缓存没有 A(还没创建完)
    • 二级缓存也没有(还没暴露)
    • 去三级缓存拿到 A 的 factory,生成 A 的早期引用
    • 把这个早期引用放入二级缓存,并从三级缓存移除
    • 返回 A 的早期引用给 B 注入
  4. B 完成属性填充与初始化 → 放入一级缓存
  5. 回到 A,拿到 B,完成属性填充与初始化 → 放入一级缓存

5.2 三张缓存的状态变化(直观)

  • A 刚实例化:A 在 三级(factory)
  • B 请求 A:A 从 三级(factory) → 生成早期引用 → 放到 二级
  • A 初始化完成:A 从 二级(早期) → 升级到 一级(成品)

6. 关键源码位置(看懂这几个就够了)

不同版本方法细节可能有变化,但核心结构基本稳定。

6.1 提供早期引用的入口:getSingleton(beanName, allowEarlyReference)

典型逻辑(伪代码):

java 复制代码
Object singleton = singletonObjects.get(name);
if (singleton == null && isCurrentlyInCreation(name)) {
    singleton = earlySingletonObjects.get(name);
    if (singleton == null && allowEarlyReference) {
        ObjectFactory<?> factory = singletonFactories.get(name);
        if (factory != null) {
            singleton = factory.getObject();          // 生成早期引用(可能是代理)
            earlySingletonObjects.put(name, singleton);
            singletonFactories.remove(name);
        }
    }
}
return singleton;

6.2 何时放入三级缓存:addSingletonFactory

在 bean 实例化之后、属性填充之前:

  • 实例化后:已经有了 raw bean
  • 属性注入前:正是循环依赖会发生的位置

6.3 早期引用如何可能变成代理:getEarlyBeanReference

AOP 场景下通常由 SmartInstantiationAwareBeanPostProcessor 参与,例如:

  • AbstractAutoProxyCreator#getEarlyBeanReference

它会判断该 bean 是否需要代理,如果需要就提前返回代理对象。


7. Spring 能解决哪些循环依赖?不能解决哪些?

7.1 ✅ 能解决(典型)

  • 单例(singleton)
  • Setter/字段注入
  • 依赖发生在"属性填充阶段"
  • AOP 也可以(靠 getEarlyBeanReference)

7.2 ❌ 不能解决(高频面试点)

  1. 构造器注入循环依赖
  • A 的构造器需要 B,B 的构造器需要 A
  • 还没实例化就需要对方,根本没有"先露个面"的机会
  • 三级缓存也救不了
  1. 原型(prototype)Bean 的循环依赖
  • prototype 不进单例缓存体系(每次都是 new)
  • 没有统一的缓存"早期暴露"机制
  1. 过早暴露导致的"代理/最终对象不一致"问题(复杂 AOP 链)
  • 虽然 Spring 做了很多兼容,但某些极端组合仍可能翻车
  • 常见表现:拿到的是 raw bean 而不是代理,导致事务/切面失效

8. Spring Boot 的默认行为(非常实用)

从 Spring Boot 2.6 开始,默认禁止 Bean 循环依赖(更鼓励你改代码结构),需要时可以手动打开。

application.yml:

yaml 复制代码
spring:
  main:
    allow-circular-references: true

也可以通过 SpringApplication#setAllowCircularReferences(true) 设置。

现实建议:能不靠循环依赖就别靠。允许只是 "救急开关",不是"架构特性"。


9. 如何"从根上"消除循环依赖(更像高级工程师的答案)

  1. 引入中间层/抽象
  • A 依赖接口 X,B 实现 X 或由第三方协调者实现
  1. 事件驱动 / 发布订阅
  • A 发布事件,B 监听事件,避免直接互相持有引用
  1. @Lazy 延迟注入
  • 注入一个延迟代理,打断初始化期的强依赖

    java 复制代码
    @Autowired @Lazy
    private A a;
  1. 把"互相调用"改成"单向调用 + 回调/策略"
  • 循环依赖本质经常是职责混乱,拆职责通常就顺便拆掉依赖环

10. 面试速记版

  • Spring 解决的是 单例 + setter/字段注入 的循环依赖
  • 核心是 提前暴露早期引用 来打断依赖环
  • 三级缓存
    • 一级:成品单例
    • 二级:早期引用(半成品)
    • 三级:ObjectFactory(按需生成早期引用,支持 AOP 提前代理)
  • 构造器循环依赖、prototype 循环依赖基本无解
  • Spring Boot 2.6+ 默认禁止循环依赖,可通过 spring.main.allow-circular-references=true 临时开启

相关推荐
云烟成雨TD21 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Java成神之路-1 天前
SpringMVC 响应实战指南:页面、文本、JSON 返回全流程(Spring系列13)
java·spring·json
砍材农夫1 天前
spring-ai 第六模型介绍-聊天模型
java·人工智能·spring
云烟成雨TD1 天前
Spring AI Alibaba 1.x 系列【5】ReactAgent 构建器深度源码解析
java·人工智能·spring
Flittly1 天前
【SpringAIAlibaba新手村系列】(15)MCP Client 调用本地服务
java·笔记·spring·ai·springboot
Flittly1 天前
【SpringAIAlibaba新手村系列】(14)MCP 本地服务与工具集成
java·spring boot·笔记·spring·ai
mfxcyh1 天前
基于xml、注解、JavaConfig实现spring的ioc
xml·java·spring
Flittly1 天前
【SpringAIAlibaba新手村系列】(13)Tool Calling 函数工具调用技术
java·spring boot·spring·ai
xdscode1 天前
Spring 依赖注入方式全景解析
java·后端·spring