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 临时开启

相关推荐
diudiu96283 小时前
Maven配置阿里云镜像
java·spring·阿里云·servlet·eclipse·tomcat·maven
222you7 小时前
SpringAOP的介绍和入门
java·开发语言·spring
CodeAmaz7 小时前
Spring编程式事务详解
java·数据库·spring
谷哥的小弟8 小时前
Spring Framework源码解析——RequestContext
java·后端·spring·框架·源码
程序员阿鹏9 小时前
SpringBoot自动装配原理
java·开发语言·spring boot·后端·spring·tomcat·maven
老华带你飞9 小时前
工会管理|基于springboot 工会管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
⑩-10 小时前
SpringCloud-Feign客户端实战
后端·spring·spring cloud
qq_124987075310 小时前
基于springboot的幼儿园家校联动小程序的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·小程序
后端小张11 小时前
【Java 进阶】深入理解Redis:从基础应用到进阶实践全解析
java·开发语言·数据库·spring boot·redis·spring·缓存