天工AI助手体验Spring循环依赖三级缓存机制

小编 3 0

更新时间:2026年4月10日 10:30 · 北京

【天工AI助手体验】 据2026年Java生态调研报告数据,约23%的Spring应用开发者曾遇到过循环依赖问题,字段注入导致的循环依赖占比高达67%-29。无论你是在校学生准备面试,还是技术入门者初学Spring框架,理解循环依赖的底层原理,都是通往Spring进阶之路上一道绕不开的门槛。


一、开篇引入:为什么循环依赖是Spring必学知识点

循环依赖(Circular Dependency)是Spring IoC容器中的核心高频考点,也是大厂面试中考察候选人框架理解深度的“试金石”。很多开发者在使用Spring时,可能遇到过项目启动失败、控制台打印BeanCurrentlyInCreationException异常的经历,却搞不清楚Spring到底是如何解决这个问题的-1

本文将从概念定义、痛点分析、三级缓存机制、源码解析到面试考点,层层递进,带你彻底搞懂Spring解决循环依赖的底层原理。


二、痛点切入:传统实现方式的问题

什么是循环依赖?

循环依赖,简单来说,就是两个或多个Bean之间互相持有对方的引用,形成了一个闭环依赖关系-1。最典型的场景如下:

java
复制
下载
@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;   // A依赖B
}

@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;   // B依赖A,形成循环
}

不处理循环依赖会发生什么?

在默认情况下,如果Spring不做特殊处理,项目启动时就会抛出BeanCurrentlyInCreationException异常-1。让我们来剖析一下Spring创建Bean的三个关键阶段:

阶段说明完成后的状态
实例化调用构造函数创建对象实例半成品(属性仍为null)
属性填充注入依赖的属性(@Autowired生效)正在装配
初始化调用初始化方法,完成AOP代理等成品Bean

ServiceA依赖ServiceBServiceB又依赖ServiceA时,两个Bean互相等待对方先完成创建,形成了“先有鸡还是先有蛋”的死锁困境-29


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

三级缓存的定义

为了解决单例Bean的循环依赖问题,Spring设计了三级缓存机制(Three-Level Cache) ,通过提前暴露半成品Bean的方式打破依赖闭环-1

三级缓存的三个核心组件定义如下:

缓存级别缓存名称英文全称作用
一级缓存singletonObjectsSingleton Objects存放完全初始化完成的成品Bean
二级缓存earlySingletonObjectsEarly Singleton Objects存放提前暴露的半成品Bean
三级缓存singletonFactoriesSingleton Factories存放ObjectFactory对象工厂

生活化类比理解

把三级缓存想象成“快餐店的备餐流程”

  • 一级缓存(成品) :顾客可以直接取走的完成品套餐

  • 二级缓存(半成品) :已经切好食材、半熟状态的备料

  • 三级缓存(工厂) :存放着“如何制作这道菜”的配方卡,需要时才按配方制作

当两个顾客互相等待对方的餐食时(循环依赖),厨师就可以先拿出配方卡(三级缓存),快速备好半成品给另一方先用,等主菜完成后最终补上成品。


三、核心概念讲解(二):源码层面的三级缓存

源码中的缓存定义

Spring处理Bean创建的核心逻辑集中在DefaultSingletonBeanRegistry类中-1

java
复制
下载
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry {
    
    // 一级缓存:存放完全初始化好的单例Bean(成品)
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
    // 三级缓存:存放Bean的工厂对象
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    
    // 二级缓存:存放提前暴露的Bean实例(半成品)
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    
    // 记录当前正在创建的Bean名称
    private final Set<String> singletonsCurrentlyInCreation = 
        Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}

为什么三级缓存要存ObjectFactory而不是直接存对象?

这是理解Spring循环依赖机制的关键问题。三级缓存存储的是ObjectFactory(对象工厂) ,这是一个函数式接口,只有调用getObject()时才会真正创建Bean实例-1

这样做的好处在于:

  • 按需生成代理:如果Bean需要AOP代理(如@Transactional@Async),就在这里动态生成代理对象-2

  • 延迟决策:代理的生成时机延迟到第一次被其他Bean引用时才执行,避免过早代理带来的副作用-2


四、概念关系与区别总结

三级缓存的核心协作逻辑

缓存存放内容访问优先级何时移出
一级缓存成品Bean最高(先查)不移出(单例池)
二级缓存半成品Bean次之Bean初始化完成后
三级缓存ObjectFactory工厂最后工厂被调用后移出

一句话概括:一级缓存存“成品”,二级缓存存“已确定的早期引用”,三级缓存存“将来可能生成代理的工厂”-2


五、代码示例演示:完整的循环依赖解决流程

示例代码

ServiceA依赖ServiceBServiceB依赖ServiceA为例:

java
复制
下载
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    public void doA() {
        serviceB.doSomething();
    }
}

@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
    
    public void doB() {
        serviceA.doSomething();
    }
}

核心流程解析

Spring的getSingleton()方法是解决循环依赖的入口-1

java
复制
下载
@Nullable
public Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 步骤1:优先从一级缓存获取成品Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 步骤2:从二级缓存获取提前暴露的半成品Bean
        singletonObject = this.earlySingletonObjects.get(beanName);
        
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 双重检查
                // 步骤3:从三级缓存获取ObjectFactory并生成早期引用
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

完整创建流程

  1. 创建ServiceA:实例化A,将A的工厂对象放入三级缓存

  2. 注入依赖B:发现A需要B,开始创建B

  3. 创建ServiceB:实例化B,将B的工厂对象放入三级缓存

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

  5. 完成B创建:B拿到A的引用后完成初始化,放入一级缓存

  6. 完成A创建:A接着完成初始化,也放入一级缓存-25


六、底层原理与技术支撑

Spring循环依赖解决机制底层依赖以下核心技术点:

  1. 反射机制(Reflection) :通过createBeanInstance方法反射调用构造函数创建Bean实例

  2. 代理模式(Proxy Pattern) :AOP代理的生成依赖JDK动态代理或CGLIB代理

  3. 工厂模式(Factory Pattern) :ObjectFactory本质上是工厂模式的函数式实现

  4. 三级缓存的并发控制:通过synchronizedConcurrentHashMap保证线程安全


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

面试题1:Spring是如何解决循环依赖的?

标准答案:Spring通过三级缓存机制解决了单例Bean的Setter/Field注入场景下的循环依赖。当A依赖B、B依赖A时,Spring在创建A实例后将其ObjectFactory放入三级缓存;当B需要注入A时,从三级缓存获取ObjectFactory生成A的早期引用并放入二级缓存;B完成初始化后放入一级缓存;A接着完成初始化,完成循环依赖的打破-25

面试题2:为什么要用三级缓存?两级缓存不够吗?

标准答案:两级缓存无法解决AOP代理场景下的循环依赖问题。如果只有二级缓存,需要在实例化时立即决定是否生成代理对象,但此时尚未走到初始化阶段,无法判断是否需要增强。三级缓存通过ObjectFactory将代理生成延迟到第一次被其他Bean引用时才执行,既按需代理,又不破坏生命周期,保证了最终暴露的对象与最终单例一致-2-9

面试题3:构造器注入的循环依赖为什么解决不了?

标准答案:构造器注入要求在实例化时就提供所有依赖,而循环依赖的场景下,两个Bean都无法完成实例化。Spring的三级缓存机制依赖于提前暴露早期引用,这在构造器注入的场景下无法实现,因为实例化阶段尚未完成,无法提前暴露引用-25-4

面试题4:哪些循环依赖场景Spring无法解决?

标准答案:①构造器注入的循环依赖;②多例(prototype)作用域的Bean;③循环中使用了@Async注解的Bean(代理对象与原始对象不一致导致报错)-19-12

面试题5:Spring Boot 2.6之后循环依赖默认行为有何变化?

标准答案:从Spring Boot 2.6开始,为了鼓励更清晰的代码设计,默认已禁用循环依赖,需要在application.properties中设置spring.main.allow-circular-references=true才能开启。构造器注入的循环依赖即使在配置开启后也无法解决-25-41


八、结尾总结

回顾全文,Spring循环依赖的核心知识点可以浓缩为以下要点:

知识点核心内容
概念Bean之间相互依赖形成闭环
解决方案三级缓存机制(singletonObjects、earlySingletonObjects、singletonFactories)
适用场景单例Bean + Setter/字段注入
不适用场景构造器注入、多例Bean、@Async循环依赖
底层支撑反射、代理、工厂模式
Spring Boot 2.6+默认禁止,需手动开启

面试应答技巧:面试时建议按照“先说结论→阐述三级缓存流程→说明限制条件→补充设计优化建议”的逻辑层次递进,既能展示技术深度,又能体现系统思考能力-25

下篇预告:下一篇将深入讲解Spring AOP的底层原理——JDK动态代理与CGLIB的区别与实现机制,敬请期待。