Spring循环依赖的终极解密:三级缓存如何拯救Bean创建死锁

Spring循环依赖的终极解密:三级缓存如何拯救Bean创建死锁

引子:一场让人头疼的"鸡生蛋"困局

还记得那个让所有Java新手抓狂的问题吗?UserService依赖OrderService,OrderService又依赖UserService,就像两个人互相等对方先开口说话一样尴尬。

那天下午,小李兴冲冲地写完代码,准备展示他的"完美设计":

java 复制代码
@Service
public class UserService {
    @Autowired
    private OrderService orderService;
    // ...业务逻辑
}

@Service
public class OrderService {
    @Autowired
    private UserService userService;
    // ...业务逻辑
}

结果启动项目时,Spring直接给了他一个"循环依赖"的大红叉。就像两个人同时伸手要握手,结果撞在了一起。

探索:Spring容器的"智慧"登场

好奇心驱使下,我们开始探索Spring是如何解决这个看似无解的问题的。原来,Spring容器就像一个聪明的调解员,它设计了一套"三级缓存"机制。

想象一下,这就像一个三层的仓库系统:

缓存级别 名称 作用
一级缓存 singletonObjects 存放完全初始化好的Bean
二级缓存 earlySingletonObjects 存放提前暴露的Bean
三级缓存 singletonFactories 存放Bean工厂

转折:深入缓存背后的"魔法"

刚开始我以为很简单,不就是分层存储吗?直到我看到Spring源码中的关键方法:

java 复制代码
protected 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) {
            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
            if (singletonFactory != null) {
                singletonObject = singletonFactory.getObject();
                // ...缓存转移逻辑
            }
        }
    }
    return singletonObject;
}

这里的逻辑简直像侦探小说一样精妙!Spring会依次从三个缓存中查找Bean,就像在三个不同的抽屉里找钥匙。

解决:循环依赖的"时空穿越"术

关键时刻到了!Spring解决循环依赖的核心思路是:提前暴露未完成的Bean实例

想象这样的场景:

  1. 创建UserService:Spring先创建一个"半成品"的UserService实例
  2. 提前暴露:把这个半成品放入三级缓存
  3. 创建OrderService:需要UserService时,从缓存中拿到那个半成品
  4. 完善注入:OrderService创建完成后,回过头完善UserService
java 复制代码
// Spring内部的Bean创建流程(简化版)
public Object createBean(String beanName) {
    // 1. 创建Bean实例(构造函数调用)
    Object bean = instantiateBean(beanName);
  
    // 2. 提前暴露到三级缓存
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean));
  
    // 3. 属性注入(这里可能触发循环依赖)
    populateBean(bean);
  
    // 4. 初始化完成
    return initializeBean(bean);
}

这就像两个工匠同时造房子,A工匠先把地基打好(创建实例),然后告诉B工匠"我的地址在这里"(提前暴露),B工匠就可以先连接管道了(依赖注入)。

踩坑瞬间:不是所有循环依赖都能救

然而,Spring的三级缓存并非万能药。我曾天真地以为所有循环依赖都能解决,直到遇到了构造函数循环依赖:

java 复制代码
@Service
public class UserService {
    private final OrderService orderService;
  
    public UserService(OrderService orderService) {  // 构造函数注入
        this.orderService = orderService;
    }
}

这种情况下,Spring直接投降了!因为构造函数必须在Bean实例化时就执行,无法"提前暴露"一个还没创建的对象。

就像你不能在房子还没开始建的时候,就告诉别人房子的地址一样。

经验启示:设计模式中的智慧

通过这次深入了解,我发现了几个宝贵经验:

1. 优先使用字段注入或Setter注入

java 复制代码
@Autowired  // 字段注入,支持循环依赖
private OrderService orderService;

2. 合理设计依赖关系

  • 避免强耦合的双向依赖
  • 考虑使用事件驱动或中介者模式
  • 适当时候引入接口抽象

3. 理解Spring的生命周期

Spring的Bean创建就像一场精心编排的舞蹈,每一步都有它的节拍和意义。

总结:化解困局的艺术

Spring的三级缓存机制,本质上是一种"延迟满足"的设计哲学。它告诉我们,有时候解决问题不是硬碰硬,而是巧妙地利用时间差和空间差。 就像人生中的很多困境一样,看似无解的循环往往可以通过改变思路和时机来化解。Spring用三级缓存教会了我们:真正的智慧不在于避免所有问题,而在于优雅地解决看似无解的问题。 下次再遇到循环依赖时,你就知道Spring容器正在幕后默默地为你运行这套精妙的"时空穿越"算法了!

本文转自渣哥zha-ge.cn 本文转自渣哥zha-ge.cn

相关推荐
Miraitowa_cheems11 小时前
LeetCode算法日记 - Day 38: 二叉树的锯齿形层序遍历、二叉树最大宽度
java·linux·运维·算法·leetcode·链表·职场和发展
稻草猫.11 小时前
Java多线程(一)
java·后端·java-ee·idea
躲在云朵里`11 小时前
Spring Scheduler定时任务实战:从零掌握任务调度
java·数据库·mybatis
Java中文社群11 小时前
炸裂:SpringAI新版发布,终于支持断线重连了!
java·后端·ai编程
哈喽姥爷12 小时前
Spring Boot--Bean的扫描和注册
java·spring boot·后端·bean的扫描和注册
problc12 小时前
Spring Boot `@Service` 互相调用全攻略:`@Autowired` vs `@Resource`
java·spring boot·后端
码熔burning12 小时前
JVM 对象创建的核心流程!
java·jvm
努力努力再努力wz12 小时前
【C++进阶系列】:万字详解红黑树(附模拟实现的源码)
java·linux·运维·c语言·开发语言·c++
毕设源码纪师姐12 小时前
计算机毕设 java 高校机房综合管控系统 基于 SSM+Vue 的高校机房管理平台 Java+MySQL 的设备与预约全流程系统
java·mysql·课程设计
渣哥13 小时前
HashMap 扩容为啥总是 2 的倍数?一场来自底层的“强迫症”探险
java