SpringBoot中循环依赖问题由浅入深看源码

    科技2022-08-08  97

    Spring在启动类启动的时候,就会自动扫描所有加了对应注解的类,然后把它们通过反射,调用默认的构造方法以单例模式创建出来,然后存放在Map容器中。这个是传说中的IOC。
    但是假设现在有这种情况:
    @Component public class A { public B b = new B(); public A(){ System.out.println("Bean A 的实例化依赖了 B:"+b); } } @Component public class B { public A a = new A(); public B(){ System.out.println("Bean B 的实例化依赖了 A:"+a); } }
    启动会发生什么?
    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'a' defined in file [/Users/jojo/jojo.springcloud/cloud-purchase/target/classes/com/jojo/cloud/pojo/A.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.jojo.cloud.pojo.A]: Constructor threw exception; nested exception is java.lang.StackOverflowError at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1320) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1214) ~[spring-beans-5.2.2.RELEASE.jar:5.2.2.RELEASE] ......... Caused by: java.lang.StackOverflowError: null at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na] at com.jojo.cloud.pojo.A.<init>(A.java:7) ~[classes/:na] at com.jojo.cloud.pojo.B.<init>(B.java:7) ~[classes/:na]
    这个就是两个Bean之间循环依赖导致的栈溢出,原因是IOC创建 A的时候,发现需要new B ,于是又去创建B,创建B的时候,发现又需要A…

    如何快速解决?

    基于JVM类加载的基础,可以知道类加载过程中,实例化分配内存地址的时候,不一定要初始化赋值,所以从这个方向入手,我们采用懒汉式创建,改动如下:
    @Component public class B { public A a ; public B(){ System.out.println("Bean B 的实例化依赖了 A:"+a); } } @Component public class A { public B b ; public A(){ System.out.println("Bean A 的实例化依赖了 B:"+b); } }
    启动成功后可以看到调用默认的无餐构造,直接打印出:
    2020-10-05 12:22:44.749 WARN 64539 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : Unable to start LiveReload server Bean A 的实例化依赖了 B:null Bean B 的实例化依赖了 A:null 2020-10-05 12:22:45.475 WARN 64539 --- [ restartedMain] c.n.c.sources.URLConfigurationSource : No URLs will be polled as dynamic configuration sources.
    对于null是因为还没初始化。在要使用的时候,再new A () ;这就是懒汉式。

    这样确实能快速解决循环依赖的问题,但是这样就破坏了单例模式,一定程度上违背了Spring IOC 的原则。

    网上有的博客说循环依赖问题可以加@Lazy解决,这个…我的理解是@Lazy是启动时候不加载,所以才不报错,用到对应的Bean的时候才构造出来放入IOC容器,但是它还是没办法解决循环依赖问题啊,创建的时候一样会依赖死循环,除非也是改成以上这种懒汉式。有大神骚操作的讲解下谢谢。
    以上代码都是我们DIY的POJO,那么如果是Spring,有两个Controller互相依赖,它会报错吗?会强制要求我们自己处理吗?

    很明显发现不会,并且跟我们处理方式一样,都是null,这个时候又有新疑问,既然是未初始化,假如要访问其中的方法或者变量,是否访问不到:

    结果当然是可以的,Spring是允许,并且支持循环依赖的!这个时候,任谁都会忽然心生好奇,为何这么神奇,Spring底层做了什么,能完美解决循环依赖这个问题?它的具体操作又是怎么样?
    Spring采用了三级缓存:

    singletonObjects:一级缓存,实例化的对象; earlySingletonObjects:二级缓存,提前曝光的对象; singletonFactories:三级缓存,实例化的对象的对象工厂。

    三个步骤:

    1:实例化 2:填充属性 3:初始化

    上图应该可以很清晰看懂源码的逻辑了。回到我们最初的问题,Spring的解决流程大概是:
    1:创建A,第一步就在三级缓存中实例化,分配内存空间,产生内存地址引用,但是这个时候还是null。
    2:帮A进行填充属性,发现有需要B,于是去容器中找B的引用。
    3:当然找不到B,所以要创建B。
    4:创建B跟创建A步骤一样,先实例化,分配内存,保存引用。
    5:发现填充B需要A,于是回去容器找A。
    6:一级缓存没有,二级缓存没有,三级缓存找到了A的引用,于是返回(此时的A还是null,但是总比没有好),同时把A移到二级缓存中,此时B的引用以及找到并且可以给到A,所以这个没问题。
    7:B拿着A的引用,首先成功初始化。然后返回。
    8:A获得初始化成功的B,也成功初始化。
    9:至此全流程结束,循环引用问题完美解决~
    以上就是我的个人总结,希望大佬们热情洋溢发表自己的意见,提出问题,谢谢~
    Processed: 0.025, SQL: 9