合封芯片

Spring核心考点全解析:考证AI助手带你精通循环依赖与三级缓存

小编 2026-04-28 合封芯片 23 0

2026年4月9日 发布于佛山

首段

在Java后端面试与技术进阶中,Spring循环依赖(Circular Dependency) 几乎是绕不开的核心考点,但很多开发者仅停留在“三级缓存能解决循环依赖”的模糊印象层面,遇到“为什么需要三级而不是二级”“构造器注入为什么不行”等追问时往往答不上来。本文通过考证AI助手辅助完成资料梳理与内容构建,从零开始系统拆解Spring如何通过三级缓存机制优雅破解单例Bean的循环依赖难题,涵盖定义、痛点、原理、代码示例、底层支撑和高频面试题,帮助读者建立完整知识链路。

一、痛点切入:传统方式下的“鸡生蛋”死锁

1.1 一个最直观的循环依赖示例

在日常开发中,最典型的循环依赖场景是这样的:

java
复制
下载
@Service
public class OrderService {
    @Autowired
    private UserService userService;
    // 业务方法...
}

@Service
public class UserService {
    @Autowired
    private OrderService orderService;
    // 业务方法...
}

这是最常见的字段注入(Field Injection)场景。OrderService依赖UserService,UserService又依赖OrderService,形成了“你中有我、我中有你”的依赖闭环-2。如果在传统对象管理体系中,创建OrderService需要先有UserService,创建UserService又需要先有OrderService,两者互相等待,陷入死锁。Spring启动时会抛出BeanCurrentlyInCreationException异常-2

1.2 传统方式的缺陷分析

这种循环依赖问题在Spring出现早期,开发者只能通过手动调整Bean加载顺序或重构代码来规避-2。随着现代企业应用平均依赖层级已达5-7层,手动解耦越来越难以满足需求-2。据2024年Java生态调研报告显示,约23%的Spring应用开发者曾遇到过循环依赖问题-2

核心痛点总结:

  • 代码耦合高:Bean之间的双向依赖本身就是设计缺陷的信号

  • 启动失败风险:循环依赖未被妥善处理时直接抛出异常,可能引发微服务雪崩效应-2

  • 调试困难:深层循环依赖往往掩盖设计缺陷,维护成本指数级上升-2

  • 构造器注入场景完全无法解决:这恰恰是面试中的高频陷阱

正是在这样的背景下,Spring框架设计出三级缓存机制,成为解决单例Bean循环依赖问题的精妙方案。

二、核心概念讲解:什么是循环依赖

2.1 标准定义

循环依赖(Circular Dependency) ,指的是两个或多个Bean之间互相持有对方的引用,形成闭环依赖关系。最典型的场景是Bean A依赖Bean B,同时Bean B又依赖Bean A-1

2.2 生活化类比

可以把Bean创建过程类比为“拼乐高”:每个Bean的完整构建分为三个步骤——实例化(拿出零件)、属性填充(拼上配件)、初始化(完成质检)。当A拼到一半发现需要B的某个配件才能继续,于是去找B;结果B也拼到一半,发现需要A的配件。两人互相等对方先拼完,都卡住了。

循环依赖的三种形态:

依赖类型注入方式Spring是否支持原因简析
构造器循环依赖通过构造函数注入❌ 不支持实例化时就必须提供依赖,无法提前暴露
Setter循环依赖通过setter方法注入✅ 支持实例化后可先暴露半成品
字段注入循环依赖通过@Autowired字段注入✅ 支持本质上是Setter注入的变体

-3

三、关联概念讲解:三级缓存的定义与职责

3.1 标准定义

三级缓存是Spring在DefaultSingletonBeanRegistry类中维护的三个Map集合,用于分层存储不同状态的单例Bean实例-2

缓存级别源码变量名核心存储内容核心作用
一级缓存singletonObjects完全初始化完成的单例Bean供业务直接获取使用,是最终的“成品缓存”
二级缓存earlySingletonObjects提前暴露的半成品Bean避免同一Bean在循环依赖中被反复创建,起“临时占位”作用
三级缓存singletonFactoriesObjectFactory工厂对象存储“生成Bean实例的逻辑”,能在需要时动态生成代理对象

-6

3.2 类比理解:食堂打饭

一级缓存好比食堂出餐窗口——菜已做好,直接端走;二级缓存好比备餐台——菜做到一半,但已拿出来让大家知道“这道菜在做着”;三级缓存好比后厨的备料方案——暂时不决定做红烧还是清蒸,等人来点单时再决定做法。

3.3 底层原理与技术支撑

三级缓存机制底层依赖以下关键技术:

  • 反射机制(Reflection) :Spring通过反射调用构造器完成Bean的实例化

  • 动态代理(Dynamic Proxy) :为支持AOP场景(如@Transactional),三级缓存中的ObjectFactory能在需要时动态生成代理对象-8

  • ObjectFactory函数式接口:三级缓存存储的是() -> getEarlyBeanReference(...)形式的lambda表达式,实现“延迟创建代理”

四、概念关系与区别总结

4.1 三级缓存之间的协作关系

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

  • 一级缓存是最终结果存放处,不参与循环依赖“破局”

  • 二级缓存是临时存储提前暴露的Bean引用,防止重复生成

  • 三级缓存是“工厂层”,在第一次被依赖时才动态决定——是否要生成代理对象

4.2 为什么需要三级缓存?二级不够吗?

这是面试中的核心追问,答案很关键:二级缓存无法解决“循环依赖 + AOP代理”的组合场景-6

问题本质: 如果只有二级缓存,A在实例化后就必须立刻决定是否生成代理对象,但此时A的生命周期还没走到“初始化”阶段,无法判断它是否需要AOP增强(如@Transactional的切面信息)。如果先放原始Bean进二级缓存,B拿到的是原始对象;等到A后续初始化时才生成代理对象并放入一级缓存,此时B中注入的A和最终一级缓存中的A不是同一个对象,导致“代理对象不一致”问题-6

三级缓存的解决之道: 将“要不要代理”的判断延迟到第一次被其他Bean引用时才计算。三级缓存存储的不是Bean对象本身,而是一个工厂逻辑() -> getEarlyBeanReference(A),当B需要A时,执行该工厂,此时A的生命周期已足够判断是否需要代理,然后生成正确的对象放入二级缓存供B使用-6。既按需代理,又不破坏Bean的生命周期顺序-8

五、代码/流程示例演示

5.1 场景搭建:字段注入循环依赖

java
复制
下载
// ServiceA.java
@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    public void doSomething() {
        System.out.println("ServiceA is working");
    }
}

// ServiceB.java
@Component
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
    
    public void doSomething() {
        System.out.println("ServiceB is working");
    }
}

5.2 执行流程详解

核心流程三步走:

  1. 创建ServiceA:实例化 → 放入三级缓存(存ObjectFactory)→ 属性填充时发现依赖ServiceB

  2. 创建ServiceB:实例化 → 放入三级缓存 → 属性填充时发现依赖ServiceA

  3. 破解循环:从三级缓存取ServiceA的工厂 → 执行工厂生成半成品 → 放入二级缓存 → 注入ServiceB → ServiceB完成初始化 → ServiceA接着完成

-1-32

5.3 对比新旧实现

维度无三级缓存(传统方式)有三级缓存(Spring机制)
构造器循环依赖❌ 无法解决,启动抛异常❌ 仍无法解决
Setter/字段循环依赖❌ 需手动调整加载顺序✅ 自动解决
AOP代理场景❌ 代理对象不一致✅ 注入正确的代理对象
开发者感知需要大量手动配置对开发者透明

六、底层原理/技术支撑点

6.1 源码定位

三级缓存机制的核心实现在DefaultSingletonBeanRegistry类中,关键源码如下-1

java
复制
下载
// 一级缓存:存放完全初始化好的单例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));

6.2 getSingleton()方法的核心逻辑

java
复制
下载
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<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();
                this.earlySingletonObjects.put(beanName, singletonObject);
                this.singletonFactories.remove(beanName);
            }
        }
    }
    return singletonObject;
}

-1

6.3 关键技术依赖

  • 反射(Reflection) :通过反射调用构造器完成Bean实例化

  • 动态代理(Dynamic Proxy)ObjectFactory.getObject()内部调用getEarlyBeanReference(),根据是否需要AOP决定返回原始Bean还是代理对象

  • ObjectFactory函数式接口:将代理对象的创建延迟到第一次被引用时

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

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

参考答案: Spring通过三级缓存机制解决单例Bean的Setter/字段注入场景下的循环依赖。三级缓存包括:一级缓存singletonObjects存成品Bean,二级缓存earlySingletonObjects存半成品Bean,三级缓存singletonFactoriesObjectFactory工厂。当A依赖B、B依赖A时,A实例化后将自己的工厂放入三级缓存;B创建时从三级缓存获取A的工厂并生成早期引用,完成B的初始化;B完成后A再继续完成初始化,从而打破循环-12

Q2:为什么构造器注入的循环依赖无法解决?

参考答案: 因为构造器注入要求在实例化时就完成所有依赖的注入,而此时Bean还未创建完成,无法提前暴露。三级缓存机制依赖于“先实例化、再暴露半成品、后填充属性”的流程,而构造器注入将“实例化”和“依赖注入”绑定在一起,Spring没有机会将半成品提前暴露出去-3-49

Q3:为什么需要三级缓存,二级不够吗?

参考答案: 二级缓存不够。原因是AOP代理场景:如果只有二级缓存,A实例化后就必须立即决定是否生成代理对象,但此时A的生命周期还未到初始化阶段,无法判断是否需要AOP增强(如@Transactional)。三级缓存存储的是ObjectFactory工厂,将“是否生成代理”的决策延迟到第一次被其他Bean引用时才执行,此时可以正确判断并生成代理对象,保证注入的对象和最终成品一致-6

Q4:Spring能解决所有循环依赖吗?有哪些限制?

参考答案: 不能。Spring仅能解决单例作用域 + Setter注入/字段注入场景的循环依赖。以下场景无法解决:①构造器注入循环依赖;②多例(Prototype)Bean的循环依赖;③通过@Lazy注解虽可规避但并非真正解决-12-54。另外,从Spring Framework 5.3/Spring Boot 2.6开始,默认已禁用循环依赖,需要显式配置spring.main.allow-circular-references=true才能开启-12

Q5:如何在设计层面避免循环依赖?(加分项)

参考答案: 虽然Spring提供了技术解决方案,但循环依赖通常是代码设计有问题的信号。可以从设计层面解决:①提取公共接口,让双方依赖抽象而非具体实现;②使用@Lazy延迟加载作为临时方案;③分析职责边界,将双向依赖拆分为三个类,引入中间层;④使用事件驱动(ApplicationEvent)将直接调用改为发布订阅模式-12

八、结尾总结

8.1 核心知识点回顾

核心要点一句话总结
什么是循环依赖Bean之间互相持有对方引用形成的闭环
三级缓存构成singletonObjects(成品) + earlySingletonObjects(半成品) + singletonFactories(工厂)
为什么是三级解决AOP代理场景下对象一致性问题,实现“延迟决定是否代理”
适用条件单例Bean + Setter/字段注入
不适用场景构造器注入、多例Bean
底层依赖反射、动态代理、ObjectFactory

8.2 重点与易错点强调

  • 记忆口诀:一级存成品,二级存已确定,三级存工厂——等被依赖时才决定-8

  • 常见误解:❌“三级缓存是为了性能”→ ❌错误,核心是为了解决AOP代理提前暴露问题-11

  • 版本注意:Spring Boot 2.6+默认禁用循环依赖,面试时可主动提及,体现技术关注度

8.3 进阶预告

下一篇将深入Spring Bean生命周期与AOP代理的源码实现,从doCreateBeanpopulateBeaninitializeBean,彻底打通Spring IoC的核心链路,敬请期待。

猜你喜欢