首页 > Java > java教程 > 正文

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

betcha
发布: 2025-09-03 21:04:01
原创
688人浏览过
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被完全实例化之前,提供一个“半成品”供对方引用。

AI建筑知识问答
AI建筑知识问答

用人工智能ChatGPT帮你解答所有建筑问题

AI建筑知识问答 22
查看详情 AI建筑知识问答

举个例子:

@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 如何解决循环依赖问题?的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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