当前位置: 首页 > news >正文

Spring 源码解读:解决循环依赖的三种方式


引言

在复杂的应用开发中,循环依赖是一个常见的问题。简单来说,循环依赖是指两个或多个Bean之间互相依赖,导致程序无法正常实例化这些Bean。Spring容器通过依赖注入(DI)来管理Bean的创建与生命周期,并在遇到循环依赖时采取了多种策略进行处理。本篇文章将带你实现三种解决循环依赖的方式,包括构造函数注入、Setter注入和ObjectFactory方式,并对比Spring的循环依赖处理机制,帮助你理解不同的处理方式及其应用场景。

什么是循环依赖

循环依赖(Circular Dependency)是指两个或多个Bean互相依赖,导致它们无法正常实例化。通常情况下,Spring通过依赖注入来管理Bean的生命周期,当遇到循环依赖时,Spring必须采取额外的策略来解决这个问题。

常见的循环依赖类型

  1. 构造函数循环依赖

    • 两个Bean通过构造函数相互依赖,导致它们在实例化时陷入循环。
  2. Setter方法循环依赖

    • 两个Bean通过Setter方法相互依赖,Spring可以通过提前暴露Bean的部分引用来解决这个问题。
  3. ObjectFactory方式

    • 通过ObjectFactory延迟依赖注入,避免在Bean创建时立即解决依赖,允许Bean先部分创建。

手动实现三种循环依赖的解决方式

为了更好地理解循环依赖的处理方式,我们将手动实现三种常见的解决策略:构造函数注入、Setter方法注入和ObjectFactory方式。

实现构造函数注入的循环依赖

构造函数注入的循环依赖比较难解决,因为它要求所有的依赖在实例化时就已经准备好。下面我们通过一个示例展示构造函数循环依赖的问题。

示例代码
public class ServiceA {private ServiceB serviceB;// 使用构造函数注入ServiceBpublic ServiceA(ServiceB serviceB) {this.serviceB = serviceB; // ServiceA依赖于ServiceB}public void doSomething() {System.out.println("ServiceA is doing something...");}
}public class ServiceB {private ServiceA serviceA;// 使用构造函数注入ServiceApublic ServiceB(ServiceA serviceA) {this.serviceA = serviceA; // ServiceB依赖于ServiceA}public void doSomething() {System.out.println("ServiceB is doing something...");}
}public class ConstructorInjectionTest {public static void main(String[] args) {// 手动实例化构造函数依赖会导致循环依赖问题// ServiceA serviceA = new ServiceA(new ServiceB(serviceA)); // 这会导致无限递归,无法解决}
}

问题描述

  • 在上面的代码中,ServiceA通过构造函数依赖ServiceB,同时ServiceB也通过构造函数依赖ServiceA。由于构造函数注入要求在实例化时就提供依赖,因此出现了循环依赖问题,导致递归创建的错误。

解决方法:Setter方法注入

Setter方法注入的循环依赖比构造函数注入要容易解决,因为Spring可以提前暴露部分未完成的Bean引用,在Bean完全实例化前进行部分注入。

示例代码
public class ServiceA {private ServiceB serviceB;// 通过Setter方法注入ServiceBpublic void setServiceB(ServiceB serviceB) {this.serviceB = serviceB; // ServiceA依赖于ServiceB}public void doSomething() {System.out.println("ServiceA is doing something...");}
}public class ServiceB {private ServiceA serviceA;// 通过Setter方法注入ServiceApublic void setServiceA(ServiceA serviceA) {this.serviceA = serviceA; // ServiceB依赖于ServiceA}public void doSomething() {System.out.println("ServiceB is doing something...");}
}public class SetterInjectionTest {public static void main(String[] args) {// 创建实例ServiceA serviceA = new ServiceA();ServiceB serviceB = new ServiceB();// 通过Setter方法解决循环依赖serviceA.setServiceB(serviceB); // ServiceA依赖ServiceB,延迟注入serviceB.setServiceA(serviceA); // ServiceB依赖ServiceA,延迟注入// 测试方法调用serviceA.doSomething(); // 输出:ServiceA is doing something...serviceB.doSomething(); // 输出:ServiceB is doing something...}
}

解决思路

  • 使用Setter方法注入解决循环依赖问题,通过先创建空的Bean对象,再通过Setter方法进行依赖注入。Spring能够在部分Bean完成初始化时将其暴露给其他Bean,从而解决循环依赖问题。

解决方法:ObjectFactory延迟注入

ObjectFactory方式通过延迟注入来解决循环依赖问题。ObjectFactory允许Spring在需要时才创建依赖对象,从而避免在Bean初始化时立即解决依赖。

示例代码
import org.springframework.beans.factory.ObjectFactory;public class ServiceA {private ObjectFactory<ServiceB> serviceBFactory;// 使用ObjectFactory进行延迟注入public ServiceA(ObjectFactory<ServiceB> serviceBFactory) {this.serviceBFactory = serviceBFactory; // 延迟注入ServiceB}public void doSomething() {System.out.println("ServiceA is doing something...");serviceBFactory.getObject().doSomething(); // 当需要时才获取ServiceB}
}public class ServiceB {private ObjectFactory<ServiceA> serviceAFactory;// 使用ObjectFactory进行延迟注入public ServiceB(ObjectFactory<ServiceA> serviceAFactory) {this.serviceAFactory = serviceAFactory; // 延迟注入ServiceA}public void doSomething() {System.out.println("ServiceB is doing something...");serviceAFactory.getObject().doSomething(); // 当需要时才获取ServiceA}
}public class ObjectFactoryInjectionTest {public static void main(String[] args) {// 使用ObjectFactory进行延迟注入,解决循环依赖问题ObjectFactory<ServiceA> serviceAFactory = () -> new ServiceA(() -> new ServiceB(serviceAFactory));ObjectFactory<ServiceB> serviceBFactory = () -> new ServiceB(serviceAFactory);// 创建ServiceA和ServiceB的实例ServiceA serviceA = serviceAFactory.getObject();ServiceB serviceB = serviceBFactory.getObject();// 测试方法调用serviceA.doSomething(); // 输出:ServiceA is doing something... ServiceB is doing something...serviceB.doSomething(); // 输出:ServiceB is doing something... ServiceA is doing something...}
}

解决思路

  • ObjectFactory方式通过延迟创建对象的方式解决循环依赖问题。ObjectFactory允许在依赖实际使用时才实例化依赖对象,从而打破了Bean初始化时的相互依赖问题。

类图与流程图

为了更好地理解三种解决方式的工作原理,我们提供了类图和流程图。

类图
ServiceA
-ServiceB serviceB
+doSomething()
ServiceB
-ServiceA serviceA
+doSomething()
ObjectFactory<T>
+T getObject()

解释

  • ServiceAServiceB互相依赖,通过ObjectFactory延迟实例化来避免直接的循环依赖。
流程图
解决循环依赖
依赖ServiceB
ServiceB实例化
依赖ServiceA

解释

  • 使用ObjectFactory延迟注入的方式,ServiceAServiceB可以在需要时获取对方的实例,避免了直接的循环依赖问题。

Spring中的循环依赖处理机制

在Spring中,循环依赖的处理是通过多种策略实现的,主要包括三级缓存机制。Spring容器通过提前暴露未完成的Bean实例、延迟依赖注入等方式解决循环依赖。

Spring的三级缓存机制

Spring容器内部通过三级缓存来处理循环依赖问题:
1

. 一级缓存:存储完全初始化好的单例Bean。
2. 二级缓存:存储部分实例化、但尚未完成初始化的Bean。
3. 三级缓存:存储可以通过代理对象获取的Bean,用于解决复杂的循环依赖场景。

当Spring遇到循环依赖时,能够通过三级缓存中的代理对象提前暴露未完成的Bean,从而解决依赖问题。

源码解析:Spring如何解决循环依赖

Spring在DefaultSingletonBeanRegistry类中,通过addSingletonFactory()方法提前暴露创建中的Bean来解决循环依赖。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory); // 将未完成的Bean放入三级缓存this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}

对比分析:手动实现与Spring的区别

  • Spring的实现

    • Spring采用了三级缓存的机制,通过提前暴露Bean的引用来解决循环依赖。它能够处理复杂的依赖关系和代理对象。
    • 三级缓存是Spring容器处理循环依赖的核心策略,通过将未完成的Bean放入三级缓存,可以提前暴露这些Bean的引用。
  • 手动实现

    • 我们的手动实现展示了三种常见的循环依赖解决方式,虽然能够处理基本的循环依赖问题,但缺乏Spring的高级功能,如三级缓存和生命周期管理。

总结

通过实现构造函数注入、Setter方法注入和ObjectFactory延迟注入三种解决循环依赖的方式,你应该对循环依赖的解决策略有了更深入的理解。在Spring框架中,三级缓存机制是其处理循环依赖的核心策略,它能够灵活地解决复杂的依赖关系。理解这些机制,将帮助你在实际开发中更好地管理Bean的生命周期,并在需要时解决循环依赖问题。


互动与思考

你是否在项目中遇到过循环依赖问题?你更倾向于使用哪种解决策略?欢迎在评论区分享你的经验与见解!


如果你觉得这篇文章对你有帮助,请别忘了:

  • 点赞
  • 收藏 📁
  • 关注 👀

让我们一起深入学习Spring框架,成为更优秀的开发者!


相关文章:

Spring 源码解读:解决循环依赖的三种方式

引言 在复杂的应用开发中&#xff0c;循环依赖是一个常见的问题。简单来说&#xff0c;循环依赖是指两个或多个Bean之间互相依赖&#xff0c;导致程序无法正常实例化这些Bean。Spring容器通过依赖注入&#xff08;DI&#xff09;来管理Bean的创建与生命周期&#xff0c;并在遇…...

Web3 详解

1. 使用 Web3 库 Web3 是一个 JavaScript 库&#xff0c;可用于通过 RPC 通信与以太坊节点通信。 Web3 的工作方式是&#xff0c;公开已通过 RPC 启用的方法&#xff0c;这允许开发利用 Web3 库的用户界面&#xff0c;以便与部署在区块链上的合约进行交互。 一旦 Geth JavaScri…...

Spring 中依赖注入注解的区别详解

一、依赖注入的基本概念 依赖注入是一种设计模式,通过将对象的依赖以参数的形式传入类中,而不是在类中自行创建依赖对象。这样做有几个好处: 降低耦合度:类与类之间的依赖关系变得更清晰,避免了硬编码依赖。提高可测试性:通过依赖注入,可以轻松地进行单元测试,因为可以…...

PTA求一批整数中出现最多的个位数字

作者 徐镜春 单位 浙江大学 给定一批整数&#xff0c;分析每个整数的每一位数字&#xff0c;求出现次数最多的个位数字。例如给定3个整数1234、2345、3456&#xff0c;其中出现最多次数的数字是3和4&#xff0c;均出现了3次。 输入格式&#xff1a; 输入在第1行中给出正整数…...

探索国产编程工具:如何实现工作效率翻倍

在当前软件开发领域&#xff0c;国产编程工具正在迅速发展&#xff0c;它们在功能、性能以及用户体验上都有显著提升&#xff0c;以下是一些国产编程工具&#xff0c;它们可以帮助开发者提升工作效率。 智能代码编辑器 CodeGeeX&#xff1a;这是一款由清华大学和智谱AI合作开…...

秒懂:进程相关的操作

1.进程的查看 1.1创建test.cc文件&#xff0c;运行以下代码 #include <stdio.h> #include <sys/types.h> #include <unistd.h>int main() {while(1){sleep(1);} return 0;}1.2 执行以下命令 1. 运行test.cc文件 并将其最终的可执行文件命名为 test gcc t…...

PDF 软件如何帮助您编辑、转换和保护文件。

如何找到最好的 PDF 编辑器。 无论您是在为您的企业寻找更高效的 PDF 解决方案&#xff0c;还是尝试组织和编辑主文档&#xff0c;PDF 编辑器都可以在一个地方提供您需要的所有工具。市面上有很多 PDF 编辑器 — 在决定哪个最适合您时&#xff0c;请考虑这些因素。 1. 确定您的…...

蓝桥杯嵌入式国三备赛经验分享

1 学习STM32入门视频 向大家推荐一套宝藏级别的视频&#xff1a;【STM32入门教程-2023版 细致讲解 中文字幕】 如果已经比过蓝桥杯单片机或学习过单片机相关课程的同学&#xff0c;你们可以尝试不需要STM32套件进行学习。如果没有学过单片机相关课程的同学&#xff0c;可以买…...

AI编程工具合集

1. 简介 1.1. 概述 AI编程,即人工智能编程,是编写用于创建智能系统(如机器学习模型、自然语言处理应用程序等)的代码的过程。AI编程涉及使用算法和数据结构来实现能够执行任务的程序,这些任务通常需要人类智能才能完成。 AI编程的基础是计算机科学原理,包括数据结构、…...

[网络编程]通过java用TCP实现网络编程

文章目录 一. 通过java用TCP实现网络编程api介绍代码实现上述代码存在的问题 一. 通过java用TCP实现网络编程 api介绍 1. ServerSocket ServerSocket是专门给服务器用的api 构造方法: 方法: 2. Socket 不管是客⼾端还是服务端Socket&#xff0c;都是双⽅建⽴连接以后&#…...

Python(TensorFlow)和Java及C++受激发射损耗导图

&#x1f3af;要点 神经网络监督去噪预测算法聚焦荧光团和检测模拟平台伪影消除算法性能优化方法自动化多尺度囊泡动力学成像生物研究多维分析统计物距粒子概率算法 Python和MATLAB图像降噪算法 消除噪声的一种方法是将原始图像与表示低通滤波器或平滑操作的掩模进行卷积。…...

IEEE投稿模板翻译

>将这一行替换为您的稿件id号(双击此处编辑)< IEEE 期刊和会议论文的撰写准备&#xff08;2022&#xff09; 第一作者 A. 作者&#xff0c;IEEE成员&#xff0c;第二作者 B. 作者&#xff0c;第三作者 C. 作者 Jr.&#xff0c;IEEE成员 摘要—本文档为IEEE会刊、期刊和…...

log4j 1.x 日志输出线程以唯一ID的形式配置

在 Log4j 1.x 中&#xff0c;直接以线程ID&#xff08;如Java中的Thread.currentThread().getId()返回的ID&#xff09;的形式记录日志是可行的&#xff0c;但 Log4j 1.x 本身并不直接提供一个内建的、自动将每个线程ID转换为“同一时间段内唯一ID”的机制。线程ID本身在JVM的上…...

宏观学习笔记:GDP分析(二)

GDP分析&#xff08;一&#xff09;主要是介绍GDP相关的定义以及核算逻辑&#xff0c;本节主要介绍GDP的分析思路。GDP分析主要是2种方法&#xff1a;总量分析和结构分析。 1. 总量分析 1.1 数值选择 一般情况下&#xff0c;分析的对象都是 官方公布的GDP当季值。 1.2 趋势规…...

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位&#xff1a;为1时表示在内存期间被访问过&#xff0c;为0时表示未被访问&#xff1b;修改位&#xff1a;为1时表示该页面自从被装入内存后被修改过&#xff0c;为0时表示未修改过。 置换页面时&#xff0c;最先置换访问位和修改位为…...

C高级编程 第十六天(树 二叉树)

1.树 1.1结构特点 非线性结构&#xff0c;有一个直接前驱&#xff0c;但可能有多个直接后继有递归性&#xff0c;树中还有树可以为空&#xff0c;即节点个数为零 1.2相关术语 根&#xff1a;即根结点&#xff0c;没有前驱叶子&#xff1a;即终端结点&#xff0c;没有后继森…...

OpenCV结构分析与形状描述符(11)椭圆拟合函数fitEllipse()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 围绕一组2D点拟合一个椭圆。 该函数计算出一个椭圆&#xff0c;该椭圆在最小二乘意义上最好地拟合一组2D点。它返回一个内切椭圆的旋转矩形。使…...

904.水果成篮

题目 链接&#xff1a;leetcode链接 思路分析&#xff08;滑动窗口&#xff09; 读完题目&#xff0c;很明显&#xff0c;这个题目需要我们寻找一个最长子数组&#xff0c;使得这个子数组里面最多存在两种不同的数字&#xff0c;很容易联想到使用滑动窗口。 另外&#xff…...

【网络安全】漏洞挖掘之 2FA 恢复代码安全措施不当

未经许可,不得转载。 文章目录 正文正文 目标:example.com 2024年6月,我在HackerOne上参与一个私人项目时发现了一个与2FA(双因素身份验证)恢复代码管理相关的安全漏洞。该漏洞发生在用户禁用并重新启用2FA的过程中。问题在于,系统在2FA重新启用后,仍然接受此前生成的…...

指令微调与参数微调的代码实践与分析

文章目录 指令微调的实验性分析LoRA 代码实践与分析指令微调的示例代码与预训练的代码高度一致,区别主要在于指令微调数据集的构建(SFTDataset)和序列到序列损失的计算(DataCollatorForSupervisedDataset)。以下代码展示了 LLMBox 和 YuLan-Chat 中指令微调的整体训练流程…...

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度​

一、引言&#xff1a;多云环境的技术复杂性本质​​ 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时&#xff0c;​​基础设施的技术债呈现指数级积累​​。网络连接、身份认证、成本管理这三大核心挑战相互嵌套&#xff1a;跨云网络构建数据…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

2024年赣州旅游投资集团社会招聘笔试真

2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:

根据万维钢精英日课6的内容&#xff0c;使用AI&#xff08;2025&#xff09;可以参考以下方法&#xff1a; 四个洞见 模型已经比人聪明&#xff1a;以ChatGPT o3为代表的AI非常强大&#xff0c;能运用高级理论解释道理、引用最新学术论文&#xff0c;生成对顶尖科学家都有用的…...

什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南

文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...

使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度

文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...

Yolov8 目标检测蒸馏学习记录

yolov8系列模型蒸馏基本流程&#xff0c;代码下载&#xff1a;这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中&#xff0c;**知识蒸馏&#xff08;Knowledge Distillation&#xff09;**被广泛应用&#xff0c;作为提升模型…...

动态 Web 开发技术入门篇

一、HTTP 协议核心 1.1 HTTP 基础 协议全称 &#xff1a;HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09; 默认端口 &#xff1a;HTTP 使用 80 端口&#xff0c;HTTPS 使用 443 端口。 请求方法 &#xff1a; GET &#xff1a;用于获取资源&#xff0c;…...