zoukankan      html  css  js  c++  java
  • Spring源码分析(十七)循环依赖

    本文结合《Spring源码深度解析》来分析Spring 5.0.6版本的源代码。若有描述错误之处,欢迎指正。

    实例化bean是一个非常复杂的过程,而其中比较难以理解的就是对循环依赖的解决, 不管之前读者有没有循环依赖方面的研究,这里有必要先对此知识点稍作回顾。

    一. 什么是循环依赖

    循环依赖就是循环引用,就是两个或多个bean相互之间的持有对方,比如CirdeA引用 CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环。此处不是循环调用,循环调用是方法之间的环调用,如下图所示。

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

    二. Spring如何解决循环依赖

    Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类:

    public class TestA {
    
        private TestB testB;
    
        public TestA() {
        }
    
        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 TestB() {
        }
    
        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 TestC() {
        }
    
        public void c() {
            testA.a();
        }
    
        public TestA getTestA() {
            return testA;
        }
    
        public void setTestA(TestA testA) {
            this.testA = testA;
        }
    }

    在Spring中将循环依赖的处理分成了3种情况。

    1. 构造器循环依赖 

    表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。

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

    Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建bean过程中发现自己已经在“当前创建bean池”里时,将抛出BeanCurrentlylnCreationFexception异常表示循环依赖;而对于创建完毕的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>

    (2)创建测试用例。

    @Test(expected = BeanCurrentlylnCreationException.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 池”中,因为表示循环依赖,抛出BeanCurrentlylnCreationException。

     2. setter循环依赖

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

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

    具体步骤如下:

    (1)Spring容器创建单例“testA” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testA”标识符放到“当前创建bean池”,然后进行setter注人“testB”。

    (2)Spring容器创建单例“testB” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testB”标识符放到“当前创建bean池”,然后进行setter注人“testC”。

    (3)Spring容器创建单例“testC” bean,首先根据无参构造器创建bean,并暴露一个 “ObjectFactory”用于返回一个提前暴露的创建中的bean,并将“testC”标识符放到“当前创建bean池',然后进行setter注人“testA”。进行注人“testA”时由于提前暴露了 “ObjectFactory” 工厂,从而使用它返回提前暴露的创建中的bean。

    (4)最后在依赖注入“testB”和“testA”,完成setter注人。

    3. prototype范围的依赖处理

    对于“prototype”作用域bean,Spring容器无法完成依赖注人,因为Spring容器不进行缓存“prototype”作用域的bean,因此无法提前暴露一个创建中的bean。示例如下:

    (1)创建配置文件。

    <bean id = "testA" class = "com.bean.CircleA" scope = "prototype">
        <property name = "testB" ref = "testB"/>
    </bean>
    <bean id = "testB" class = "com.bean.CircleB" scope = "prototype">
        <property name = "testC" ref = "testC"/>
    </bean>
    <bean id = "testC" class = "com.bean.CircleC" scope="prototype">
        <property name = "testA" ref = "testA"/>
    </bean>

    (2)创建测试用例。

    @Test(expected = BeanCurrentlylnCreationException.class) 
    public void testCircleBySetterAndPrototype() throws Throwable { 
        try {
            ApplicationContext context =  new ClassPathXmlApplicationContext("test-prototype.xml");
            System.out.println(context.getBean("testA"));
        } catch(Exception e) {
            Throwable el = e.getCause().getCause().getCause{); 
            throw el;
        }
    }

    对于 “singleton” 作用域 bean,可以通过 “setAllowCircularReferences(false); ”来禁用循环引用。

  • 相关阅读:
    Ubuntu 系统下载
    Shell 变量自增实现方法
    shell 单引号以及双引号
    A look at WeChat security
    利用HTTP Cache来优化网站
    require.js 简洁入门
    Nginx rewrite URL examples with and without redirect address
    页面制作部分之PS切图
    Joda-Time 简介
    [官方摘要]Setup And Configuration memcached with Tomcat
  • 原文地址:https://www.cnblogs.com/warehouse/p/9382628.html
Copyright © 2011-2022 走看看