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

相关推荐
S妖O风F18 分钟前
IDEA报JDK版本问题
java·ide·intellij-idea
Mr. Cao code21 分钟前
使用Tomcat Clustering和Redis Session Manager实现Session共享
java·linux·运维·redis·缓存·tomcat
纪莫23 分钟前
DDD领域驱动设计的理解
java·ddd领域驱动设计
山中月侣1 小时前
Java多线程编程——基础篇
java·开发语言·经验分享·笔记·学习方法
java水泥工1 小时前
Java项目:基于SpringBoot和VUE的在线拍卖系统(源码+数据库+文档)
java·vue.js·spring boot
程序员岳焱1 小时前
使用 JPype 实现 Java 与 Python 的深度交互
java·后端·python
neoooo2 小时前
JDK 新特性全景指南:从古早版本到 JDK 17 的华丽变身
java·spring boot·后端
心月狐的流火号2 小时前
深入剖析 Java NIO Selector 处理可读事件
java
王廷胡_白嫖帝2 小时前
Qt文件压缩工具项目开发教程
java·开发语言·qt
渣哥2 小时前
Java开发必看!序列化与反序列化到底有多重要?
java