【星际AI助手】2026年4月 Spring循环依赖三级缓存原理与源码深度剖析

小编头像

小编

管理员

发布于:2026年04月29日

6 阅读 · 0 评论

2026年4月,星际AI助手为您深度拆解Spring循环依赖这一核心面试考点的底层实现原理与面试应答技巧。

循环依赖(Circular Dependency)是Spring面试中的必考题,很多人知道“三级缓存”这四个字,却说不清为什么是三级不是两级、ObjectFactory到底存的是什么、AOP代理又是如何与三级缓存协同工作的-3。本文将从概念到源码,从示例到面试题,帮你彻底打通对Spring循环依赖的认知链路。


一、痛点切入:为什么需要这个技术?

传统方式存在的问题

假设没有Spring的三级缓存机制,两个Bean相互依赖的场景会直接陷入死锁:

java
复制
下载
// ServiceA 依赖 ServiceB
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;   // 需要B的实例
}

// ServiceB 依赖 ServiceA
@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;   // 需要A的实例
}

传统流程的死循环:

  1. 创建A → 属性注入时发现需要B → B尚未创建

  2. 转而创建B → 属性注入时发现需要A → A尚未完成创建

  3. 无限递归,最终抛出 BeanCurrentlyInCreationException

传统方式的缺点:

  • ❌ 循环依赖直接导致应用启动失败

  • ❌ 开发者需要在编码时手动规避,增加心智负担

  • ❌ 业务代码的依赖关系天然复杂,难以完全避免循环引用

  • ❌ 传统的工厂模式无法在对象未完全初始化时提前暴露引用

Spring的设计初衷

Spring通过三级缓存机制,实现了“在Bean实例化后、属性填充前,将早期引用暴露出去”的能力,从而优雅地打破依赖闭环-5


二、核心概念讲解:三级缓存

标准定义

Spring在 DefaultSingletonBeanRegistry 类中定义了三个Map,用于在Bean创建的不同阶段存储对象,统称为三级缓存

缓存级别名称数据结构作用
一级缓存singletonObjectsConcurrentHashMap<String, Object>存放完全初始化完成的成品Bean
二级缓存earlySingletonObjectsConcurrentHashMap<String, Object>存放提前暴露的半成品Bean(已实例化但未完成属性填充)
三级缓存singletonFactoriesConcurrentHashMap<String, ObjectFactory<?>>存放ObjectFactory工厂,用于按需生成Bean的早期引用-1

生活化类比

想象你在装修房子:

  • 一级缓存 = 装修完毕、家具配齐的房子 → 可以直接入住使用

  • 二级缓存 = 刚打完地基、建好框架的房子 → 结构有了,但内部还没完善

  • 三级缓存 = 装修公司的施工队(工厂) → 需要房子时才会派出施工队去建

核心作用:三级缓存不是为了“存对象”,而是为了 “提前暴露正在创建中的Bean” ,让其他依赖它的Bean能拿到引用,避免陷入死循环-5


三、关联概念讲解:ObjectFactory

标准定义

ObjectFactory 是Spring定义的一个函数式接口,核心方法只有一个:

java
复制
下载
@FunctionalInterface
public interface ObjectFactory<T> {
    T getObject();   // 获取对象实例
}

ObjectFactory与三级缓存的关系

三级缓存存的不直接是Bean实例,而是一个ObjectFactory工厂对象,它封装了“如何获取或创建Bean早期引用”的逻辑。在Spring源码中,放入三级缓存的典型方式为:

java
复制
下载
// 将lambda表达式存入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

当需要提前暴露Bean时,Spring才会调用 singletonFactory.getObject() 来真正获取早期引用,并将其升级到二级缓存中-5

为什么不能直接存对象?

因为Spring要兼顾两个需求:

  1. 解决循环依赖(需要提前暴露)

  2. 保证AOP代理生效(代理对象必须在初始化后、但可能在早期暴露时就要决定)

如果只有二级缓存,就得在Bean实例化后立刻决定是否代理,但此时还没走到初始化步骤,无法判断是否需要增强(比如@Transactional@PostConstruct方法中才起作用)。三级缓存把“要不要代理”延迟到第一次被其他Bean引用时才计算,实现按需代理-5


四、概念关系与区别总结

对比维度三级缓存(思想)二级缓存(中间产物)
存储内容ObjectFactory工厂(生成逻辑)Bean实例(早期引用/半成品)
存在时机Bean实例化后立即放入第一次被其他Bean引用时产生
生命周期最早存在,升级到二级后移除临时过渡,升级到一级后移除
核心价值延迟决策(是否代理、何时创建)缓存已确定的早期引用

一句话概括:

三级缓存是“设计思想”(延迟工厂模式),二级缓存是“落地产物”(早期对象引用),一级缓存是“最终成果”(成品Bean)。


五、代码/流程示例演示

循环依赖场景模拟

两个Service相互依赖:

java
复制
下载
@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;   // A依赖B
    
    public void doA() {
        System.out.println("ServiceA doA");
    }
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;   // B依赖A,形成循环
    
    public void doB() {
        System.out.println("ServiceB doB");
    }
}

Spring解决循环依赖的完整流程

ServiceAServiceBServiceA 的场景为例:

步骤一:创建ServiceA

  • 实例化A(调用构造器,生成原始对象)

  • 将A的ObjectFactory放入三级缓存 singletonFactories

  • 标记A正在创建中

步骤二:填充A的属性,发现需要B

  • 调用 getBean(B) 开始创建B

步骤三:创建ServiceB

  • 实例化B(调用构造器,生成原始对象)

  • 将B的ObjectFactory放入三级缓存

  • 填充B的属性,发现需要A

步骤四:获取A的早期引用(关键环节)

  • B调用 getBean(A) 时,检查一级缓存→无

  • 发现A正在创建中,检查二级缓存→无

  • 检查三级缓存→有A的ObjectFactory

  • 调用 factory.getObject() 获取A的早期引用

  • 将早期引用放入二级缓存 earlySingletonObjects

  • 从三级缓存移除A的工厂

步骤五:完成B的创建

  • 将A的早期引用注入B

  • 继续执行B的初始化

  • B初始化完成后,放入一级缓存 singletonObjects

  • 清理B在二三级缓存中的痕迹

步骤六:完成A的创建

  • 将B的完整实例注入A

  • 执行A的初始化

  • A初始化完成后,放入一级缓存 singletonObjects

  • 清理A在二三级缓存中的痕迹

关键代码示意(简化版)

java
复制
下载
// 核心获取逻辑(来自DefaultSingletonBeanRegistry.getSingleton)
public Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 1. 一级缓存取成品
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 2. 二级缓存取半成品
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            // 3. 三级缓存取ObjectFactory,调用getObject()生成对象并升级到二级
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
    return singletonObject;
}

关键注释:上述代码展示了三级缓存如何协同工作——一级取成品,二级取半成品,三级取工厂并生成对象后升级-1


六、底层原理/技术支撑

依赖的核心技术

  1. 反射:通过反射调用Bean的构造器完成实例化

  2. Bean生命周期管理:Spring将Bean创建拆分为实例化、属性填充、初始化等多个阶段,三级缓存在实例化之后、属性填充之前插入“提前暴露”环节-

  3. 代理模式ObjectFactory.getObject() 内部会调用 getEarlyBeanReference,该方法负责判断是否需要对Bean进行AOP增强

  4. 工厂模式:三级缓存不存对象,存工厂,实现延迟创建

为什么AOP场景下三级缓存不可或缺?

当Bean需要被AOP代理时(如加了@Transactional),最终注入到其他Bean中的必须是代理对象,而不能是原始对象,否则事务等增强功能将失效。三级缓存在getEarlyBeanReference方法中判断是否需要生成代理,从而保证早期暴露出去的对象与最终完成初始化的对象是同一个代理对象-5。如果只用二级缓存,原始对象在实例化后就被放进缓存,后续再被代理替换,就会导致“前面的人拿到的是原始对象,后面的人拿到的是代理对象”的不一致问题。


七、高频面试题与参考答案

Q1:Spring是如何解决循环依赖的?

参考答案:

  1. Spring通过三级缓存机制解决单例Bean的循环依赖问题

  2. 在Bean实例化后、属性注入前,将Bean的ObjectFactory放入三级缓存,提前暴露早期引用

  3. 当其他Bean需要依赖该Bean时,从三级缓存获取工厂,调用getObject()生成对象并升级到二级缓存

  4. 最终完成所有Bean的初始化,将成品放入一级缓存

踩分点:三级缓存的三个Map名称、各自存储内容、缓存升级流程。

Q2:为什么要用三级缓存?两级不行吗?

参考答案:

  • 一级存成品,二级存已确定的早期引用,三级存工厂

  • 如果只用二级缓存,需要在Bean实例化时就决定是否生成代理对象,但此时Bean尚未初始化,无法知道是否需要增强

  • 三级缓存将“是否代理”的决策延迟到第一次被其他Bean引用时,实现了按需代理,同时保证早期暴露的对象与最终对象是同一个代理实例-5

Q3:构造器注入的循环依赖为什么解决不了?

参考答案:

  • 构造器注入要求在实例化阶段就提供所有依赖,而循环依赖的场景下,两个Bean都无法完成实例化

  • 三级缓存机制依赖于 “实例化之后、属性注入之前” 这个时间窗口来提前暴露引用,构造器注入没有这个窗口-

Q4:如果A依赖B,B依赖A,Spring创建A和B的完整流程是怎样的?

参考答案:

  1. 实例化A → 放入三级缓存

  2. 填充A属性,发现需要B → 开始创建B

  3. 实例化B → 放入三级缓存

  4. 填充B属性,发现需要A → 从三级缓存获取A的工厂 → 生成A早期引用放入二级缓存

  5. 完成B的初始化 → 放入一级缓存

  6. 返回A,完成A的初始化 → 放入一级缓存

Q5:哪些情况下Spring无法解决循环依赖?

参考答案:

  • 构造器注入的循环依赖

  • 原型(Prototype)作用域的Bean

  • 某些AOP代理场景(如@Async导致的代理对象不一致)--2


八、结尾总结

核心知识点回顾

知识点关键内容
什么是循环依赖两个或多个Bean互相持有对方引用,形成闭环
Spring如何解决三级缓存机制,提前暴露早期引用
三级缓存分工一级→成品,二级→早期引用,三级→工厂
为什么是三级兼顾循环依赖解耦与AOP代理的一致性
解决不了的场景构造器注入、prototype作用域

重点与易错点

⚠️ 注意:三级缓存不是Spring自动解决所有循环依赖的万能药。构造器注入的循环依赖仍会报错,推荐使用字段注入或Setter注入并合理设计依赖关系。Spring Boot 2.6+默认禁止循环依赖,可通过spring.main.allow-circular-references=true开启,但不建议-44

下一篇预告

下一期将深入剖析 Spring AOP的底层原理——动态代理如何生成、@Transactional是如何工作的、JDK动态代理与CGLIB的区别与选择,敬请关注星际AI助手的技术专栏。

标签:

相关阅读