在构建大型Java后端系统的过程中,Spring框架的循环依赖问题是每个开发者绕不开的技术难关。而值得关注的是,购物助手AI背后复杂的微服务调用链路同样充斥着模块间的相互引用。这就好比购物助手AI中的检索增强生成(RAG)需要多个服务模块互相协作——若A服务依赖B服务,B服务又依赖A服务,容器在创建这些Bean时就会陷入“先有鸡还是先有蛋”的困局。本文将从底层原理出发,深入拆解Spring三级缓存机制,配套完整代码示例与面试考点,帮助读者彻底搞懂循环依赖问题。
一、痛点切入:为什么需要解决循环依赖

在日常开发中,循环依赖是一个高频出现的场景-2。比如,在购物助手AI的智能推荐模块中,用户意图识别服务可能依赖商品召回服务,而商品召回服务又依赖用户画像服务来获取偏好——这种依赖关系很容易形成闭环。
旧实现方式存在的问题

// 代码示例:典型的循环依赖场景 @Component class AService { // A依赖B @Autowired private BService b; } @Component class BService { // B依赖A,形成循环 @Autowired private AService a; }
如果不做特殊处理,项目启动时Spring会抛出BeanCurrentlyInCreationException异常-2。问题根源在于:容器需要先实例化A才能注入B,但B又需要A才能完成注入,形成“死锁”。
传统方式的缺陷
耦合过高:模块间直接相互引用,难以解耦
扩展性差:添加新功能时可能引入新的循环依赖
维护困难:代码依赖关系错综复杂,难以梳理
启动失败:项目无法正常启动,影响开发效率
Spring正是为了解决这一问题,设计了精妙的三级缓存机制,通过提前暴露“半成品Bean”的方式打破依赖闭环-2。
二、核心概念讲解:什么是循环依赖
循环依赖(Circular Dependency) ,指的是两个或多个Bean之间相互持有对方的引用,形成闭环依赖关系-2。
场景化类比
想象一下,两个工人互相等着对方先给自己递工具:工人A需要工人B的锤子才能干活,工人B需要工人A的扳手才能干活。两人都空着手站在工位上,谁也无法开始工作——这就是循环依赖的生动写照。
Spring的三级缓存机制,相当于在工人A刚开始搭架子时,就让他先把一个“半成品架子”放在旁边供工人B使用,这样B就能拿着这个架子继续干活,最终两人都能完成各自的任务。
Spring支持的循环依赖场景
| 依赖类型 | 注入方式 | 是否支持 |
|---|---|---|
| 构造器注入 | 通过构造函数 | ❌ 不支持 |
| Setter注入 | 通过setter方法 | ✅ 支持 |
| 字段注入 | @Autowired注解 | ✅ 支持 |
三、关联概念讲解:三级缓存(概念B)
三级缓存(Three-Level Cache) ,是Spring在DefaultSingletonBeanRegistry类中维护的三个Map结构,用于管理单例Bean在不同创建阶段的状态-2。
三级缓存定义
| 缓存级别 | 缓存名称 | 存储内容 | 作用 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化完成的成品Bean | 供业务直接使用 |
| 二级缓存 | earlySingletonObjects | 提前暴露的半成品Bean(已实例化但未完成属性填充) | 存放早期引用 |
| 三级缓存 | singletonFactories | ObjectFactory对象工厂 | 按需生成Bean实例(代理或原始对象) |
与循环依赖的关系
三级缓存是实现循环依赖解决方案的关键载体:
概念A(循环依赖) 是问题本身
概念B(三级缓存) 是Spring解决这一问题的具体技术手段
简单来说:循环依赖是“病”,三级缓存是“药” 。前者描述的是问题场景,后者是Spring框架给出的解决方案。
四、概念关系与区别总结
关系梳理
| 维度 | 循环依赖 | 三级缓存 |
|---|---|---|
| 角色定位 | 问题(Problem) | 解决方案(Solution) |
| 抽象层次 | 设计层 | 实现层 |
| 适用范围 | 所有IoC容器都可能遇到 | Spring框架特有的实现机制 |
| 核心思想 | 描述依赖闭环 | 提前暴露+懒加载代理 |
一句话概括:循环依赖是Spring容器管理Bean时可能遇到的问题形态,而三级缓存是Spring为解决单例模式下Setter/字段注入循环依赖所设计的具体缓存架构。
五、代码示例:Spring如何解决循环依赖
完整可运行示例
// 1. 定义相互依赖的两个Bean @Component public class A { private B b; @Autowired public void setB(B b) { // Setter注入(Spring支持此种方式解决循环依赖) this.b = b; System.out.println("A: B注入完成"); } public void doSomething() { System.out.println("A执行方法"); } } @Component public class B { private A a; @Autowired public void setA(A a) { this.a = a; System.out.println("B: A注入完成"); } public void doSomething() { System.out.println("B执行方法"); } } // 2. 启动容器并测试 @SpringBootApplication public class Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Application.class, args); A a = context.getBean(A.class); B b = context.getBean(B.class); a.doSomething(); b.doSomething(); } }
核心执行流程
创建Bean A时,Spring在doCreateBean()中会依次执行以下步骤-1:
实例化:通过反射调用构造函数,创建A的“空对象”
提前暴露:将包含A的
ObjectFactory放入三级缓存属性填充:发现A依赖B,触发B的创建
递归创建B:B实例化后同样放入三级缓存,属性填充时发现依赖A
从缓存获取A的早期引用:从三级缓存获取
ObjectFactory,调用getObject()得到A的引用,放入二级缓存完成B的初始化:B注入完成后放入一级缓存
回到A:将已完成的B注入到A,完成A的初始化
关键源码逻辑
// DefaultSingletonBeanRegistry核心逻辑(基于Spring 5.3.x) protected 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并生成对象 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { singletonObject = singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } return singletonObject; }
六、底层原理:技术支撑点
三级缓存机制的精妙之处在于,它充分利用了Java的以下底层技术:
1. 反射机制
Bean的实例化依赖反射调用构造函数,这是Spring IoC容器的基础能力-1。
2. 函数式接口(ObjectFactory)
三级缓存不直接存储Bean实例,而是存储ObjectFactory——一个函数式接口:
@FunctionalInterface public interface ObjectFactory<T> { T getObject(); }
存入的是一个Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean),将“什么时候生成对象” 的决定权交给调用方。
3. AOP代理机制
三级缓存存在的最关键原因——当Bean需要被AOP代理时(如加了@Transactional),三级缓存中的ObjectFactory会在调用getObject()时动态决定返回原始对象还是代理对象-9。
为什么二级缓存不够?
如果只用二级缓存,Spring必须在实例化后立刻决定是否生成代理,但此时还未执行BeanPostProcessor后置处理,无法判断是否需要AOP增强。三级缓存将代理决策延迟到第一次被其他Bean引用时,实现按需代理-9。
七、高频面试题与参考答案
Q1:Spring是如何解决循环依赖的?
参考答案(简洁版):
Spring通过三级缓存机制和提前暴露半成品Bean引用的方式来解决单例作用域下Setter/字段注入的循环依赖问题。核心是在Bean实例化后、属性填充前,将包含Bean引用的ObjectFactory放入三级缓存,当其他Bean需要依赖时,从缓存中获取早期引用进行注入,从而打破依赖闭环。
踩分点: ①三级缓存具体哪三级 ②提前暴露的时机 ③ObjectFactory的作用 ④适用场景限制
Q2:为什么必须用三级缓存?二级不够吗?
参考答案:
二级缓存不足以解决AOP代理场景下的循环依赖问题。因为如果只用二级缓存,Spring必须在Bean实例化后立刻决定是否生成代理对象,但此时还未执行BeanPostProcessor后置处理,无法获知该Bean是否需要AOP增强。三级缓存存储的是ObjectFactory工厂,它将代理对象的创建延迟到第一次被其他Bean实际引用时,既能解决循环依赖,又能保证AOP代理正确生效。
踩分点: ①AOP代理的时机 ②延迟决策的设计思想 ③三级缓存vs二级缓存的本质区别
Q3:哪些类型的循环依赖Spring解决不了?
参考答案:
Spring的三级缓存机制无法解决以下两类循环依赖:
构造器注入的循环依赖:因为构造器注入要求在实例化时就完成所有依赖注入,无法实现“提前暴露”
多例(Prototype)作用域的循环依赖:多例Bean每次获取都会创建新实例,无法通过缓存复用,会导致无限递归
踩分点: ①能解决的前提条件 ②两种解决不了的场景 ③各场景的原因分析
Q4:三级缓存中各级缓存分别存什么?
参考答案:
一级缓存(
singletonObjects):存放完全初始化完成的成品Bean,是最终的可用对象二级缓存(
earlySingletonObjects):存放提前暴露的半成品Bean(已实例化但未完成属性填充和初始化)三级缓存(
singletonFactories):存放ObjectFactory对象工厂,不直接存对象,仅在调用getObject()时才生成Bean实例
踩分点: ①三个Map的准确名称 ②各级存储内容的区别 ③读取顺序:一级→二级→三级
Q5:在实际开发中如何避免循环依赖?
参考答案:
优先使用构造器注入:构造器注入在编码阶段就能发现循环依赖,强制开发者遵循单一职责
使用@Lazy延迟加载:在其中一个依赖上添加
@Lazy注解,让Spring延迟初始化该依赖重新设计模块边界:将相互依赖的公共逻辑抽取到独立的中间服务中
使用Setter/字段注入但谨慎设计:仅在确认不会引入构造器循环依赖时使用
八、结尾总结
核心知识点回顾
循环依赖定义:Bean之间相互引用形成闭环,导致容器无法正常创建
三级缓存机制:三级缓存(成品池→半成品池→工厂池)是Spring的解决方案核心
解决前提:单例作用域 + 非构造器注入(Setter/字段注入)
三级原因:兼顾循环依赖破局与AOP代理的按需生成
无法解决场景:构造器注入 + 多例Bean
重点提示
不要为了使用循环依赖而设计代码,它应该是不得已的情况,而非常规设计
面试中除了回答三级缓存的机制,更要能说清楚“为什么必须是三级”以及“二级不够的原因”
理解三级缓存的关键在于理解
ObjectFactory的延迟执行思想
下一篇预告
在后续文章中,我们将深入Spring IoC容器的Bean生命周期全流程,从源码级别剖析实例化、属性填充、初始化、销毁的完整链路,并结合购物助手AI场景下的实际生产案例,帮助你真正吃透Spring核心源码。