0

0

Spring 如何解决循环依赖问题?

betcha

betcha

发布时间:2025-09-03 21:04:01

|

696人浏览过

|

来源于php中文网

原创

Spring通过三级缓存机制解决单例Bean的循环依赖问题,核心在于提前暴露“半成品”对象。当Bean A依赖Bean B,而Bean B又依赖A时,Spring在A实例化后将其ObjectFactory放入三级缓存(singletonFactories),B在创建过程中通过该工厂获取A的原始或代理实例,完成自身初始化并放入一级缓存(singletonObjects),随后A再注入已初始化的B,最终双方都完成创建。此机制依赖Bean生命周期的分阶段处理:实例化→放入三级缓存→属性填充→初始化→升级至一级缓存。二级缓存(earlySingletonObjects)用于存放从三级缓存中取出的早期暴露对象,避免重复创建代理。该方案仅适用于单例作用域下的setter或字段注入,无法解决构造器注入的循环依赖,因为构造器要求所有依赖在实例化时即就绪,无法提前暴露半成品对象。构造器注入导致的循环依赖会直接抛出BeanCurrentlyInCreationException,这实则是设计警示,提示模块耦合过紧。尽管Spring能处理setter循环依赖,但应视为代码“异味”:它增加理解与测试难度,可能导致@PostConstruct中访问未初始化Bean引发运行时异常,且不利于重构。最佳实践包括:优先使用构造器注入以暴露设计问题;拆分职责、引入接口或门面模式解耦;采用事件驱动

spring 如何解决循环依赖问题?

Spring解决循环依赖的核心在于其三级缓存机制,结合提前暴露的单例对象,打破了对象创建的僵局。简单来说,当一个Bean A依赖Bean B,同时Bean B又依赖Bean A时,Spring会在Bean A实例化但未完全初始化(属性填充)时,将其“半成品”状态提前放入一个缓存中,供Bean B引用,从而允许Bean B完成初始化,最终Bean A也能顺利完成初始化。

要深入理解Spring如何优雅地处理循环依赖,我们得把目光投向它内部的Bean生命周期管理和那套精妙的“三级缓存”机制。这东西听起来有点玄乎,但实际上它解决的是一个经典的两难问题:当两个或多个Bean互相引用时,到底谁先完全准备好?

想象一下,我们有两个单例Bean:

ServiceA
依赖
ServiceB
,而
ServiceB
又依赖
ServiceA

  1. 实例化
    ServiceA
    Spring容器开始创建
    ServiceA
    。它首先调用构造器实例化
    ServiceA
    ,此时
    ServiceA
    只是一个“裸”对象,其依赖的
    ServiceB
    还没有被注入。
  2. 提前暴露
    ServiceA
    (一级缓存 -> 三级缓存):
    实例化后的
    ServiceA
    会被立即放入一个三级缓存(
    singletonFactories
    ,存储的是一个
    ObjectFactory
    ,可以生产未完全初始化的Bean)。这个
    ObjectFactory
    至关重要,它封装了获取
    ServiceA
    原始对象,并可能进行AOP代理的逻辑。
  3. 填充
    ServiceA
    属性,发现
    ServiceB
    依赖:
    Spring尝试为
    ServiceA
    注入属性,发现它依赖
    ServiceB
  4. 创建
    ServiceB
    容器转而去创建
    ServiceB
  5. 实例化
    ServiceB
    ServiceB
    被实例化。
  6. 提前暴露
    ServiceB
    实例化后的
    ServiceB
    同样被放入三级缓存。
  7. 填充
    ServiceB
    属性,发现
    ServiceA
    依赖:
    Spring尝试为
    ServiceB
    注入属性,发现它依赖
    ServiceA
  8. 从缓存中获取
    ServiceA
    此时,容器不会重新创建
    ServiceA
    ,而是检查它的一级、二级、三级缓存。它会在三级缓存中找到那个可以生成
    ServiceA
    ObjectFactory
    。通过这个工厂,它获取到
    ServiceA
    的“半成品”实例(原始的、未完全初始化的
    ServiceA
    对象,如果需要AOP代理,此时也会进行代理)。
  9. ServiceB
    完成初始化:
    ServiceB
    成功获取到
    ServiceA
    的引用,并完成自己的属性注入和初始化。此时,
    ServiceB
    是一个完全可用的对象,并被放入一级缓存(
    singletonObjects
    )。
  10. ServiceA
    完成初始化:
    ServiceB
    完成后,Spring回到
    ServiceA
    的初始化流程。
    ServiceA
    现在可以顺利获取到已经完全初始化的
    ServiceB
    对象,完成自己的属性注入和初始化。最终,
    ServiceA
    也被放入一级缓存。

这三级缓存具体是什么?

  • 一级缓存 (
    singletonObjects
    ):
    存放已经完全初始化并可用的单例Bean。
  • 二级缓存 (
    earlySingletonObjects
    ):
    存放已经实例化但尚未完全初始化(属性填充、AOP代理等)的单例Bean。当一个Bean被其他Bean提前引用时,它会从三级缓存提升到二级缓存。
  • 三级缓存 (
    singletonFactories
    ):
    存放一个
    ObjectFactory
    ,这个工厂可以生产出原始的、未完全初始化的Bean实例。它的存在是为了处理AOP代理。如果Bean需要被代理,那么在其他Bean引用它时,应该引用的是代理对象而不是原始对象。这个
    ObjectFactory
    就负责在需要时生成代理对象。

这种机制巧妙地利用了Bean的生命周期阶段,在Bean实例化后、属性注入前,就将其“半成品”暴露出来,从而打破了循环引用的死锁。但需要注意的是,这种机制主要针对单例Bean的setter注入和字段注入有效。对于构造器注入的循环依赖,Spring是无法解决的,因为它无法在构造器执行完成前就暴露一个“半成品”对象。

Spring Bean的构造器注入为什么会导致循环依赖异常?

这是一个很实际的问题,尤其是在提倡“构造器注入优先”的当下。答案其实很简单,也很直接:Spring的循环依赖解决机制,也就是我们前面提到的三级缓存,其核心在于Bean的“提前暴露”。这个“提前暴露”发生在一个Bean被实例化之后,但在其所有依赖被注入之前。

当使用构造器注入时,一个Bean的实例化过程本身就需要它的所有依赖都准备就绪。换句话说,

ServiceA
的构造器需要
ServiceB
的实例才能完成,而
ServiceB
的构造器又需要
ServiceA
的实例。这就形成了一个鸡生蛋、蛋生鸡的死循环,没有任何一个Bean可以在另一个Bean被完全实例化之前,提供一个“半成品”供对方引用。

CA.LA
CA.LA

第一款时尚产品在线设计平台,服装设计系统

下载

举个例子:

@Service
public class ServiceA {
    private final ServiceB serviceB;

    public ServiceA(ServiceB serviceB) { // 构造器需要ServiceB
        this.serviceB = serviceB;
    }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;

    public ServiceB(ServiceA serviceA) { // 构造器需要ServiceA
        this.serviceA = serviceA;
    }
}

Spring在尝试创建

ServiceA
时,会发现需要
ServiceB
。它会暂停
ServiceA
的创建去创建
ServiceB
。创建
ServiceB
时,又发现需要
ServiceA
。此时,
ServiceA
甚至还没有完成实例化,更别提被放入任何缓存了。它卡在了构造器这一步,根本没有机会走到“提前暴露”的阶段。所以,Spring会直接抛出
BeanCurrentlyInCreationException
或类似的异常,明确告诉你存在循环依赖。

这并不是Spring的缺陷,而是构造器注入本身的特性所决定的。它强制要求所有依赖在对象构造时就已存在,这使得它在处理循环依赖时变得无能为力。因此,在设计系统时,如果发现构造器注入导致循环依赖,这通常是一个代码设计上的“异味”,暗示着模块之间的职责划分可能不够清晰,或者耦合过于紧密,需要重新审视。

即使Spring能解决,循环依赖会带来哪些潜在问题和最佳实践?

虽然Spring能够优雅地解决大多数单例Bean的setter/field注入循环依赖,但这并不意味着我们应该忽视它。循环依赖,即使被框架解决了,也常常是代码设计中潜在问题的信号。

潜在问题:

  1. 理解难度增加: 当系统存在循环依赖时,理解各个模块之间的关系变得更加复杂。你很难清晰地画出依赖图,因为箭头总是绕来绕去,这会给新成员的加入和现有成员的维护带来不小的障碍。
  2. 测试困难: 单元测试或集成测试时,如果模块之间存在循环依赖,你可能需要同时初始化多个Bean才能进行测试,这增加了测试的复杂性。模拟(Mock)对象时也可能遇到麻烦。
  3. 运行时行为不可预测: 虽然Spring解决了循环依赖,但如果你在Bean的构造器或
    @PostConstruct
    方法中执行了依赖于其他Bean的逻辑,而那个Bean此时还处于“半成品”状态,就可能导致意想不到的
    NullPointerException
    或其他运行时错误。因为提前暴露的Bean可能还没有完全初始化。
  4. 职责不清: 很多时候,循环依赖暗示着两个或多个类承担了过于相似或交叉的职责,导致它们彼此需要对方才能正常工作。这违反了“单一职责原则”。
  5. 难以重构: 紧密的循环依赖使得代码库像一个缠绕的线团,任何一个小的改动都可能牵一发而动全身,导致重构变得异常困难和风险高。

最佳实践:

  1. 优先使用构造器注入,并以此为契机发现循环依赖: 尽管构造器注入无法解决循环依赖,但这恰恰是它的优点。当它抛出循环依赖异常时,它是在“警告”你,你的设计可能存在问题。这提供了一个重构的机会。
  2. 重新审视模块职责: 如果发现循环依赖,尝试重新思考这些Bean的职责。能否将它们拆分成更小的、职责单一的组件?或者引入一个新的协调者(Facade)来管理它们?
  3. 引入接口抽象: 有时,循环依赖是因为两个具体实现类互相引用。引入接口可以帮助解耦,让一个类依赖于另一个类的接口,而不是具体实现。
  4. 事件驱动或消息队列: 对于某些业务场景,如果两个服务需要互相通知,可以考虑引入事件发布/订阅机制或消息队列。这样,服务A完成任务后发布一个事件,服务B订阅这个事件并作出响应,避免了直接的同步调用依赖。
  5. 延迟初始化或
    ObjectProvider
    /
    Provider
    在极少数情况下,如果循环依赖确实无法避免,并且是setter注入,可以考虑使用
    @Lazy
    注解来延迟Bean的初始化,或者注入
    ObjectProvider
    (Spring)或
    Provider
    (JSR-330)来按需获取Bean实例,而不是在启动时就完全注入。但这通常是权宜之计,而不是首选方案。
  6. 避免在
    @PostConstruct
    中访问循环依赖的Bean:
    确保在
    @PostConstruct
    方法中访问的任何Bean都已完全初始化。如果存在循环依赖,

相关专题

更多
spring框架介绍
spring框架介绍

本专题整合了spring框架相关内容,想了解更多详细内容,请阅读专题下面的文章。

102

2025.08.06

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1018

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

63

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

410

2025.12.29

java数据库连接教程大全
java数据库连接教程大全

本专题整合了java数据库连接相关教程,阅读专题下面的文章了解更多详细内容。

20

2026.01.15

Java音频处理教程汇总
Java音频处理教程汇总

本专题整合了java音频处理教程大全,阅读专题下面的文章了解更多详细内容。

5

2026.01.15

windows查看wifi密码教程大全
windows查看wifi密码教程大全

本专题整合了windows查看wifi密码教程大全,阅读专题下面的文章了解更多详细内容。

26

2026.01.15

浏览器缓存清理方法汇总
浏览器缓存清理方法汇总

本专题整合了浏览器缓存清理教程汇总,阅读专题下面的文章了解更多详细内容。

2

2026.01.15

ps图片相关教程汇总
ps图片相关教程汇总

本专题整合了ps图片设置相关教程合集,阅读专题下面的文章了解更多详细内容。

7

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Spring中文手册
Spring中文手册

共0课时 | 0人学习

马士兵spring视频教程
马士兵spring视频教程

共25课时 | 9.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号