> 文章列表 > java与Spring的循环依赖

java与Spring的循环依赖

java与Spring的循环依赖

java与Spring的循环依赖

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

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

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

    java与Spring的循环依赖

  • 循环依赖有什么危害
    循环依赖在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的循环依赖
    那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也无法解决,他会抛出栈溢出的异常,如下是上述代码的运行结果:
    java与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{@Autowiredprivate H h;
    }
    class H{@Autowiredprivate G g;
    }
    

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

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

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

    Spring容器启动时的输出如下,可以看到在初始化时就已经加载了,但确实是没有报错的。
    java与Spring的循环依赖

三、Spirng如何解决循环依赖

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

  • 构造器循环依赖何解
    原始构造器中的循环依赖无解,Spring也处理不了,所以我们必须得改变写法,不要在构造器中实例化对象,可以更改为使用实例变量进行DI,或者setter方法进行DI。只需要改动一个就会破了这个死循环,如下所示,是第二节中死循环的解决方法,这种我们只能通过改动代码实现了(注Spring中尽量不要使用构造器注入,很容易产生循环依赖而没有发现):
    @RestController
    public class SimpleConstructorCircular {
    }@Component
    class E{public E(){new F();}
    }
    @Component
    class F{@Autowiredprivate 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{@Autowiredprivate B b;
    }
    @Component
    class B{@Autowiredprivate A a;
    }
    

    下面是Spring利用三级缓存来解决循环依赖的的一个流程图:
    java与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{@Autowiredprivate E e;public F(){System.out.println("实例化F");}
      }
      

      亦或者是下面这样都是没有问题的

      @Component
      @DependsOn("f")
      class E{public E(){System.out.println("实例化E");new F();}
      }
      @Component()
      class F{@Autowiredprivate E e;public F(){System.out.println("实例化F");}
      }
      

四、总结

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