java与Spring的循环依赖

java与Spring的循环依赖

  • 一、循环依赖是什么有什么危害
  • 二、循环依赖在Spring中的体现和类型
  • 三、Spirng如何解决循环依赖
  • 四、总结

一、循环依赖是什么有什么危害

  • 什么是循环依赖
    java中循环依赖用一张图来说就是下图:在对象的创建过程中多个对象形成了依赖闭环,导致了一个死循环。最少两个对象之间就可以形成循环依赖,最多则没有限制,下图是举了一个2个对象和三个对象的例子。

    在这里插入图片描述

  • 循环依赖有什么危害
    循环依赖在java中肯定会造成栈溢出。下面是循环依赖的一个简单代码,做个示例

    public class SimpleCircular {
        public static void main(String[] args) {
            A a = new A();
        }
    
    
    }
    class A{
        private B b;
        public A(){
            System.out.println("开始初始化A");
            this.b = new B();
        }
    }
    class B{
        private A a;
        public B(){
            System.out.println("开始初始化B");
            this.a = new A();
        }
    }
    

    当我们执行main方法时,会尝试初始化A的实例化对象,A的构造器又尝试初始化B,同时B的构造器又尝试初始化A,这样就形成了一个闭环形成一个无线循环的死循环,可以设想下这种场景会发生什么?当一次次重复调用构造器时,相当于是在调用一个个方法,方法的调用本质上对应的是虚拟机栈的入栈和出栈过程,现在只有无限的入栈而没有出栈势必会发生栈溢出的问题,执行main方法后的结果如下所示
    在这里插入图片描述
    那java原始代码还有别的循环依赖吗,自然是有的,比如静态变量的初始化也可以进行相互依赖,也是可以形成循环依赖的,实例变量在借助构造器进行初始化时也是可以形成循环依赖的。

二、循环依赖在Spring中的体现和类型

上面介绍了什么是循环依赖,下面需要说说本文的重点了Spring的循环依赖。Sping的循环依赖有哪些呢?首先明确一点Spring之所以有循环依赖,一部分是和原生一样的循环依赖,也就是构造器的循环依赖。这种无论是java还是Spring都是有的,此外因为Spring底层采用反射的方式为我们生成单例bean,在生成bean期间他不仅调用构造器进行对象创建还会对他的属性进行初始化,也就是我们说的DI,所以DI的过程Spirng也是存在循环依赖的。Spring其实可以分为三个场景会有循环依赖的可能:构造器之间的循环依赖、字段注入的循环依赖(DI)、setter方法注入的循环依赖(DI),下面分别从三个地方说下这些循环依赖。

  • 构造器循环依赖
    构造器的循环依赖其实在Spring中其实还是无解的,因为无论如何在进行对象构建时都是需要调用构造器的,即使Spring是采用反射技术来创建java对象,反射还是需要依赖构造器来进行对象的创建,所以单纯的构造器形成的循环依赖在Spring中也是无解的,下面是一个简单的实例:

    @RestController
    public class SimpleConstructorCircular {
    }
    
    @Component
    class E{
    
       public E(){
           new F();
       }
    }
    @Component
    class F{
        public F(){
            new E();
        }
    }
    

    上面的例子中,E和F形成了构造器的循环依赖,当Spring容器启动时,Spring在初始化E时调用他的构造器,就会先尝试初始化F,进行F构造时同理,这样就形成了死循环,也就是我们说的循环依赖,对于这个场景Spring也无法解决,他会抛出栈溢出的异常,如下是上述代码的运行结果:
    在这里插入图片描述
    所以这种循环依赖一旦写了项目直接会启动失败,是不是感觉自己不会写这么蠢的代码?确实很少有人会写出这种代码,但是当一个项目维护的人原来越多,后来的人对之前代码不熟悉则很可能会犯这种问题。

  • 字段注入循环依赖
    字段注入也是我们日常开发中最长使用的一种DI的方式了,这种循环依赖Spring已经替我们解决了,所以使用起来不会报错,先来说说字段注入的原理这样会更方便我们理解Spirng中是如何形成循环依赖的。当我们在一个类G中DI了H的对象,那么当Spring容器在进行IOC时通过java的反射技术便会尝试将H的对象进行传递给G,实例化H时则会尝试初始化G这样就会形成死循环。这里的循环依赖于java原生的依赖并不相同,因为对象的创建时并没有相互依赖,产生依赖的地方其实是在DI的阶段,不过此时Spring依然是利用反射技术来进行DI,所以就会有这样一个问题:Spring利用反射创建了G对象但是属性实例化失败了,同样的H也是。不过我们在日常开发中好像发现并没有类似问题产生,其实Spring已经帮我们解决了这一问题:解决方法下一节来说下。下面是一段字段注入的实例代码:

    @RestController
    public class SimpleFieldCircular {
    }
    class G{
        @Autowired
       private H h;
    }
    class H{
        @Autowired
        private G g;
    }
    

    我们日常写这种代码(实例变量注入或者叫字段注入)时并没有报错,那是因为Spring已经替我们解决了这个问题,当然具体的原理在下一小节我们来具体分析下。运行截图如下,可以看到Spring容器是启动完全ok的:
    在这里插入图片描述

  • setter方法注入循环依赖
    最后要说的这一种循环依赖就是使用setter方法进行属性注入,这种循环依赖依然不会有问题,产生的原理其实和上面的实例变量方式进行属性注入是没有区别的。但是对于这个问题的解决和第一种是有区别的,因为Spring在进行setter方法注入时,是在Bean实例化时进行的,而即使在实例化期间,也有循环依赖易软不会有循环依赖问题,具体原因下一节来详细说,下面是一个实例代码:

    
    @Component
    public class SimpleCircularBysetter {
    }
    
    @RestController
    class C{
        
        private D d;
    
        @Autowired
        public void setD(D d) {
            System.out.println("执行了C的set");
            this.d = d;
        }
    }
    @Component
    class D{
    
        private C c;
    
        @Autowired
        public void setC(C c) {
            System.out.println("执行了D的set");
            this.c = c;
        }
    }
    

    Spring容器启动时的输出如下,可以看到在初始化时就已经加载了,但确实是没有报错的。
    在这里插入图片描述

三、Spirng如何解决循环依赖

上面介绍了三种循环依赖的代码,这一节介绍下我们该如何解决Spring没有解决的循环依赖,其次介绍Spring解决的循环依赖是如何解决的。

  • 构造器循环依赖何解
    原始构造器中的循环依赖无解,Spring也处理不了,所以我们必须得改变写法,不要在构造器中实例化对象,可以更改为使用实例变量进行DI,或者setter方法进行DI。只需要改动一个就会破了这个死循环,如下所示,是第二节中死循环的解决方法,这种我们只能通过改动代码实现了(注Spring中尽量不要使用构造器注入,很容易产生循环依赖而没有发现):
    @RestController
    public class SimpleConstructorCircular {
    }
    
    @Component
    class E{
    
       public E(){
           new F();
       }
    }
    @Component
    class F{
        @Autowired
        private E e;
        public F(){
        }
    }
    
    如上,我们便解决了构造器产生的循环依赖,当然这不是唯一办法,我们还可以采用setter方法进行注入同样可以解决。
  • 字段注入的循环依赖与setter注入的循环依赖何解
    Spring利用三级缓存解决了循环依赖问题,三级缓存是什么呢?一起看下Spring是怎么定义的
    // 单例池,存放所有单例bean的地方,也叫一级缓存
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
    // 早期单例池,存放未初始化完成的单例bean,也叫二级缓存
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
    // 单例工厂,存放单例对象的工厂类,也叫三级缓存
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
    
    上面是Spring定义的三级缓存的代码,一级缓存其实就是我们说的单例池,他是一个ConcurrentHashMap,初始容量是256,他的目的就是存放初始化完成的单例对象的,二级缓存就是实例化完成初始化未完成的单例bean存放的地方,所以叫做早期的单例池,他也是一个ConcurrentHashMap,早期单例池中的对象的最终归宿仍是单例池也就是我们说的一级缓存。三级缓存什么作用呢?他的作用其实是为了解决循环依赖问题的同时来应对AOP的处理,我们知道Spring最大的亮点之一就是AOP,那他的AOP是怎么实现的呢?两种方式一种是JDK的动态代理,一种是CGLIB的动态代理。无论哪种代理我们都需要生成代理对象,如果一个对象需要AOP那放入单例池中的对象一定得是这个代理对象而不是原始对象,这里的单例工厂就是为了对象的AOP和循环依赖而存在的。假设Spring有如下循环依赖的代码:
    @Component
    class A{
        @Autowired
        private B b;
    }
    @Component
    class B{
        @Autowired
        private A a;
    }
    
    下面是Spring利用三级缓存来解决循环依赖的的一个流程图:
    在这里插入图片描述
    拆解下图中的大致过程可以是这样的(bean的创建分三大步:实例化–>属性赋值–>初始化–>其他操作):
    • 1.执行获取单例A的操作,依次从一级、二级、三级缓存中寻找单例A,发现找不到
    • 2.找不到A将A加入到singletonsCurrentlyInCreation,这是一个专门用于存放创建中对象的set,他的作用与三大缓存都很重要。
    • 3.执行createBean生成A对象,这里只是实例化了A,他的属性均为null。
    • 4.判断A是否是允许循环依赖,且单例,且在创建中,是将其加入到singletonFactories,也就是加入到三级缓存中。
    • 5.执行A创建中的填充操作populateBean(A)(属性赋值)此时就是为属性B赋值,这时B也是需要去经历上面的四个步骤,先是从三个缓存中依次查找,然后加入创建中的set,加入三级缓存等。
    • 6.B经过以上类似的步骤后也进入到三级缓存中,之后开始对B执行populateBean(B)也就是属性填充,B的属性填充就是填充A,然后又是从一级、二级、三级缓存中去寻找A,当找到三级缓存时发现A是存在的,此时Spring会判断A是不是有AOP,如果有则生成一个代理对象把他的引用交给B,如果没有就生成一个普通单例Bean把他的引用交给B,A的引用交给B之后A会进入到二级缓存(此时如果有别的对象依赖A,那就可以直接从二级缓存中获取了,这也是早期单例池名称的由来),当B的属性赋值结束,其他操作也结束后B会直接进入一级缓存,并从创建中的set删除自己(B)。
    • 7.当B初始化完成后又会回到第5步,因为最开始循环依赖开始的地方就是A对象的属性赋值时开始的,当B初始化完成后。进入到一级缓存,此时A中的b属性就可以拿到B的引用了,就可以继续执行A的populateBean方法和之后的操作了,完成之后同样加入到一级缓存,并从创建中的set删除自己(A),同时删除二级缓存中的A。到此时循环依赖就会被解决了。
  • 循环依赖必须使用三级缓存解决吗?
    如果只是针对循环依赖这一个问题,那答案是否定的,我们完全可以使用两级缓存来解决这个问题:一级缓存仍然是单例池,二级缓存就是存放早期的对象引用,当对象创建发生循环依赖时可以直接从二级缓存中拿到依赖,这样也能解决掉循环依赖的问题。那为什么非要整个三级缓存出来呢?三级缓存其实主要是为了解决循环依赖的同时来解决AOP问题的,AOP是Spring重要的特性之一,AOP底层是通过动态代理来实现AOP的,所以当一个对象是单例时,那单例池中的对象应该是AOP产生的代理对象,而不是对象本身。使用三级缓存的意义就是为了解决当一个对象被其他对象循环依赖时,我们应该给到其他对象的是AOP代理后的代理对象而不是普通对象。回想下上面的过程。当只使用二级缓存时,其实就没了代理对象啥事了,会直接将对象的引用直接给到其他对象,那就会造成同一个对象单例池中与其他对象的引用不相同的情况。
  • Spring解决不了的循环依赖如何处理
    • 原型模(prototype)的bean循环依赖Spring解决不了
    • 异步模式的单例Bean的循环依赖Spring解决不了@Synch
    • 全部由构造器形成的循环依赖Spring解决不了
      除了这些其他循环依赖都解决了吗,有时候也会因为bean加载顺序而产生循环依赖,此时使用@Lazy注解可以解决这部分问题,该注解原理就是延迟加载,Spring加载Bean对象时,都是再容器启动时进行实例化和初始化的,当我们加了@Lazy后,当真正使用到时才会对其进行创建初始化,这样也就没了循环依赖了。
  • Spring能解决的循环依赖总结
    • 字段注入的循环依赖都可以解决
    • setter方法的循环依赖都能解决
    • setter与字段注入形成的循环依赖可以解决
    • setter与构造器形成的循环依赖可以解决
      网上看到有部分博主说setter与构造器、字段注入与构造器注入,混和使用时部分情况下会存在解决不了的情况,笔者尝试了A-B-A循环依赖时无论先加载B还是先加载A,Spring都是可以正常加载的,笔者是目前没有复现网上的说法,这里我认为setter与构造器的循环依赖、字段注入与构造器的循环依赖是都可以解决的。
      @Component
      class E{
      
          public E(){
              System.out.println("实例化E");
              new F();
      
          }
      }
      @Component()
      @DependsOn("e")
      class F{
          @Autowired
          private E e;
          public F(){
              System.out.println("实例化F");
          }
      }
      
      亦或者是下面这样都是没有问题的
      @Component
      @DependsOn("f")
      class E{
      
          public E(){
              System.out.println("实例化E");
              new F();
      
          }
      }
      @Component()
      class F{
          @Autowired
          private E e;
          public F(){
              System.out.println("实例化F");
          }
      }
      

四、总结

本文总结了java里的循环依赖,以及Spring里的循环依赖,Spring中的循环依赖更多是因为他的对象初始化过程中自身动作引起的,除了构造器的循环依赖,其他和java中循环并不相同。然后介绍了Spring是如何利用三级缓存解决了循环依赖,又介绍了为什么要用三级缓存而不是二级换粗,这里就涉及了AOP的问题了。若是没有AOP直接二级缓存完全是OK的。然后说了下Spring解决不了的循环依赖我们要如何处理,使用万能的@Lazy即可,希望这个总结能帮到路过的朋友。

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

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

相关文章

初识linux之管道

一、进程间通信的概念大家都知道&#xff0c;进程是具有独立性的&#xff0c;因为一个程序运行起来生成进程时&#xff0c;也会生成它的进程结构体&#xff0c;即PCB&#xff0c;然后然后通过进程结构体中的结构体指针找到它的虚拟地址空间&#xff0c;然后再通过它的页表映射到…

C语言——字符函数和字符串函数【详解】(一)

文章目录函数介绍1.strlen2.strcpy3. strcat4. strcmp5. strncpy6. strncat7. strncmp8. strstr函数介绍 求字符串长度 strlen 长度不受限制的字符串函数&#xff08;使用时不安全&#xff09; strcpy strcat strcmp 长度受限制的字符串函数介绍&#xff08;与长度不受限制函数…

【洛谷刷题】蓝桥杯专题突破-深度优先搜索-dfs(9)

目录 写在前面&#xff1a; 题目&#xff1a;P1025 [NOIP2001 提高组] 数的划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述&#xff1a; 输入格式&#xff1a; 输出格式&#xff1a; 输入样例&#xff1a; 输出样例&#xff1a; 解题思路&#xff1a; 代…

【数据结构】哈希表

目录 1、哈希表 1.1 哈希表的简介 1.2 降低哈希冲突率 1.3 解决哈希冲突 1.3.1 闭散列 1.3.2 开散列&#xff08;哈希桶&#xff09; 1、哈希表 1.1 哈希表的简介 假设我们目前有一组数据&#xff0c;我们要从这组数据中找到指定的 key 值&#xff0c;那么咱们目…

【Java集合面试宝典】HashMap的put流程和特性?HashMap的扩容机制?原理— day08

目录 数组和链表分别适用于什么场景&#xff0c;为什么&#xff1f; 数组 链表 List和Set的区别 List和Map、Set的区别 HashMap 、HashTable 和TreeMap有什么区别&#xff1f; hashmap的特性 HashMap和HashTable有什么区别&#xff1f;&#xff08;必会&#xff09; J…

【数据结构】树的介绍

文章目录前言树的概念及结构树的概念树的表示树在实际中的运用二叉树的概念及结构二叉树的概念现实中的二叉树特殊的二叉树二叉树的性质二叉树的储存结构顺序存储链式存储写在最后前言 &#x1f6a9;本章给大家介绍一下树。树的难度相对于前面的数据结构来说&#xff0c;又高了…

ESP32设备驱动-HDC1080温度湿度传感器驱动

HDC1080温度湿度传感器驱动 文章目录 HDC1080温度湿度传感器驱动1、HDC1080介绍2、硬件准备3、软件准备4、驱动实现1、HDC1080介绍 HDC1080 是一款集成温度传感器的数字湿度传感器,可在极低功耗下提供出色的测量精度。 HDC1080 在很宽的电源范围内工作,是一种低成本、低功耗…

“提效”|教你用ChatGPT玩数据

ChatGPT与数据分析&#xff08;二&#xff09; 上文给简单聊了一下为什么ChatGPT不能取代数据分析师&#xff0c;本文我们来深入感受一下如何让GPT帮助数据分析师“提效”。 场景一&#xff1a;SQL取数 背景&#xff1a;多数数据分析师都要用SQL语言从数据库中提取数据&#x…

ctfshow web入门 命令执行29-33

1.web29eval()函数是把所有字符串当作php代码去执行&#xff0c;这题过滤了flag,使用通配符绕过过滤应该要注意文件中没有重名的文件&#xff0c;或一部分是一样的文件payload:cecho%20nl flag.php; #官方解法&#xff0c;反引号表示执行系统命令&#xff0c;nl为linux系统命令…

springboot智慧外贸平台

053-springboot智慧外贸平台演示录像2022开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff…

干货:浅谈主数据管理项目建设思路

“主数据是数据之源&#xff0c;是数据资产管理的核心&#xff0c;是信息系统互联互通的基石&#xff0c;是信息化和数字化的重要基础。 ——《主数据管理实践白皮书》” 近期&#xff0c;国家印发《数字中国建设整体布局规划》&#xff0c;提出数字中国建设的整体框架…

I2C协议简介 Verilog实现

I2C协议 IIC 协议是三种最常用的串行通信协议&#xff08;I2C&#xff0c;SPI&#xff0c;UART&#xff09;之一&#xff0c;接口包含 SDA&#xff08;串行数据线&#xff09;和 SCL&#xff08;串行时钟线&#xff09;&#xff0c;均为双向端口。I2C 仅使用两根信号线&#xf…

Django 实现瀑布流

需求分析 现在是 "图片为王"的时代&#xff0c;在浏览一些网站时&#xff0c;经常会看到类似于这种满屏都是图片。图片大小不一&#xff0c;却按空间排列&#xff0c;就这是瀑布流布局。 以瀑布流形式布局&#xff0c;从数据库中取出图片每次取出等量&#xff08;7 …

Educational Codeforces Round 145 (Rated for Div. 2) (A~E)

Problem - B - Codeforces 思路&#xff1a; 我们选择长度后&#xff0c;其特定长度会构成一个正方形&#xff0c;因为点与点距离大于1&#xff0c;所以偶数的正方形里面只能包含偶数的正方形&#xff0c;奇数的包含奇数。计算每个长度容纳最大点数&#xff1a; 发现cnt[0]1,…

WPF毛笔字实现过程

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

Python中生产者消费者模型

Python生产者消费者模型 一、消费模式 生产者消费者模式 是Controlnet网络中特有的一种传输数据的模式。用于两个CPU之间传输数据&#xff0c;即使是不同类型同一厂家的CPU也可以通过设置来使用。 二、传输原理 类似与点对点传送&#xff0c;又略有不同&#xff0c;一个生产…

能把爬虫讲的这么透彻的,没有20年功夫还真不行【0基础也能看懂】

前言 可以说很多人学编程&#xff0c;不玩点爬虫确实少了很多意思&#xff0c;不管是业余、接私活还是职业爬虫&#xff0c;爬虫世界确实挺精彩的。 今天来给大家浅谈一下爬虫&#xff0c;目的是让准备学爬虫或者刚开始起步的小伙伴们&#xff0c;对爬虫有一个更深更全的认知…

chatGPT爆火,什么时候中国能有自己的“ChatGPT“

目录 引言 一、ChatGPT爆火 二、中国何时能有自己的"ChatGPT" 三、为什么openai可以做出chatGPT? 四、结论 引言 随着人工智能技术的不断发展&#xff0c;自然语言处理技术也逐渐成为了研究的热点之一。其中&#xff0c;ChatGPT作为一项领先的自然语言处理技术…

【软件测试】基础知识第一篇

文章目录一. 什么是软件测试二. 测试和调试的区别三. 什么是测试用例四. 软件的生命周期五. 软件测试的生命周期一. 什么是软件测试 软件测试就是验证软件产品特性是否满足用户的需求。 那需求又是什么呢&#xff1f;在多数软件公司&#xff0c;会有两种需求&#xff0c;一种…

【vue3】小小入门介绍

⭐【前言】 首先&#xff0c;恭喜你打开了一个系统化的学习专栏&#xff0c;在这个vue专栏中&#xff0c;大家可以根据博主发布文章的时间顺序进行一个学习。博主vue专栏指南在这&#xff1a;vue专栏的学习指南 &#x1f973;博主&#xff1a;初映CY的前说(前端领域) &#x1f…
最新文章