细石混凝土泵

【AI有道助手】Spring循环依赖必问:三级缓存2026终极原理

小编 2026-04-29 细石混凝土泵 1 0

⏰ 发布时间:2026年4月9日 09:00

Spring循环依赖是每个后端开发者必须跨越的技术门槛——很多人只会在代码中无意识地使用@Autowired而从未思考过AI有道助手今天要剖析的这个问题:当Bean A依赖Bean B、Bean B又依赖Bean A时,Spring到底是如何“破局”的?它为什么能够自动化解这种看似无解的“死锁”,却又有一些场景会直接启动报错?本文将从痛点切入,逐层拆解三级缓存的每一个环节,通过源码分析与完整代码演示,帮助读者建立起从“会用”到“懂原理”的完整知识链路。

一、痛点切入:为什么会有循环依赖

传统手动管理的困境

在Spring出现之前,开发者需要手动管理对象之间的依赖关系。假设有两个相互依赖的服务:

java
复制
下载
// 手动管理的痛苦
public class ServiceA {
    private ServiceB b;
    public void setB(ServiceB b) { this.b = b; }
}

public class ServiceB {
    private ServiceA a;
    public void setA(ServiceA a) { this.a = a; }
}

// 客户端代码必须按特定顺序手动注入
ServiceA a = new ServiceA();
ServiceB b = new ServiceB();
a.setB(b);
b.setA(a);

这种手动方式存在明显的缺陷:耦合高(对象之间必须知道对方的创建细节)、扩展性差(新增依赖需要修改多处代码)、维护困难(循环依赖无法被自动检测和解决)。

Spring默认行为

当两个Bean互相依赖时,如果Spring不做特殊处理,项目启动会直接抛出BeanCurrentlyInCreationException异常,提示存在无法解决的循环依赖-1。而Spring的核心设计初衷恰恰是自动化解决依赖关系,因此它必须有一套机制来处理这类问题。

二、核心概念讲解:三级缓存(Three-Level Cache)

标准定义

三级缓存是Spring在DefaultSingletonBeanRegistry类中设计的一套存储机制,用于在Bean的创建过程中管理不同生命周期阶段的对象。

Spring通过三个Map来存储不同状态的Bean-1

缓存级别缓存名称存储内容作用
一级缓存singletonObjects完全初始化完成的Bean供业务直接使用(成品)
二级缓存earlySingletonObjects已实例化但未完成初始化的Bean存放提前暴露的半成品
三级缓存singletonFactoriesObjectFactory工厂对象按需生成代理或原始对象

生活化类比

假设你在工厂里生产机器人:一级缓存是成品仓库,放着可以出厂销售的完整机器人;二级缓存是半成品暂存区,放着刚搭好骨架、还没装零件的“毛坯版”;三级缓存是一张预订券,别人拿着券来找你要机器人时,你才决定是给“毛坯版”还是给“代理版”(比如给机器人装个远程操控模块)-

三、关联概念讲解:Bean生命周期(Bean Lifecycle)

标准定义

Bean生命周期是指Spring容器中一个Bean从创建到销毁的完整过程,核心阶段为:实例化 → 属性填充 → 初始化 → 使用 → 销毁-62

与三级缓存的关系

三级缓存正是嵌入在Bean生命周期的“属性填充”阶段发挥作用-62。理解两者的关系是掌握循环依赖解决逻辑的关键:

  • 实例化阶段:Spring通过反射调用构造函数创建对象实例,此时只是一个“空壳对象”-62

  • 属性填充阶段:Spring开始注入依赖属性,如果发现循环依赖,三级缓存在此介入-62

  • 初始化阶段:执行@PostConstructInitializingBean等回调,AOP代理在此之后生成-62

一句话概括

Bean生命周期定义了“什么时候做什么”,三级缓存定义了“遇到循环依赖时怎么做”。

四、概念关系与区别总结

对比维度三级缓存Bean生命周期
本质解决方案/机制过程/阶段划分
角色解决循环依赖的工具Bean创建的时间轴
关系在生命周期中发挥作用为三级缓存提供切入点

一句话记忆:Bean生命周期是“时间线”,三级缓存是“破局点”。

五、代码示例演示

场景:用户服务与订单服务相互依赖

java
复制
下载
@Service
public class UserService {
    @Autowired
    private OrderService orderService;
    
    public void createUser() {
        System.out.println("User created");
        orderService.createOrderForNewUser();
    }
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;
    
    public void createOrderForNewUser() {
        System.out.println("Order created for user");
    }
}

启动项目,一切正常! Spring通过三级缓存自动解决了这个循环依赖-40

完整解决流程演示

  1. 创建UserService:实例化 → 放入三级缓存 → 填充属性,发现需要OrderService

  2. 转而创建OrderService:实例化 → 放入三级缓存 → 填充属性,发现需要UserService

  3. 关键步骤:从三级缓存获取UserService的ObjectFactory → 调用getObject()生成早期引用 → 放入二级缓存

  4. OrderService完成初始化 → 移入一级缓存

  5. UserService继续填充 → 从一级缓存获取OrderService → 完成初始化

构造器注入为何失败?

@Autowired字段注入改为构造器注入:

java
复制
下载
@Service
public class UserService {
    private final OrderService orderService;
    public UserService(OrderService orderService) {
        this.orderService = orderService;
    }
}

@Service
public class OrderService {
    private final UserService userService;
    public OrderService(UserService userService) {
        this.userService = userService;
    }
}

启动直接报循环依赖错误-40。原因:构造器在实例化阶段就被调用,此时Bean尚未放入三级缓存,Spring无法提前暴露半成品-11

六、底层原理与技术支撑

关键源码解析

Spring处理循环依赖的核心逻辑位于DefaultSingletonBeanRegistry.getSingleton()方法中-1

java
复制
下载
public Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 第一步:从一级缓存获取
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 第二步:从二级缓存获取
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                // 第三步:从三级缓存获取ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

为什么需要三级缓存(而不是二级)?

如果只有二级缓存,Spring必须在Bean实例化后立刻决定是否生成代理对象。但此时还未执行初始化逻辑(如@PostConstruct中可能动态添加切面标记),无法判断是否需要AOP代理。三级缓存把“要不要代理”的决策延迟到第一次被其他Bean引用时计算,既解决了循环依赖,又保证了AOP的正确性-3

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

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

标准答案要点:Spring通过三级缓存机制解决单例Bean在Setter/Field注入场景下的循环依赖-41。在Bean实例化后,将ObjectFactory存入三级缓存;当其他Bean需要引用时,从三级缓存获取工厂并生成早期引用存入二级缓存,从而打破循环。构造器注入和原型Bean的循环依赖无法解决

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

标准答案要点:二级缓存可以解决循环依赖,但无法正确处理AOP代理-。三级缓存存放ObjectFactory,在需要时才通过getEarlyBeanReference决定是否生成代理对象,实现代理生成的懒加载,保证最终暴露的对象和最终单例一致。

面试题3:Spring Boot 2.6+对循环依赖有什么变化?

标准答案要点:从Spring Boot 2.6开始(Spring Framework 5.3),默认禁止了循环依赖,如果项目中存在,启动时会直接报错,需要显式设置spring.main.allow-circular-references=true才能开启-41。这一变化旨在鼓励更清晰的代码设计。

面试题4:构造器注入的循环依赖如何解决?

标准答案要点:Spring无法自动解决构造器循环依赖,因为构造器在实例化阶段执行,Bean尚未放入缓存。解决方案:①使用@Lazy延迟加载其中一侧的依赖;②改用Setter/Field注入;③重构代码设计,提取公共逻辑到第三个类-58

面试题5:循环依赖是设计问题吗?

标准答案要点:循环依赖通常是代码设计存在问题的信号,应优先从设计层面解决-41。Spring提供了技术解决方案,但“能用”不等于“该用”。推荐重构方法:提取公共接口、使用@Lazy临时方案、重新划分职责、采用事件驱动模式。

八、结尾总结

核心知识点回顾

  1. 三级缓存:一级存成品、二级存已确定的早期引用、三级存可动态生成代理的工厂

  2. 解决条件:仅限于单例Bean + Setter/Field注入场景

  3. 不可解决:构造器注入、原型Bean、多例Bean

  4. 三级优于二级:关键在于AOP代理的懒加载,决策延迟到真正需要时

  5. 版本变化:Spring Boot 2.6+默认禁用了循环依赖

重点与易错点

  • ⚠️ 构造器注入与字段注入对循环依赖的处理结果完全不同

  • ⚠️ @Lazy可以解决构造器循环依赖,但治标不治本

  • ⚠️ 三级缓存不是为了性能,而是为了兼顾AOP和循环依赖

下期预告

下一篇将深入讲解Spring AOP的底层原理,从JDK动态代理到CGLIB,从@Transactional失效场景到拦截器链的执行机制,彻底搞懂Spring是如何“偷偷”增强你的方法的。

猜你喜欢