spring 源码解析番外篇之什么是循环依赖

43 阅读4分钟

什么是循环依赖

循环依赖就是循环引用,就是两个或多个 bean 相互之间的持有对方,比如 TestA 引用 TestB,TestB 引用 TestC,TestC引用 TestA,则他们最终会形成一个闭环。

循环调用是无法解决的,除非有终结的条件,否则就是死循环,最终导致内存溢出错误。

Spring 是如何解决循环依赖的

循环依赖定义

 public class TestA {
 ​
     private TestB testB;
 ​
     public void a(){
         testB.b();
     }
 ​
     public TestB getTestB() {
         return testB;
     }
 ​
     public void setTestB(TestB testB) {
         this.testB = testB;
     }
 }
 ​
 ​
 public class TestB {
 ​
     private TestC testC;
 ​
     public void b(){
         testC.c();
     }
 ​
     public TestC getTestC() {
         return testC;
     }
 ​
     public void setTestC(TestC testC) {
         this.testC = testC;
     }
 }
 ​
 public class TestC {
 ​
     private TestA testA;
 ​
     public void c(){
         testA.a();
     }
 ​
     public TestA getTestA() {
         return testA;
     }
 ​
     public void setTestA(TestA testA) {
         this.testA = testA;
     }
 }

构造器循环依赖

  • 表示通过构造器注人构成的循环依赖,此依赖是无法解决的,只能拋出 BeanCurcenlylCreationException 异常表示循环依赖

  • 如在创建 TestA类时,构造器需要 TestB 类,那将去创建 TestB,在创建 TestB 类时又发现需要 TestC 类,则又去创建TestC,最终在创建 TestC 时发现又需要 TestA,从而形成一个环,没办法创建。

  • Spring 容器将每一个正在创建的 bean 标识符放在一个当前创建 bean 池中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建 bean 过程中发现自己已经在当的创建bean 池里时,将抛出 BeanCurrentlyInCreationException 异常表示循环依赖;而对于创是完毕的bean 将从当前创建bean池中清除掉。 我们通过一个直观的测试用例来进行分析。

    1.创建配置文件。

     ‹bean id="testA" class="com.bean.TestA">
       <constructor-arg index="0" ref="testB"/>
     </bean>
     ‹bean id="testB" class="com.bean.TestB">
       <constructor-arg index="0" ref="testC"/>
     </bean>
     ‹bean id="testC" class="com.bean.TestC">
       <constructor-arg index="0" ref="testA"/>
     </bean>
    
    1. 创建测试用例。

       @Test (expected = BeanCurrentlyInCreationException.class)
       public void testCircleByConstructor () throws Throwable{
         try {
           new ClassPathXmlApplicationContext ("test.xml") ;
         } catch (Exception e) {
           //因为要在创建testC时抛出;
           Throwable el = e.getCause().getCause ().getCause();
           throw el;
         }
       }
      

针对以上代码的分析如下

  • Spring 容器创建testA bean,首先去当前创建 bean池查找是否当前 bean 正在创建,如果没发现,则继续准备其需要的构造器参数testB,并将testA标识料放到当前创建bean池
  • Spring 容器创建testB bean,首先去当前创建 bean池查找是否当前 bean 正在创建,如果没发现,则继续准备其需要的构造器参数testC,并将testB标识符放到当前创建bean池
  • Spring 容器创建testC bean,首先去当前创建 bean池查找是否当前 bean正在创建,如果没发现,则继续准备其需要的构造器参数 testA,并将testc标识符放到当前创建 bean 池
  • 到此为止 spring 容器要去创建 testA bean,发现该 bean 标识符存在当前创建 bean 池中,因为表示循环依赖,抛出 BeanCurrentlyInCreationException

setter 循环依赖

表示通过 setter 注入方式构成的循环依赖。对于 setter 注人造成的依赖是通过 Spring 容器提前暴露刚完成构造器注入但未完成其他步骤(如 setter 注入)的bean来完成的,而且只能解决单例作用域的bean 循环依赖。通过提前暴露一个单例工厂方法,从而使其他 bean 能引用到该bean,如下代码所示:

 addSingletonFactory (beanName,new ObjectFactory (){
   public Object getObject () throws BeansException {
     return getEarlyBeanReference (beanName, mbd, bean);
   }
 });  

具体步骤如下。

  • Spring 容器创建单例 testA bean,首先根据无参构造器创建 bean,并暴露一个ObjectFactory用于返回一个提前暴露一个创建中的bean,并将testA 标识符放到当前创建bean 池,然后进行 setter 注入testB。
  • Spring 容器创建单例 testB bean,首先根据无参构造器创建 bean,并暴露一个ObjectFactory用于返回一个提前暴露一个创建中的 bean,并将testB 标识符放到当前创建 bean池,然后进行 setter 注入testC。
  • Spring 容器创建单例 testC bean,首先根据无参构造器创建 bean,并暴露一个ObjectFactory用于返回一个提前暴露一个创建中的 bean,并将testC 标识符放到当前创建 bean池,然后进行 setter 注入testA。进行注入testA 时由于提前暴露了ObjectFactory工厂,从而使用它返回提前暴露一个创建中的bean。
  • 最后在依赖注入testB 和 testA,完成 setter 注入。
  • 对于单例 bean 来说,可以通过 setAllowCircularRefernces(false)来禁用循环引用

prototype 范围的依赖处理

对于 prototype作用域 bean,Spring 容器无法完成依赖注入,因为 Spring 容器不进行缓存prototype 作用域的bean,因此无法提前暴露一个创建中的 bean

如需查看相关 spring 源码解析,请查看 spring 源码解析之 doCreateBean

这里会有spring 详细解决循环依赖的源码解析 spring 源码解析之 doGetBean

附上spring 解决三级缓存的几个缓存

image-20240122213345018.png