spring 如何解决循环依赖

news/2024/5/21 23:07:39/文章来源:https://blog.csdn.net/a141210104/article/details/128026702

什么是循环依赖

A 类中有一个属性 B ,也就是说 A 依赖 B,同时 B 类中有一个属性 A, 也就是说 B 依赖 A. 他们之间的依赖关系形成了环。就是我们说的循环依赖,如下图:

循环依赖示例

public class CircularDependenciesDemo {public static void main(String[] args) {new A1();}
}class A1 {private B1 b1;public A1() {this.b1 = new B1();}
}class B1 {private A1 a1;public B1() {this.a1 = new A1();}
}

结果:

Exception in thread "main" java.lang.StackOverflowErrorat io.dc.B1.<init>(CircularDependenciesDemo.java:23)at io.dc.A1.<init>(CircularDependenciesDemo.java:16)at io.dc.B1.<init>(CircularDependenciesDemo.java:24)

如上所示,发生 栈溢出错误。下面我们试着解决这种循环依赖

解决循环依赖

public class CircularDependenciesDemo {// 实例缓存池public static Map<String, Object> existsObject = new HashMap<>();public static Object loadObject (String objectName) {if(existsObject.containsKey(objectName)) {return existsObject.get(objectName);}if("A1".equals(objectName)) {A1 a1 = new A1();// 先将空白的实例放入缓存 existsObject.put("A1",a1);// 然后将对该空白实例进行属性赋值a1.setB1((B1)loadObject("B1"));return a1;}if ("B1".equals(objectName)) {B1 b1 = new B1();existsObject.put("B1",b1);b1.setA1((A1)loadObject("A1"));return b1;}return null;}public static void main(String[] args) {A1 a1 = (A1)loadObject("A1");a1.getB1().sayHello();}
}class A1 {private B1 b1;public B1 getB1() {return b1;}public void setB1(B1 b1) {this.b1 = b1;}}class B1 {private A1 a1;public A1 getA1() {return a1;}public void setA1(A1 a1) {this.a1 = a1;}public void sayHello(){System.out.println("我是 B1 , 你好");}
}

结果:

我是 B1 , 你好

本来由构造方法来构造 A1 中的 B1. 现在分成两步:

  1. 先调用无参构造器生成一个空白实例
  2. 再调用 set 方法,为空白实例赋值

为了解决循环依赖,设置了一个实例缓存池,existsObject. 用来存放已经生成的实例。但是这个实例池会存放空白对象的状态。在多线程的情况下,会取到一个空白实例。也就是对象中的字段都是 null, 引发程序错误。我们可以再添加一层二级缓存,二级缓存中存放空白实例。一级缓存中只放完整实例。

纯净的缓存

public class CircularDependenciesDemo {// 一级缓存,只存放完整的对象public static Map<String, Object> existsObject = new HashMap<>();// 二级缓存,包含空白的对象public static Map<String, Object> blankObject = new HashMap<>();// 加载对象public static Object loadObject (String objectName) {// 先从一级缓存取if(existsObject.containsKey(objectName)) {return existsObject.get(objectName);}// 再从二级缓存取if(blankObject.containsKey(objectName)) {return blankObject.get(objectName);}if("A1".equals(objectName)) {A1 a1 = new A1();// 先将空白的实例放入二级缓存blankObject.put("A1",a1);// 然后将对该空白实例进行属性赋值a1.setB1((B1)loadObject("B1"));// 再放入一级缓存existsObject.put("A1",a1);return a1;}if ("B1".equals(objectName)) {B1 b1 = new B1();blankObject.put("B1",b1);b1.setA1((A1)loadObject("A1"));existsObject.put("B1",b1);return b1;}return null;}public static void main(String[] args) {A1 a1 = (A1)loadObject("A1");a1.getB1().sayHello();B1 b1 = (B1) loadObject("B1");b1.getA1().sayHello();}
}class A1 {private B1 b1;public B1 getB1() {return b1;}public void setB1(B1 b1) {this.b1 = b1;}public void sayHello(){System.out.println("我是 A1 , 你好");}}class B1 {private A1 a1;public A1 getA1() {return a1;}public void setA1(A1 a1) {this.a1 = a1;}public void sayHello(){System.out.println("我是 B1 , 你好");}
}

通过添加二级缓存,把空白对象和完整对象剥离了。当从一级缓存中取实例时,要么拿到的是完整对象,要么拿到的是 null, 而不会获取到空白的对象,引发错误。

但是上面的代码是有问题的,当实例未初始化完,并且在在多线程的情况下,仍然会取到不完整对象,如下:

那么单单只加二级缓存并不能解决这个并发问题,同时还要加锁:

public static Object loadObject (String objectName) {// 先从一级缓存取,因为可以确保一级缓存中只有完整的对象if(existsObject.containsKey(objectName)) {return existsObject.get(objectName);}synchronized (existsObject) {// 第二次判断一级缓存是否有,因为有可能在等待锁的时候,已经有其他线程把实例放入一级缓存中if(existsObject.containsKey(objectName)) {return existsObject.get(objectName);}// 再从二级缓存取if(blankObject.containsKey(objectName)) {return blankObject.get(objectName);}if("A1".equals(objectName)) {A1 a1 = new A1();// 先将空白的实例放入二级缓存blankObject.put("A1",a1);// 然后将对该空白实例进行属性赋值a1.setB1((B1)loadObject("B1"));// 再放入一级缓存existsObject.put("A1",a1);// 然后移除二级缓存中的 A1blankObject.remove("A1");return a1;}if ("B1".equals(objectName)) {B1 b1 = new B1();blankObject.put("B1",b1);b1.setA1((A1)loadObject("A1"));existsObject.put("B1",b1);blankObject.remove("B1");return b1;}}return null;
}

到此我们才完整的解决了循环依赖,并且保证不会取到不完整的实例.

其实 spring 也是这样来解决bean循环依赖的,不过 spring 还添加动态代理bean的功能,为了解耦,还添加了三级缓存,保证代码整洁,优雅。

spring 是如何解决循环依赖的

由于 spring 在处理循环依赖时考虑很多其他功能,代码非常复杂,为了便于展示,这里只模仿核心功能:

详细说明已在注释中

// 主类
public class MainStart {// 本应该使用 ConcurrentHashMap 类型,但是为了演示效果,确保先实例化A,使用了有序HashMapprivate static Map<String, BeanDefinition> beanDefinitionMap = new LinkedHashMap<>(256);/*** 读取bean定义,当然在spring中肯定是根据配置 动态扫描注册*/public static void loadBeanDefinitions() {RootBeanDefinition aBeanDefinition=new RootBeanDefinition(InstanceA.class);RootBeanDefinition bBeanDefinition=new RootBeanDefinition(InstanceB.class);beanDefinitionMap.put("instanceA",aBeanDefinition);beanDefinitionMap.put("instanceB",bBeanDefinition);}public static void main(String[] args) throws Exception {// 加载了BeanDefinitionloadBeanDefinitions();// 循环创建Beanfor (String key : beanDefinitionMap.keySet()){// 先创建AgetBean(key);}IApi instanceA = (IApi) getBean("instanceA");instanceA.say();}// 一级缓存public static Map<String,Object> singletonObjects=new ConcurrentHashMap<>();// 二级缓存: 为了将 成熟Bean和纯净Bean分离,避免读取到不完整得Beanpublic static Map<String,Object> earlySingletonObjects=new ConcurrentHashMap<>();// 三级缓存public static Map<String,ObjectFactory> singletonFactories=new ConcurrentHashMap<>();// 循环依赖标识public  static  Set<String> singletonsCurrennlyInCreation=new HashSet<>();// 假设A 使用了Aop @PointCut("execution(* *..InstanceA.*(..))")   要给A创建动态代理// 获取Beanpublic  static Object getBean(String beanName) throws Exception {Object singleton = getSingleton(beanName);if(singleton!=null){return singleton;}Object instanceBean = null;synchronized (singletonObjects) {// 第二次判断if(singletonObjects.containsKey(beanName)) {return singletonObjects.get(beanName);}// 标记正在创建,而不是用二级缓存是否包含 beanName 来判断if(!singletonsCurrennlyInCreation.contains(beanName)){singletonsCurrennlyInCreation.add(beanName);}// 实例化RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);// 获取 class 对象Class<?> beanClass = beanDefinition.getBeanClass();// 通过无参构造函数,构造一个空白对象instanceBean = beanClass.newInstance();// 创建动态代理// 只在循环依赖的情况下在实例化后创建proxy   判断当前是不是循环依赖Object finalInstanceBean = instanceBean;// 这是一个三级缓存,三级缓存放的不是 bean, 而是一个可以创建动态代理bean的函数。(lambda 表达式。关键词: 函数接口)// 为什么不直接创建一个代理对象放入二级缓存中,而是先把 lambda 表达式放入三级缓存,// 而后面再调用这个表达式去生成代理对象,然后放入二级缓存(getSingleton 方法)// 我认为是是为了逻辑上的统一,getSingleton方法负责创建或返回对象,而不是在这里。singletonFactories.put(beanName, () -> new JdkProxyBeanPostProcessor().getEarlyBeanReference(finalInstanceBean,beanName));// 属性赋值Field[] declaredFields = beanClass.getDeclaredFields();for (Field declaredField : declaredFields) {Autowired annotation = declaredField.getAnnotation(Autowired.class);// 说明属性上面有Autowiredif(annotation!=null){declaredField.setAccessible(true);String name = declaredField.getName();Object fileObject= getBean(name);declaredField.set(instanceBean,fileObject);}}// 由于递归完后A 还是原实例,, 所以要从二级缓存中拿到proxy 。if(earlySingletonObjects.containsKey(beanName)){instanceBean=earlySingletonObjects.get(beanName);}// 添加到一级缓存   AsingletonObjects.put(beanName,instanceBean);// remove 二级缓存和三级缓存earlySingletonObjects.remove(beanName);singletonFactories.remove(beanName);}return instanceBean;}public  static Object getSingleton(String beanName){// 先从一级缓存中拿Object bean = singletonObjects.get(beanName);synchronized (singletonObjects) {// 说明是循环依赖if(bean==null && singletonsCurrennlyInCreation.contains(beanName)){bean=earlySingletonObjects.get(beanName);// 如果二级缓存没有就从三级缓存中拿if(bean==null) {// 从三级缓存中拿ObjectFactory factory = singletonFactories.get(beanName);if (factory != null) {// 拿到动态代理// 为什么需要动态代理对象,而不是我们自己原来的bean.// 因为原始bean的代理对象扩展了功能,同时还和原始 bean 有相同的类型。因为它们继承了同一个接口// 或者 代理对象继承了原始bean. (详情可以搜索 JDK动态代理,cglib 代理)bean=factory.getObject();// 将代理对象放入二级缓存,这个代理对象仍然是空白对象// 所以这个方法如果不加锁仍然可能会返回一个空白对象earlySingletonObjects.put(beanName, bean);}}}}return bean;}}//  InstanceA 接口, 为了动态代理使用
public interface IApi {void say();
}// InstanceA 
@Component
public class InstanceA implements IApi {@Autowiredprivate InstanceB instanceB;public InstanceB getInstanceB() {return instanceB;}public void setInstanceB(InstanceB instanceB) {this.instanceB = instanceB;}public InstanceA(InstanceB instanceB) {this.instanceB = instanceB;}public InstanceA() {System.out.println("InstanceA实例化");}@Overridepublic void say() {System.out.println("I'm A");}
}// InstanceB
@Component
public class InstanceB  {@Autowiredprivate InstanceA instanceA;public InstanceA getInstanceA() {return instanceA;}public void setInstanceA(InstanceA instanceA) {this.instanceA = instanceA;}public InstanceB(InstanceA instanceA) {this.instanceA = instanceA;}public InstanceB() {System.out.println("InstanceB实例化");}}//  JDK 动态代理使用
public class JdkDynimcProxy implements InvocationHandler {private Object target;public JdkDynimcProxy(Object target) {this.target = target;}public <T> T getProxy() {return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("测试");return method.invoke(target,args);}
}// 
@Component
public class JdkProxyBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {// 假设:A 被切点命中 需要创建代理  @PointCut("execution(* *..InstanceA.*(..))")if(bean instanceof InstanceA) {JdkDynimcProxy jdkDynimcProxy = new JdkDynimcProxy(bean);return  jdkDynimcProxy.getProxy();}return bean;}
}

为什么需要三级缓存,而不是两级缓存

我认为两级缓存完全可以解决循环依赖,完全可以先创建 bean的动态代理放入二级缓存中,而不是在 getSingleton 方法中延迟调用三级缓存中的 lambda 表达式再去生成动态代理。

我认为三级缓存作用之一是为了代码解耦,逻辑统一。

spring 如何避免拿到不完整的bean

  1. spring 容器加载完成后,因为解决了循环依赖不会存在不完整的bean,
  2. 在 spring 容器加载过程中,通过加锁的方式可以避免取到不完整的bean

spring 没有解决的循环依赖

  1. 没有解决构造函数的循环依赖
    所以不建议构造函数注入方式
  2. 没有解决多例下的循环依赖
    好像也无法解决,没有必要

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_36533.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SSM基于上述环境实现简单CUDA操作

目录 1. 结构 2. 环境&#xff1a; 3. controller 4. mapper 5. service 6. serviceImpl 7. mapper.xml 8. emplist.html 9. update 1. 结构 2. 环境&#xff1a; SSM整合 Spring SprintMVC Mybatishttps://blog.csdn.net/qq_41950447/article/details/128033971 3.…

Android -- 每日一问:Activity的启动模式(launchMode)有哪些,有什么区别?

经典回答 这应该是一道很虐人的面试题&#xff0c;很多人都答不上来&#xff0c;很多人根本就没有用过。当我发现在被我面试的人中有80%的比例对它不了解时&#xff0c;我找过一些同事讨论是否还有在面试中考查这个问题的必要&#xff0c;得到的回答是“程序员何苦为难程序员”…

2020-RKT

2020-RKT&#xff1a;Relation-Aware Self-Attention for Knowledge Tracing 有代码&#xff1a;https://github.com/shalini1194/RKT 摘要 学生在解决练习的过程中获得技能&#xff0c;每一次这样的互动都对学生解决未来练习的能力有明显的影响。 这种影响表现为:1)互动中涉…

电脑c盘满了怎么清理,快速清理,用这5招

​新买的电脑没用多久&#xff0c;突然发现系统提示磁盘空间不足。点击一看&#xff0c;电脑c盘空间已经爆满变红。当出现这种情况时&#xff0c;很多电脑的运行速度会大大降低&#xff0c;甚至导致部分应用无法正常运行。那么电脑c盘满了怎么清理&#xff1f;如何释放电脑c盘空…

C语言:关键字----switch、case、default(开关语句)

C语言&#xff1a;基础开发----目录 C语言&#xff1a;关键字—32个(分类说明) 有32个关键字详细说明&#xff0c;还有跳转链接&#xff01; 一、开关语句----介绍 开关语句&#xff0c;包括以下四种关键字&#xff1a; switch&#xff1a;开关语句case&#xff1a; 开关语句…

【vim】系统剪切板、vim寄存器之间的复制粘贴操作命令?系统剪切板中的内容复制粘贴到命令行?vim文本中复制粘贴到命令行

一、系统剪切板和文本内容的复制粘贴 1.1 从系统剪切板复制粘贴到文本中 需要操作3次&#xff1a; 分别是英文双引号、一个加号或梅花号&#xff0c;最后是一个p 也即"p 或者直接使用组合键【Shift insert】 1.2 从文本复制粘贴到系统剪切板 也需要操作3次&#xff…

java EE初阶 — 计算机工作原理

文章目录1.操作系统2.操作系统的定位3.进程3.1 进程的基本了解3.2 操作系统内核是如何管理软件资源的3.3 PCB里描述了进程的哪些特征3.3.1 三个较为简单的特征3.3.2 进程的调度属性4.内存管理1.操作系统 操作系统是一个搞管理的软件。 对上要给软件提供稳定的运行环境。对下要…

基于JAVA的鲜花店商城平台【数据库设计、源码、开题报告】

数据库脚本下载地址&#xff1a; https://download.csdn.net/download/itrjxxs_com/86427660 摘要 在互联网不断发展的时代之下&#xff0c;鲜花软件可以为鲜花企业带来更多的发展机会&#xff0c;让企业可以挖掘到更多的潜在用户&#xff0c;同时结合企业的优势就能够为用户…

Swin Transformer目标检测实验——环境配置的步骤和避坑

Swin Transformer1. 网上基础教程&#xff08;带视频讲解&#xff09;2. 配置虚拟环境时遇到的一些问题&#xff08;按操作顺序排列&#xff09;1. 网上基础教程&#xff08;带视频讲解&#xff09; 大家是不是都从b站来的呀&#xff0c;先给你们基础环境的配置和搭配的视频教…

黑马点评--Redis消息队列

Redis消息队列 Redis消息队列实现异步秒杀 消息队列&#xff08;Message Queue&#xff09;&#xff0c;字面意思就是存放消息的队列。最简单的消息队列模型包括3个角色&#xff1a; 消息队列&#xff1a;存储和管理消息&#xff0c;也被称为消息代理&#xff08;Message Br…

【附源码】计算机毕业设计JAVA疫情下的居民管理系统

【附源码】计算机毕业设计JAVA疫情下的居民管理系统 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA…

蒙泰转债上市价格预测

蒙泰转债基本信息转债名称&#xff1a;蒙泰转债&#xff0c;评级&#xff1a;A&#xff0c;发行规模&#xff1a;3.0亿元。正股名称&#xff1a;蒙泰高新&#xff0c;今日收盘价&#xff1a;31.3&#xff0c;转股价格&#xff1a;26.15。当前转股价值 转债面值 / 转股价格 * 正…

有没有把语音转为文字的软件?这几个转换软件你值得收藏

我们在日常的工作和生活中&#xff0c;应该经常会遇到需要将音频转换成文字的情况吧。相信大部分的小伙伴都会选择直接使用转换软件进行音频转文字的操作&#xff0c;但在使用的过程中就会发现&#xff0c;有些软件会在使用次数、音频时长上面有所限制&#xff0c;导致我们会转…

《从零开始:机器学习的数学原理和算法实践》chap1

《从零开始&#xff1a;机器学习的数学原理和算法实践》chap1 学习笔记 文章目录《从零开始&#xff1a;机器学习的数学原理和算法实践》chap1 学习笔记chap1 补基础&#xff1a;不怕学不懂微积分1.1 深入理解导数的本质直观理解复合函数求导1.2 理解多元函数偏导1.3 理解微积分…

【附源码】计算机毕业设计JAVA疫情下智慧社区系统

【附源码】计算机毕业设计JAVA疫情下智慧社区系统 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA …

CorelDRAW2023最新版矢量设计软件

CorelDRAW2023最新版是我比较用的比较好的一款软件&#xff0c;因为其作为一款优秀的矢量设计软件&#xff0c;兼具功能和性能&#xff0c;它是由Corel公司出品的矢量设计工具&#xff0c;被广泛应用于排版印刷、矢量图形编辑、网页设计等行业。CDR软件的优势在于&#xff1a;易…

studio3T import a SQL Database to Mongodb(从mysql中导入数据到mongodb)

具体参考studio3T官方文档&#xff1a;Import a SQL Database to MongoDB in 5 Steps | Studio 3T 1、打开SQL Migration-->选择SQL to MongoDB Migration 2、创建源数据库的连接&#xff08;本文源数据库是mysql&#xff09; 3、选择目标数据库 默认选择当前连接的数据库…

深度学习入门(6)误差反向传播基础---计算图与链式法则

在我的第三篇博文《深度学习入门&#xff08;3&#xff09;神经网络参数梯度的计算方式》中详细介绍了通过微分方式计算神经网络权重参数的梯度。但是数值微分的方式计算梯度效率较低。后续博文会介绍另外一种更加高效的梯度计算方式---误差的反向传播。 这篇文章介绍的是误差…

新知实验室 腾讯云实时音视频 RTC WEB端初识

这里写目录标题前言初识产品产品介绍基础功能高级功能扩展功能快速上手位置创建源码下载源码文档写入密钥使用调试区域前言 当前时代是信息行业飞速发展的时代&#xff0c;万物都在朝物联网方向转化。而人作为一个意识体&#xff0c;也正在通过互联网&#xff0c;认识一个全新…

Design Compiler工具学习笔记(6)

目录 引言 知识储备 实际操作 设计源码 仿真源码 VCS执行仿真 DC 综合 引言 本篇继续学习 DC的基本使用。本篇主要学习 DC 综合之后的效果分析&#xff0c;重点在时序分析。 前文链接&#xff1a; Design Compiler工具学习笔记&#xff08;1&#xff09; Design Comp…