Spring源码 第三篇:Spring 源码深度拆解:循环依赖 + 三级缓存

一、先搞懂:什么是循环依赖?

最常见的场景:

java 复制代码
// 类 A 依赖 B
@Service
public class AService {
    @Autowired
    private BService bService;
}

// 类 B 依赖 A
@Service
public class BService {
    @Autowired
    private AService aService;
}

循环依赖过程:

  • A 创建时,需要注入 B → B 还没创建,去创建 B
  • B 创建时,需要注入 A → A 还没创建,去创建 A
  • 陷入死循环:A ← B ← A ← B...

Spring 如何打破这个死循环? 答案就是 三级缓存

二、核心前提:Spring 只解决「单例 + setter/field 注入」的循环依赖

场景 能否解决 说明
✅ 单例 Bean + setter 注入(@Autowired) Spring 默认支持
✅ 构造器注入 + @Lazy 注解 通过代理延迟实例化,打断循环
❌ 多例 Bean(@Scope("prototype")) 不能 无缓存机制
❌ 无 @Lazy 注解的构造器注入 不能 实例化时就需要依赖
❌ 原型 Bean 与单例 Bean 交叉循环依赖 不能 缓存机制不匹配

原因: 后面源码里会逐行说清。

三、三级缓存核心定义

三级缓存全部定义在 DefaultSingletonBeanRegistry 类中,是 Spring 解决循环依赖的核心容器,按优先级从高到低排列:

缓存级别 缓存名称 存储内容 核心作用
一级缓存 singletonObjects 成品 Bean(完全初始化完成,可直接使用) 供外部获取,避免重复创建,保证单例唯一性
二级缓存 earlySingletonObjects 早期暴露的 Bean 引用(实例化完成、未填充属性、未初始化) 保证循环依赖过程中,注入的是同一个早期实例,避免重复执行三级缓存的工厂逻辑
三级缓存 singletonFactories Bean 工厂(lambda 表达式,用于生成早期引用) 延迟生成代理对象,确保无论是否有循环依赖,Bean 最终只有一个代理实例(或原始实例)

源码佐证(DefaultSingletonBeanRegistry):

java 复制代码
// 一级缓存:成品 Bean(key:beanName,value:成品Bean)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存:早期暴露的 Bean 引用(未初始化)
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存:Bean 工厂(用于生成早期引用)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 正在创建中的 Bean 集合(标记哪些 Bean 正在创建,避免重复创建)
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

四、循环依赖破解全流程(源码级追踪,A ↔ B 举例)

我们以 AServiceBService 循环依赖为例,逐行追踪源码,看三级缓存如何工作。

前提:

  • A、B 都是默认单例、非懒加载、setter 注入(@Autowired)
  • 容器启动时,先创建 A(按 BeanName 字典序,可自行调试验证)

步骤 1:创建 A → 实例化 A(未填充属性)

容器执行 preInstantiateSingletons(),遍历 BeanName,先执行 getBean("aService")

  1. 进入 doGetBean(),从一级缓存 singletonObjects 拿 A → 无(null)
  2. 标记 A 为「正在创建」,加入 singletonsCurrentlyInCreation
  3. 进入 createBean()doCreateBean()
  4. 执行 createBeanInstance():通过构造器实例化 A(此时 A 只是个空对象,bService 为 null)

步骤 2:A 暴露早期引用到三级缓存

实例化 A 后,Spring 会主动暴露 A 的早期引用(未填充属性),存入三级缓存:

java 复制代码
// doCreateBean() 内部代码(AbstractAutowireCapableBeanFactory)
// 暴露早期引用:将 A 的工厂放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

进入 addSingletonFactory(DefaultSingletonBeanRegistry):

java 复制代码
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 放入三级缓存
            this.singletonFactories.put(beanName, singletonFactory);
            // 清空二级缓存(避免冲突)
            this.earlySingletonObjects.remove(beanName);
            // 标记为已注册
            this.registeredSingletons.add(beanName);
        }
    }
}

此时状态:

  • 三级缓存有 A 的工厂
  • 一级、二级缓存无 A

步骤 3:A 填充属性 → 需要注入 B

实例化 A 后,进入 populateBean(),开始填充 @AutowiredBService

  1. Spring 发现 A 需要 B → 执行 getBean("bService"),去创建 B
  2. 进入 B 的 doGetBean(),从一级缓存拿 B → 无(null)
  3. 标记 B 为「正在创建」,加入 singletonsCurrentlyInCreation
  4. 进入 B 的 doCreateBean(),执行 createBeanInstance(),实例化 B(B 的 aService 为 null)
  5. B 实例化后,同样执行 addSingletonFactory(),将 B 的工厂放入三级缓存
  6. B 进入 populateBean(),开始填充 @AutowiredAService → 执行 getBean("aService")

步骤 4:B 获取 A → 从三级缓存拿到 A 的早期引用

B 执行 getBean("aService") 后,流程如下:

  1. 从一级缓存 singletonObjects 拿 A → 无
  2. 从二级缓存 earlySingletonObjects 拿 A → 无
  3. 从三级缓存 singletonFactories 拿 A 的工厂 → 有!
  4. 执行工厂的 lambda 表达式(getEarlyBeanReference()),生成 A 的早期引用(未填充属性)
  5. 将 A 的早期引用从三级缓存移到二级缓存(三级缓存移除 A,二级缓存添加 A)
  6. 将 A 的早期引用注入到 B 中 → B 的 aService 不再为 null

步骤 5:B 完成初始化 → 成为成品 Bean,放入一级缓存

B 注入 A 后,继续执行 initializeBean()(Aware、前后置处理器、AOP 代理)

B 初始化完成,执行 addSingleton(),将 B 放入一级缓存:

java 复制代码
// DefaultSingletonBeanRegistry
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 放入一级缓存(成品 Bean)
        this.singletonObjects.put(beanName, singletonObject);
        // 清空三级、二级缓存
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}
  1. B 创建完成,返回给 A,注入到 A 的 bService 中 → A 的 bService 不再为 null

步骤 6:A 完成初始化 → 成为成品 Bean,放入一级缓存

A 注入 B 后,继续执行 initializeBean(),完成初始化(生成 AOP 代理等)

A 初始化完成,执行 addSingleton(),将 A 放入一级缓存,清空二级缓存中的 A

标记 A 为「创建完成」,从 singletonsCurrentlyInCreation 中移除

步骤 7:循环依赖破解完成

  • 一级缓存: A(成品)、B(成品)
  • 二级、三级缓存: A、B 均已清空
  • 后续获取 A 或 B,直接从一级缓存拿,无循环依赖

五、关键源码拆解(断点能追到的核心方法)

1. getEarlyBeanReference(生成早期引用)

java 复制代码
// AbstractAutowireCapableBeanFactory
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    // 执行 BeanPostProcessor 后置处理(如果有 AOP 代理,这里会生成代理)
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

作用:

  • 生成 Bean 的早期引用(未初始化,但可能已生成 AOP 代理)
  • 保证循环依赖时,注入的是同一个 Bean 引用(避免重复生成代理)

2. getSingleton(核心缓存获取逻辑)

java 复制代码
// DefaultSingletonBeanRegistry
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 先从一级缓存拿成品 Bean
    Object singletonObject = this.singletonObjects.get(beanName);

    // 2. 一级缓存没有,且 Bean 正在创建中
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 3. 从二级缓存拿早期引用
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 4. 二级缓存没有,且允许早期引用(循环依赖核心开关)
            if (singletonObject == null && allowEarlyReference) {
                // 5. 从三级缓存拿工厂,生成早期引用
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    // 6. 移到二级缓存,清空三级缓存(避免重复生成)
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

关键细节:

  • allowEarlyReference:是否允许获取早期引用(循环依赖的核心开关,默认 true)
  • 流程:一级 → 二级 → 三级,层层获取,避免死循环

六、面试高频问题

1. 为什么需要三级缓存?不能用二级缓存吗?

答: 为了 保证 AOP 代理的唯一性

  • 三级缓存存的是「Bean 工厂」,不是直接的 Bean 引用
  • 只有当发生循环依赖时,才会执行工厂生成早期引用(可能包含 AOP 代理)
  • 如果只用二级缓存,提前生成代理,会导致 Bean 初始化完成后,出现「两个代理对象」(破坏单例)
  • 三级缓存的延迟生成机制,确保无论是否有循环依赖,Bean 最终只有一个代理对象

2. 为什么 Spring 只能解决 setter 注入的循环依赖?

答: 核心是「注入时机不同」:

  • setter 注入: 实例化后、初始化前 注入(先实例化 A,再注入 B)
  • 构造器注入: 实例化时 就需要注入依赖(创建 A 的构造器时,就需要 B,此时 B 还没实例化,无法暴露早期引用)

循环依赖破解的前提是「先实例化,再暴露早期引用」,构造器注入无法满足这个前提,所以解决不了。

3. 多例 Bean 为什么不能解决循环依赖?

答: 多例 Bean 每次 getBean() 都会创建新对象,没有缓存(三级缓存只针对单例):

  • A 创建时,需要 B → 新建 B
  • B 创建时,需要 A → 新建 A
  • 新建的 A 又需要新建 B,陷入无限循环,无法通过缓存打破

4. 二级缓存的作用是什么?

答: 核心作用是 保证循环依赖过程中,注入的是同一个早期实例,性能优化是附带效果:

  • 当多个 Bean 依赖同一个早期实例时(如 A ↔ B、A ↔ C),B 第一次获取 A 时从三级缓存生成早期引用并移入二级缓存,C 再获取 A 时直接从二级缓存拿,避免重复执行三级缓存的工厂逻辑(尤其是避免重复生成 AOP 代理)
  • 若没有二级缓存,每次获取早期引用都要执行工厂逻辑,不仅低效,还可能导致实例不一致

七、循环依赖完整链路

复制代码
A 创建 → 实例化 A → 暴露 A 工厂到三级缓存 → 填充属性需要 B
        ↓
B 创建 → 实例化 B → 暴露 B 工厂到三级缓存 → 填充属性需要 A
        ↓
B 获取 A → 三级缓存取 A 工厂 → 生成 A 早期引用 → 移到二级缓存 → 注入 B
        ↓
B 填充完成 → 初始化(AOP 代理)→ 成为成品 → 放入一级缓存 → 返回给 A 注入
        ↓
A 填充完成 → 初始化(若已生成代理则跳过)→ 成为成品 → 放入一级缓存 → 循环依赖破解

八、总结

Spring 通过三级缓存机制,巧妙地解决了单例 Bean 的 setter/field 注入循环依赖问题。核心思想是 提前暴露未初始化的 Bean 引用,让依赖方能够先拿到一个"半成品",待自身初始化完成后再补全依赖关系。

关键要点:

  1. 三级缓存各司其职: 一级存成品、二级存早期引用、三级存工厂
  2. AOP 代理唯一性: 三级缓存的工厂机制确保代理对象只生成一次
  3. 注入时机是关键: setter 注入允许先实例化后注入,构造器注入则不行
  4. 多例 Bean 无解: 因为没有缓存机制,每次都是新对象

理解三级缓存不仅是面试必备,更是深入 Spring IoC 容器工作原理的重要一步。建议结合源码调试,亲自走一遍循环依赖的创建流程,印象会更加深刻。

相关推荐
武子康7 分钟前
Java-07 深入浅出 MyBatis数据库一对多关系模型实战:表结构设计与查询实现
java·后端
REDcker2 小时前
Linux OverlayFS详解
java·linux·运维
Royzst2 小时前
xml知识点
java·服务器·前端
橙子圆1233 小时前
Redis知识9之集群
数据库·redis·缓存
鱼鳞_3 小时前
苍穹外卖-Day08(缓存套餐)
java·redis·缓存
过期动态3 小时前
【LeetCode 热题 100】移动零
java·数据结构·算法·leetcode·职场和发展·rabbitmq
sinat_255487814 小时前
IDEA:查找文件/类
java·ide·设计模式·intellij-idea
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第77题】【Mysql篇】第7题:回表查询与全表扫描的区别?
java·开发语言·数据库·mysql·面试
lulu12165440785 小时前
Claude Code SpringBoot技能体系架构设计与演进
java·人工智能·spring boot·后端·ai编程
callJJ5 小时前
Nacos 详解——从概念到实战
java·spring boot·spring·spring cloud·微服务·nacos