Spring如何解决循环依赖

什么是循环依赖

关于什么是循环依赖,简单的说就是鸡生蛋和蛋生鸡的问题。例如现在有两个类定义如下:

class E {
    private F f;
    public E(F f) {
        this.f = f;
    }
    public E() {
        System.out.println("实例化E");
    }
    public F getF() {
        return f;
    }
    public void setF(F f) {
        this.f = f;
        System.out.println("为E中的F设置属性值,F中的E的值为:"+f.getE());
    }
}
class F {
    private E e;
    public F(E e) {
        this.e = e;
    }
    public F() {
        System.out.println("实例化F");
    }
    public E getE() {
        return e;
    }
    public void setE(E e) {
        this.e = e;
        System.out.println("为F中的E设置属性值,E中的F的值为:"+e.getF());
    }
}

上面有两个类E和F,E中有属性值f,而F中有属性值e。如果我们上面的代码中我们没有提供非空构造方法,那么我们在创建E时需要F的实例,而创建F时则需要E的实例。这样我们就陷入了一个死循环中。

如何解决

如果通过有参构造函数创建对象,我们是没办法实现的。但是如果提供无参构造函数,我们是可以解决的。简单的说可以分为下面几步:

  1. 使用无参构造方法创建E的实例,然后将E的实例缓存起来。
  2. 通过无参构造函数创建F的实例,然后从缓存中获取E的实例将其设置到F的实例中;或者通过有参构造函数创建F的实例,同样是从缓存中获取E的实例。
  3. 将F的实例设置到E中

上面就是我们在解决循环依赖的问题的过程。整个过程主要有两点核心内容:提供无参构造函数和缓存。

Spring如何解决

之前我们通过自己的方法解决了循环依赖的问题,其实Spring中也是使用了同样的方式,只不过Spring中需要考虑的东西比较多,过程相对我们的实现来说会复杂一些,但是整体的思路都是通过无参构造器创建实例,然后使用缓存保存实例供后续的依赖者使用。

为了更好理解Spring如何处理循环依赖,我们先了解一下Spring中的部分知识,这样有利于我们进一步理解。

Spring Bean的生命周期

简单的说Spring Bean的生命周期分为四个,分别为:实例化、属性赋值、初始化、销毁。而实际上我们比较关注的主要在前三个生命周期中。实例化简单的理解就是将这个对象new出来,而属性赋值简单的理解就是给实例的属性值设置值,而初始化就是执行实例中的初始化方法。对于这些生命周期,可以在AbstractAutowireCapableBeanFactory#doCreateBean中看出,前三个生命周期分别对应的方法为:createBeanInstance、populateBean、initializeBean。如果我们要正常使用Bean,必须是等Bean完成了初始化才能正常使用,否则我们拿到的是一个半成品Bean,在使用过程中就可能有问题了。

BeanPostProcessor

这个是Spring对Bean提供的扩展点。简单的说就是在Bean的某些生命周期中会调用该接口中的方法。例如BeanPostProcessor,它主要提供了在Bean初始化前和初始化后的方法回调。而BeanPostProcessor同样还有许多子接口,这些接口基本上都是类似,不同的点在于回调的时机不同而已。例如InstantiationAwareBeanPostProcessor它提供了Bean在实例化前后和属性赋值前的扩展,而SmartInstantiationAwareBeanPostProcessor则提供提前暴露Bean的引用的回调。

验证

关于如何实现,之前所说的都是我们的猜想,而实际上Spring是如何处理的我们看下面这个例子。

public class SingleBeanRegistryDemo2 {
    public static void main(String[] args) {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        beanFactory.addBeanPostProcessor(new MyInstantiationAwareBeanPostProcessor());
        BeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions("classpath:/spring-bean-registry-2.xml");
        E e = beanFactory.getBean(E.class);
        F f = beanFactory.getBean(F.class);
        System.out.println(e);
        System.out.println(f);
    }
}
class E {
    private F f;
    public E(F f) {
        this.f = f;
    }
    public E() {
        System.out.println("实例化E");
    }
    public F getF() {
        return f;
    }
    public void setF(F f) {
        this.f = f;
        System.out.println("为E中的F设置属性值,F中的E的值为:"+f.getE());
    }
}
class F {
    private E e;
    public F(E e) {
        this.e = e;
    }
    public F() {
        System.out.println("实例化F");
    }
    public E getE() {
        return e;
    }
    public void setE(E e) {
        this.e = e;
        System.out.println("为F中的E设置属性值,E中的F的值为:"+e.getF());
    }
}
class MyInstantiationAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        System.out.printf("%s:实例化后%s%n",beanName,bean);
        return true;
    }
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.printf("%s:初始化前%n",beanName);
        return null;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.printf("%s:初始化后%n",beanName);
        return null;
    }
    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        System.out.printf("%s:属性赋值前%n",beanName);
        return pvs;
    }
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        System.out.printf("获取提前暴露的bean引用:%s = %s%n",beanName,bean);
        return bean;
    }
}

spring-bean-registry-2.xml配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="e" class="com.buydeem.beanregistry.E">
        <property name="f" ref="f"/>
    </bean>
    <bean id="f" class="com.buydeem.beanregistry.F">
        <property name="e" ref="e"/>
    </bean>
</beans>

上面的代码比较简单,就是定义了两个Bean,在XML配置文件中他们互相依赖。同时我们还在BeanFactory中添加了SmartInstantiationAwareBeanPostProcessor的实例。最后运行的结果如下:

实例化E
e:实例化后com.buydeem.beanregistry.E@117e949d
e:属性赋值前
实例化F
f:实例化后com.buydeem.beanregistry.F@7a675056
f:属性赋值前
获取提前暴露的bean引用:e = com.buydeem.beanregistry.E@117e949d
为F中的E设置属性值,E中的F的值为:null
f:初始化前
f:初始化后
为E中的F设置属性值,F中的E的值为:com.buydeem.beanregistry.E@117e949d
e:初始化前
e:初始化后
com.buydeem.beanregistry.E@117e949d
com.buydeem.beanregistry.F@7a675056

从打印结果我们可以分析出主要的过程如下:

  1. 创建实例化对象e
  2. 对实例e进行属性赋值
  3. 容器中没有f的实例,所以创建实例化对象f
  4. 对实例f进行属性赋值,从容器中获取实例e,将其设置到f实例中
  5. f完成初始化
  6. 继续进行第2步为e进行属性赋值,将实例f设置到实例e中
  7. e完成初始化

通过对上面打印结果的分析,至少我们验证了我们的想法是没有错的,Spring确实是通过缓存半成品的Bean实例这种方式来解决循环依赖的,但是细节部分我们还是不得而知。

使用一个缓存

按照上面的分析,如果我使用一个Map做为缓存解决循环依赖可行吗?答案是不可行。还是以我们上面的例子,如果我先实例化了e,然后将e放入缓存中。如果现在另一个线程从容器中获取到e,得到的是一个半成品,这个在使用中是会有问题的。

使用两个缓存

有了上面的例子,我们知道一个缓存是行不通的。我使用两个缓存,一个用来存储半成品,一个用来存储成品。在为f设置属性e时,我先从成品缓存中拿,如果成品缓存中没有我再从半成品缓存中拿。 而外面获取e实例,我只从成品中拿,这样就不会出现拿到半成品实例了。

DefaultSingletonBeanRegistry

上面的结论都是在我们的推论下得出的,而实际上Spring内部是如何实现的还有待验证。首先我们得知道一个点就是ioc容器创建出来的Bean实例肯定得找个地方存起来,这样我们才能从容器中获取到ioc容器创建的实例,而这个存Bean的地方就是DefaultSingletonBeanRegistry。从它的原代码和其中的文档说明我们可以发现它就是通过几个Map来实现的。

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 ConcurrentHashMap<>(16);

获取一个单例Bean的源码实现:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   // Quick check for existing instance without full singleton lock
   Object singletonObject = this.singletonObjects.get(beanName);
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {
         synchronized (this.singletonObjects) {
            // Consistent creation of early reference within full singleton lock
            singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
               singletonObject = this.earlySingletonObjects.get(beanName);
               if (singletonObject == null) {
                  ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                  if (singletonFactory != null) {
                     singletonObject = singletonFactory.getObject();
                     this.earlySingletonObjects.put(beanName, singletonObject);
                     this.singletonFactories.remove(beanName);
                  }
               }
            }
         }
      }
   }
   return singletonObject;
}

从上面的代码逻辑我们能发现一个现象,获取单例Bean的顺序是singletonObjects->earlySingletonObjects->singletonFactories。比较特别的地方是singletonFactories存储的不是对象,而是ObjectFactory接口对象。如果从singletonFactories中获取到了对象,则是通过调用getObject()方法获取实际的对象,为什么明明两个缓存能解决的问题,在Spring中需要使用三个缓存来解决呢?

使用三个缓存

Spring的两大核心功能就是IOC和AOP,如果单纯只是IOC容器角度出发,两个缓存确实可以解决问题,而为什么要使用三级缓存主要原因就是因为Spring需要对AOP做支持。因为AOP最终是会生成一个代理对象,不管是jdk的代理还是CGlib的代理,最后生成的都是代理对象,而不是原对象。

对象创建过程.jpg

还是之前的例子,上面就是例子中对象的创建过程。上面的过程我们可以从Spring的源码中了解整个过程。

首先从AbstractBeanFactory中#doGetBean方法开始,它的主要逻辑就是先从缓存中获取需要查找的Bean,如果没有再通过BeanDefinition判断Bean的scope,然后创建Bean。为了分析简便,我们这里只关注scope为singleton的过程。

AbstractBeanFactory_doGetBean.png

该方法内部太长了,部分代码被我折叠了,我们只关注红字注释部分即可。首先会调用getSingleton方法,该方法的内部逻辑主要在DefaultSingletonBeanRegistry#getSingleton中,方法的主要内容如下:

DefaultSingletonBeanRegistry_getSingleton.jpg

通过该方法可以知道,它的主要逻辑就是从三个Map中去查找Bean,同时需要注意的是,如果允许返回早期的引用,也就是allowEarlyReference为true时,才会去singletonFactories中获取,并且如果得到了Bean实例还会将Bean实例放入earlySingletonObjects中并从singletonFactories移除。对于我们刚创建E实例,这个方法目前返回的为null。这样代码将会进入到AbstractBeanFactory#doGetBean方法中创建Singleton的逻辑中,即下面的关键代码:

AbstractBeanFactory_doGetBean创建单例Bean.jpg

上面代码的核心就是getSingleton方法,该方法的实现就是DefaultSingletonBeanRegistry#getSingleton方法,主要逻辑如下:

DefaultSingletonBeanRegistry_getSingleton.png

该方法的逻辑较长,主要的点就是红色框标记出来的地方。可以发现,创建Bean的逻辑并不在该方法中,而是通过ObjectFactory接口传入,主要实现还是在外部调用出,即AbstractBeanFactory#doGetBean方法中调用getSingleton方法处。如果是通过该ObjectFactory中的逻辑创建的Bean,才会执行addSingleton方法,该方法的实现在DefaultSingletonBeanRegistry#addSingleton方法中,实现逻辑如下:

DefaultSingletonBeanRegistry_addSingleton.jpg

该方法内部逻辑比较简单,从该方法中可以知道,前面创建的Bean实例会放入到singletonObjects中,而将singletonFactories和earlySingletonObjects中的实例移除。虽然已经看到此处了,但是目前还是一头雾水。下面接着分析,前面说过ObjectFactory.getObject()才是创建Bean实例的关键,跳到AbstractBeanFactory#doGetBean中可以发现它就是一个lamda表达式,里面的主要方法就是调用createBean方法(对于lamda表达式你当做是一个内部类对接口的实现即可)。下面直接分析createBean即可。该方法的实现位于AbstractAutowireCapableBeanFactory#createBean方法中,这个方法中的其他逻辑我们都不需要关注,只需要关注其中对doCreateBean方法的调用即可。通过doCreateBean该方法,你也可以了解Spring中Bean的生命周期,这个方法内容较多我们分成两部分来分析。

AbstractAutowireCapableBeanFactory_doCreateBean_上_.png

这部分逻辑主要就是创建Bean实例,然后判断是否允许Bean实例的提前暴露,因为这个时候的Bean还只是进行了生命周期中的实例化。而是否允许Bean的提前暴露的条件是Bean是单例Bean,容器允许循环引用和该Bean目前正在创建中。如果允许则调用addSingletonFactory方法,该方法的实现如下:

addSingletonFactory具体实现.png

该方法的实现主要就是将ObjectFactory放入到singletonFactories缓存中,而通过ObjectFactory可以得到实例对象。而ObjectFactory的实现则位于AbstractAutowireCapableBeanFactory#getEarlyBeanReference方法中,具体如下:

AbstractAutowireCapableBeanFactory_getEarlyBeanReference.png

该方法主要作用就是用来提前获取Bean实例的引用,这个里面就涉及到我们之前说的SmartInstantiationAwareBeanPostProcessor了,它提供了一个提前暴露Bean实例的引用的方法。如果是普通Bean则不会执行任何逻辑直接返回原Bean的引用,但是如果是经过AOP增强的Bean,则会返回Bean的代理对象,而不是原始Bean。接下来继续分析doCreateBean方法的下部分。

AbstractAutowireCapableBeanFactory_doCreateBean_中_.png

这个部分主要会进行两个操作,就是生命周期中的属性赋值和初始化。前面说的这部分都是在创建例子中的E实例,而E实例中依赖F实例,在属性赋值时F实例还没有被创建出来。所以在属性赋值时会导致F实例的创建。这部分代码的验证比较麻烦,但是可以追踪源码找到。在创建F的实例时,同样会跟创建实例E一样走到属性赋值部分。

现在我们的流程如下:E实例已经被创建了,正在进行属性赋值,属性赋值导致F实例化,而F进行到属性赋值值同样需要获取E实例。不过这次获取E实例会与之前的流程有区别了。通过前面的分析我们知道这个时候E和F的实例是以ObjectFactory的形式放到了singletonFactories中。再次调用AbstractBeanFactory#doGetBean方法时,在执行getSingleton时,返回的对象已经不为空了,而是E的实例了(此时E实例停留在属性赋值期间)。同时E也从singletonFactories转移到了earlySingletonObjects中。接着继续对实例F进行未完成的属性赋值和初始化。接着继续分析AbstractAutowireCapableBeanFactory#doCreateBean方法后面的逻辑。

AbstractAutowireCapableBeanFactory_doCreateBean_下_.png

这部分代码主要是用来依赖校验的,这段代码对我们的例子并没有什么用,主要还是用来解决依赖自己的这种情况。例如我们的实例中,如果E增加一个类型为E的字段,然后再配置文件中将自己作为依赖注入到e字段中,就会进入该逻辑。此时调用getSingleton获取到的是一个代理对象(如果Bean经过AOP增强的话),通过这个操作可以保证容器中最后是代理对象而不是原始对象。

我们继续之前的示例,在AbstractAutowireCapableBeanFactory#doCreateBean后部分这些代码我们直接略过。此时F实例已经完成了初始化,属性赋值和初始化逻辑了,此时继续进行到我们之前讲的DefaultSingletonBeanRegistry#getSingleton方法中,在该方法中,因为F实例是新创建的,所以会将newSingleton这个变量设置为true,在最后会调用addSingleton方法,该方法会将创建好的实例F放入singletonObjects缓存中,并将其实例从其他缓存中移除。


DefaultSingletonBeanRegistry_getSingleton.png

进过上面的逻辑,E在属性赋值时已经完成了F实例的创建,此时E可以获取到F实例,E与F同样完成属性赋值和初始化,最后E的实例也将被放入到singletonObjects缓存中。

上面就是整个依赖循环代码逻辑的分析。总结Bean在缓存中首先会以ObjectFactory接口的形式放在singletonFactories,接着允许从获取早期的Bean引用导致Bean从singletonFactories转移到earlySingletonObjects缓存中,此时的获取的Bean实例可能不会是原始的Bean(如果Bean被AOP增强的话,获取的是一个代理Bean),并且此时的Bean还是个半成品。最后Bean完成了属性赋值和初始化,Bean实例被放入到了singletonObjects。而此时的Bean已经是成品了。

所以总结下来Spring使用三层缓存来解决循环依赖而不是两层主要还是因为AOP。三个缓存的作用也不相同:

1. singletonObjects:存放的是已经初始化好的Bean,从缓存中取出的Bean可以直接使用。
2. earlySingletonObjects:存放提前暴露的Bean,此时该Bean还是半成品,属性赋值还未完成。
3. singletonFactories:缓存单例工厂Bean。

总结

上面解释Spring解决依赖循环的过程,代码比较多,在看本文时推荐一边调试一遍阅读。特别是文章中的关键点,麻烦的地方在于接口方法的回调,这个很容易导致忘记之前的逻辑。但是记住E和F实例的创建过程,在调试中会清楚许多。

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

推荐阅读更多精彩内容