行业资讯 2025年08月6日
0 收藏 0 点赞 543 浏览 3338 个字
摘要 :

文章目录 一、生活场景中的循环依赖困境 二、Spring三级缓存结构大揭秘 三、循环依赖解决流程深度剖析 (一)场景设定 (二)详细创建流程 (三)关键步骤图解 四、为……




  • 一、生活场景中的循环依赖困境
  • 二、Spring三级缓存结构大揭秘
  • 三、循环依赖解决流程深度剖析
    • (一)场景设定
    • (二)详细创建流程
    • (三)关键步骤图解
  • 四、为何需要三级缓存
    • (一)两级缓存的弊端
    • (二)三级缓存的优势
    • (三)各级缓存的职责分工
  • 五、常见问题答疑解惑
    • (一)构造器注入为啥解决不了循环依赖
    • (二)三级缓存中的设计模式
  • 六、最佳实践与避坑要点
    • (一)尽量避免循环依赖
    • (二)调试技巧
    • (三)性能优化
  • 七、总结与思考

在Spring框架的开发过程中,循环依赖问题常常让开发者们很头疼。今天这篇文章将通过生动的案例和清晰的流程图,带你深入了解Spring的三级缓存机制,让你轻松搞懂它是如何巧妙解决循环依赖问题的。

一、生活场景中的循环依赖困境

在生活里,循环依赖的情况其实很常见。打个比方,你去公司上班,进入公司大楼需要刷工牌,可办理工牌的时候,工作人员却要求你先登记工位号;而要分配工位号,又得先出示工牌,这就陷入了一种“死循环”。

在Spring框架的开发中,也会出现类似的情况。比如说下面这段代码:

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
}

@Service 
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
}

这里ServiceA依赖ServiceB,ServiceB又依赖ServiceA,形成了循环依赖。那么,Spring是怎么处理这个棘手问题的呢?答案就在于它的三级缓存机制。

二、Spring三级缓存结构大揭秘

在深入了解Spring解决循环依赖的过程前,我们先来认识一下Spring容器内部的三级缓存结构。Spring三级缓存是如何解决循环依赖问题的?简单来说,这三级缓存就像是三个不同功能的“仓库”。

  • 一级缓存(成品库)singletonObjects,这个“仓库”存放的是已经完全初始化好的Bean,就像是生产线上已经完工的产品,可以直接使用。
  • 二级缓存(半成品库)earlySingletonObjects,存放提前暴露的原始对象,也就是还没完全组装好的“半成品”。
  • 三级缓存(对象工厂库)singletonFactories,存放生成对象的工厂,这些工厂就像是产品的生产模具,用来生产对应的对象。

三、循环依赖解决流程深度剖析

(一)场景设定

还是以ServiceA依赖ServiceB,ServiceB又依赖ServiceA为例,看看Spring具体是怎么做的。

(二)详细创建流程

  1. 开始创建ServiceA:首先,在内存里为ServiceA开辟一块空间,这就是实例化ServiceA的过程。
  2. 接着,把ServiceA的ObjectFactory(可以理解为生产ServiceA的“模具”)放到三级缓存中。
  3. 给ServiceA填充属性的时候,发现它需要ServiceB。
  4. 于是,开始创建ServiceB,同样先实例化ServiceB,再把ServiceB的ObjectFactory放入三级缓存。
  5. 给ServiceB填充属性时,又发现它需要ServiceA。
  6. 这时,从三级缓存里获取ServiceA的ObjectFactory,通过ObjectFactory.getObject()得到ServiceA的早期引用,这就好比是拿到了一个还没完全做好,但已经有了基本框架的ServiceA。
  7. 把这个早期引用的ServiceA放到二级缓存中,同时把三级缓存里对应的ServiceA的ObjectFactory清除掉。
  8. 等ServiceB完成属性注入和初始化后,就把它放入一级缓存。
  9. 最后,ServiceA继续完成剩下的属性注入和初始化工作,也被放入一级缓存。

(三)关键步骤图解

在这个过程中,ServiceA和ServiceB在三级缓存、二级缓存和一级缓存之间不断交互。简单来说,就是先在三级缓存注册ObjectFactory,当需要注入时,从三级缓存查询对应的工厂,得到ObjectFactory后执行getObject()方法,获取早期引用并转移到二级缓存,等两个Bean都完成初始化后,最终放入一级缓存。Spring三级缓存是如何解决循环依赖问题的?

四、为何需要三级缓存

(一)两级缓存的弊端

有些同学可能会问,为什么不只用两级缓存呢?我们通过一个使用AOP代理的Bean的例子来看看。假设有这样一个ServiceA:

@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    @Transactional // 需要生成代理
    public void method() {}
}

如果采用两级缓存方案,在第一次从缓存获取时就直接创建代理对象。但要是后续在初始化过程中,Bean的状态发生了变化,就会导致代理对象和原始对象的状态不一致,出现问题。

(二)三级缓存的优势

相比之下,三级缓存的优势就很明显了。它可以延迟代理对象的生成时机,保证最终放入容器的对象是完整的代理对象,避免了上述问题。

(三)各级缓存的职责分工

不同级别的缓存,职责各不相同:

  • 一级缓存:存储完整的Bean实例,在整个应用运行期间都存在,主要作用是提供最终可以使用的Bean。
  • 二级缓存:存放原始对象的早期引用,从对象创建开始,到初始化完成前存在,主要用于解决循环依赖问题。
  • 三级缓存:存放生成对象的ObjectFactory,从实例化后到放入二级缓存前存在,它的主要作用是支持AOP等需要后置处理的情况。

五、常见问题答疑解惑

(一)构造器注入为啥解决不了循环依赖

看下面这段构造器注入的代码:

// 构造器注入示例
@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

使用构造器注入时,对象在实例化阶段就必须完成依赖注入。也就是说,ServiceA实例化需要ServiceB,ServiceB实例化又需要ServiceA,双方都无法完成实例化,只能陷入死循环。

(二)三级缓存中的设计模式

在Spring的三级缓存机制中,运用了多种设计模式:

  • 工厂模式:通过ObjectFactory来延迟对象的创建,就像前面提到的“模具”,需要的时候才用它生产对象。
  • 外观模式:AbstractBeanFactory统一处理缓存,把复杂的缓存操作封装起来,对外提供统一的接口。
  • 代理模式:处理AOP等需要生成代理的情况,保证对象在使用时具备正确的代理功能。

六、最佳实践与避坑要点

(一)尽量避免循环依赖

虽然Spring能解决循环依赖,但最好还是尽量避免出现这种情况:

  • 优先使用@Autowired进行注入,而不是构造器注入。
  • 定期运行mvn dependency:analyze命令,检查项目中的依赖关系,及时发现潜在的循环依赖。
  • 对代码进行重构,引入中间层,打破循环依赖的局面。

(二)调试技巧

在开发过程中,如果想要查看缓存的状态,可以使用下面的代码:

// 查看缓存状态
DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry)context.getAutowireCapableBeanFactory();
System.out.println(\"一级缓存:\" + registry.getSingletonNames());

(三)性能优化

为了让项目运行得更高效,还可以从以下几个方面进行性能优化:

  • 合理设置Bean的作用域,根据实际需求选择合适的作用域,避免不必要的资源浪费。
  • 避免过度使用@Autowired,减少不必要的依赖注入。
  • 及时清理项目中不再需要的Bean,释放内存资源。

七、总结与思考

Spring的三级缓存机制通过提前暴露半成品对象的方式,巧妙地解决了循环依赖的难题。这种设计不仅体现了用空间换时间的优化策略,还做到了关注点分离,让每级缓存的职责都很明确,同时灵活运用了延迟加载的技术。不过,需要注意的是,三级缓存也不是万能的,它并不能解决所有的循环依赖问题。但只要我们深入理解了它的机制原理,就能在开发过程中更好地应对类似的抽象问题。

微信扫一扫

支付宝扫一扫

版权: 转载请注明出处:https://www.zuozi.net/10325.html

管理员

相关推荐
2025-08-06

文章目录 一、Reader 接口概述 1.1 什么是 Reader 接口? 1.2 Reader 与 InputStream 的区别 1.3 …

986
2025-08-06

文章目录 一、事件溯源 (一)核心概念 (二)Kafka与Golang的优势 (三)完整代码实现 二、命令…

463
2025-08-06

文章目录 一、证明GC期间执行native函数的线程仍在运行 二、native线程操作Java对象的影响及处理方…

347
2025-08-06

文章目录 一、事务基础概念 二、MyBatis事务管理机制 (一)JDBC原生事务管理(JdbcTransaction)…

455
2025-08-06

文章目录 一、SnowFlake算法核心原理 二、SnowFlake算法工作流程详解 三、SnowFlake算法的Java代码…

515
2025-08-06

文章目录 一、本地Jar包的加载操作 二、本地Class的加载方法 三、远程Jar包的加载方式 你知道Groo…

831
发表评论
暂无评论

还没有评论呢,快来抢沙发~

助力内容变现

将您的收入提升到一个新的水平

点击联系客服

在线时间:08:00-23:00

客服QQ

122325244

客服电话

400-888-8888

客服邮箱

122325244@qq.com

扫描二维码

关注微信客服号