Spring循环依赖的解决思路

什么是循环依赖

循环依赖就是循环引用,就是两个或多个 bean 相互之间的持有对方,比如 CircleA 引用 C ircleB , CircleB 引用 CircleC, CircleC 引用 CircleA,则它们最终反映为一个环。 此处不是循环调用,循环调用是方法之间的环调用。循环调用是无法解决的 除非有终结条件 ,否则就是死循环,最终导致内存溢出错误。

Spring 如何解决循环依赖

Spring容器循环依赖包括 构造器循环依赖setter循环依赖,那 Spring容器如何解决循环依赖呢?

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

  1. 构造器循环依赖

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

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

    Spring容器将每一个正在创建的 bean标识符放在一个“当前创建 bean池”中 , bean标识符在创建过程中将一直保持在这个池中,因此如果在创建 bean 过程中发现自己已经在“当前 创建 bean池” 里时,将抛出 BeanCurrentlylnCreationException异常表示循环依赖;而对于创建 完毕的 bean 将从“ 当前创建 bean 池”中 清除掉 。

    我们通过一个直观的测试用例来进行分析 。

循环依赖配置文件—构造器注入

<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>

创建测试用例。

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

Setter循环依赖

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

     //为避免后期循环依赖,可以在 bean 初始化完成前将创建实例的 ObjectFactory 加入工厂
         addSingletonFactory(beanName, () ->
                 //对 bean再一次依赖引用,主要应用 SmartInstantiationAwareBeanPostProcessor
                 // 其中我们熟知的 AOP 就是在这里将 advice 动态织人 beaη中,若没有则直接返回bean ,不做任何处理
                 getEarlyBeanReference(beanName, mbd, bean));

具体步骤如下:

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

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

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

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

setter注入单例bean的循环依赖解决思路:

三级缓存

DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry{

 //单例对象的缓存
 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

 //单例对象工厂的缓存
 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

 //提前曝光的单例对象的缓存
 private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
  //以上三个缓存就是所谓的三级缓存
  
  //缓存正在创建的 bean
  private final Set<String> singletonsCurrentlyInCreation =
         Collections.newSetFromMap(new ConcurrentHashMap<>(16));
  @Nullable
 protected Object getSingleton(String beanName, boolean allowEarlyReference) {
     //单例对象的缓存中是否存在实例
     Object singletonObject = this.singletonObjects.get(beanName);
     // 当前创建的bean不在缓存中且当前bean正在被创建 
    //创建A时,需要注入B ,先去创建B,B又需要创建A 此时去创建A A就不在缓存中且正在创建中
     if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
         //创建bean,锁定缓存单利的对象集合
         synchronized (this.singletonObjects) {
             //创建前检查提早曝光的 单例bean中是否存在当前要创建的bean
        //如果在提前曝光的单利bean缓存中,则说明当前bean正在被创建中,不处理
             singletonObject = this.earlySingletonObjects.get(beanName);
        //如果不存在,且允许提前引用(是否允许从singletonFactories中通过getObject拿到对象)
             if (singletonObject == null && allowEarlyReference) {
                 //创建前再检查 单例bean的工厂缓存中是否存在当前bean的工厂缓存
                 //当某些方法需要提前初始化的时候则会调用 addSingletonFactory方法将对应的ObjectFactory 初始化策略缓存在singletonFactories
                 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
          //如果当前单例bean的工厂已经被缓存则直接使用工厂 获取单例bean
                 if (singletonFactory != null) {
                     //调用预先设定的getObject()方法获取单例bean
                     singletonObject = singletonFactory.getObject();
                     //获取到后将之前的三级缓存提升为二级缓存
            //添加到二级缓存中
                     this.earlySingletonObjects.put(beanName, singletonObject);
            //从三级缓存中移除
                     this.singletonFactories.remove(beanName);
                 }
             }
         }
     }
     return singletonObject;
 }
}

prototype 范围的依赖处理

//缓存正在创建中的 原型 bean--原型bean不支持循环依赖,如果循环依赖则直接报错就是基于该缓存实现的
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
 //自己注入自己,或者自己注入的bean中又包含自己
 Object curVal = this.prototypesCurrentlyInCreation.get();
 return (curVal != null &&
         (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}

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

  1. 创建配置文件

    <bean id="testA" class="com.bean.CircleA" scope="prototype">
       <property name=”tests” ref=”tests”/> 
    </bean>
    <bean id="testB" class="com.bean.Circles" scope="prototype"> 
      <property name="testC" ref="testC"/>
    </bean>
    <bean id="testC" class="com.bean.CircleC" scope="prototype">
      <property name="testA" ref="testA"/> 
    </bean>
    
  1. 创建测试用例

    @Test(expected = BeanCurrentlyinCreationException.class )
    public void testCircleBySetterAndPrototype() throws Throwable (
    try {
    ClassPathXmlApplicationContext ctx =new ClassPathXmlApplicationContext(
    "testPrototype.xml");
    System.out.println(ctx.getBean("testA"));
    }catch (Exception e) {
    Throwable el = e.getCause().getCause ().getCause(); 
      throw el;
    }
    }
    

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352