Spring 如何解決循環依賴的問題

轉自:徹底理解SpringIOC、DI-這篇文章就夠了

先看一個循環依賴問題

現象

循環依賴其實就是循環引用,也就是兩個或則兩個以上的bean互相持有對方,最終形成閉環。比如A依賴于B,B依賴于C,C又依賴于A。如下圖:

如何理解“依賴”呢,在Spring中有:

  • 構造器循環依賴
  • field屬性注入循環依賴

直接上代碼:

構造器循環依賴

@Service
public class A {  
    public A(B b) {  }
}

@Service
public class B {  
    public B(C c) {  
    }
}

@Service
public class C {  
    public C(A a) {  }
}

結果:項目啟動失敗,發現了一個cycle

2.field屬性注入循環依賴

@Service
public class A1 {  
    @Autowired  
    private B1 b1;
}

@Service
public class B1 {  
    @Autowired  
    public C1 c1;
}

@Service
public class C1 {  
    @Autowired  public A1 a1;
}

結果:項目啟動成功

3.field屬性注入循環依賴(prototype)

@Service
@Scope("prototype")
public class A1 {  
    @Autowired  
    private B1 b1;
}

@Service
@Scope("prototype")
public class B1 {  
    @Autowired  
    public C1 c1;
}

@Service
@Scope("prototype")
public class C1 {  
    @Autowired  public A1 a1;
}

結果:項目啟動失敗,發現了一個cycle。

現象總結:同樣對于循環依賴的場景,構造器注入和prototype類型的屬性注入都會初始化Bean失敗。因為@Service默認是單例的,所以單例的屬性注入是可以成功的。

分析原因

分析原因也就是在發現SpringIOC的過程,如果對源碼不感興趣可以關注每段源碼分析之后的總結和循環依賴問題的分析即可。

SpringBean的加載流程(源碼分析)

簡單一段代碼作為入口

ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
ac.getBean(XXX.class);

ClassPathXmlApplicationContext是一個加載XML配置文件的類,與之相對的還有AnnotationConfigWebApplicationContext,這兩個類大差不差的,只是ClassPathXmlApplicationContext的Resource是XML文件而AnnotationConfigWebApplicationContext是Scan注解獲得的。

看到第二行就已經可以直接獲取bean的實例了,所以第一行構造方法時,就已經完成了對所有bean的加載。

ClassPathXmlApplicationContext舉例,他里面儲存的東西如下:


構造方法如下:


接下來大概看看refresh方法:

子方法先不看,先看看refresh方法的結構,其實就有幾點值得學習:

1、方法為什么加鎖? 是為了避免多線程的場景下同時刷新Spring上下文

2、雖然整個方法是加鎖的,但是卻用了Synchronized關鍵字的對象鎖startUpShutdownMonitor,這樣做有兩個好處:

(1)關閉資源的時候會調用close()方法,close()方法也使用了同樣的對象鎖,而關閉資源的close和refresh的兩個沖突的方法,這樣可以避免沖突

(2)此處對象鎖相對于整個方法加鎖的話,同步的范圍更小了,鎖的粒度更小,效率更高

3、這個方法refresh定義了整個Spring IOC的流程,每一個方法名字都清晰易懂,可維護性、可讀性很強

總結:看源碼需要找準入口,看的時候多思考,學習Spring的巧妙的設計。ApplicationContext的構造方法中最關鍵是方法是refresh,其中有一些比價好的設計。

obtainFreshBeanFactory方法這個方法作用是獲取刷新Spring上下文的Bean工廠:

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {  
    this.refreshBeanFactory();  
    return this.getBeanFactory();
}

protected final void refreshBeanFactory() throws BeansException {  
    if (this.hasBeanFactory()) {    
        this.destroyBeans();    
        this.closeBeanFactory();  
    }  
    try {    
        DefaultListableBeanFactory beanFactory = this.createBeanFactory();    
        beanFactory.setSerializationId(this.getId());    
        this.customizeBeanFactory(beanFactory);    
        this.loadBeanDefinitions(beanFactory);    
        synchronized(this.beanFactoryMonitor) {      
            this.beanFactory = beanFactory;    }  
        } catch (IOException var5) {    
            throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);  
        }
}

這斷代碼的核心是DefaultListableBeanFactory,核心類我們再整理一下,以圖表格式:下面有三個加粗的Map,這些個Map是解決問題的關鍵。。。我們之后詳細分析


BeanDefinition在IOC容器中的注冊

接下來簡要分析一下loadBeanDefinitions。

對于這個BeanDefinition,我是這么理解的: 它是SpringIOC過程中間的一個產物,可以看成是對Bean定義的抽象,里面封裝的數據都是與Bean定義相關的,封裝了一些基本的bean的Property、initi-method、destroy-method等。

這里的主要方法是loadBeanDefinitions,這里不詳細展開說,它主要做了幾件事:

1、初始化了BeanDefinitionReader

2、通過BeanDefinitionReader獲取Resource,也就是xml配置文件的位置,并且把文件轉換成一個叫Document的對象

3、接下來需要將Document對象轉化成容器內部的數據結構(也就是BeanDefinition),也即是將Bean定義的List、Map、Set等各種元素進行解析,轉換成Managed類(Spring對BeanDefinition數據的封裝)放在BeanDefinition中;這個方法是RegisterBeanDefinition(),也就是解析的過程。

4、解析完成后,會把解析的結果放到BeanDefinition對象中并設置到一個Map中

以上這個過程就是BeanDefinition在IOC容器中的注冊。

再回到Refresh方法,總結每一步如下圖:

總結:這一部分步驟主要是Spring如何加載Xml文件或者注解,并把它解析成BeanDefinition。

Spring創建Bean的過程

先回到之前的refresh方法(也就是在構造ApplicationContext時的方法),我們跳過不重要的部分:

我們直接看finishBeanFactoryInitialization里面的preInstantiateSingletons方法,顧名思義初始化所有的單例bean,截取部分如下:

現在來看核心的getBean方法,對于所有獲取Bean對象是實例,都是用這個getBean方法,這個方法最終調用的是doGetBean方法,這個方法就是所謂的DI(依賴注入)發生的地方。

程序=數據+算法,之前的BeanDefinition就是“數據”,依賴注入也就是在BeanDefinition準備好情況下進行進行的,這個過程不簡單,因為Spring提供了很多參數配置,每一個參數都代表了IOC容器的特性,這些特性的實現需要在Bean的生命周期中完成。

代碼比較多,就不貼了,大家可以自行查看AbstractBeanFactory里面的doGetBean方法,這里直接上圖,這個圖就是依賴注入的整個過程:

總結:Spring創建好了BeanDefinition之后呢,會開始實例化Bean,并且對Bean的依賴屬性進行填充。實例化時底層使用了CGLIB或Java反射技術。上圖中instantiateBean核PupulateBean方法很重要!

循環依賴問題分析

我們先總結一下之前的結論:

1、構造器注入和prototype類型的field注入發生循環依賴時都無法初始化

2、field注入單例的bean時,盡管有循環依賴,但bean仍然可以被成功初始化

針對這幾個結論,提出問題

  1. 單例的設值注入bean是如何解決循環依賴問題呢?如果A中注入了B,那么他們初始化的順序是什么樣子的?
  2. 為什么prototype類型的和構造器類型的Spring無法解決循環依賴呢?

之前在DefaultListableBeanFactory類中,列出了一個表格;現在我把關鍵的精華屬性列出來:

一級緩存:
/** 保存所有的singletonBean的實例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

二級緩存:
/** 保存所有早期創建的Bean對象,這個Bean還沒有完成依賴注入 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
三級緩存:
/** singletonBean的生產工廠*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
 
/** 保存所有已經完成初始化的Bean的名字(name) */
private final Set<String> registeredSingletons = new LinkedHashSet<String>(64);
 
/** 標識指定name的Bean對象是否處于創建狀態  這個狀態非常重要 */
private final Set<String> singletonsCurrentlyInCreation =
    Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));

前面三個Map,我們稱為單例初始化的三級緩存,理解這個問題,我們目前只需關注“三級”,也就是singletonFactories

分析:

對于問題1,單例的設值注入,如果A中注入了B,B應該是A中的一個屬性,那么猜想應該是A已經被instantiate(實例化)之后,在populateBean(填充A中的屬性)時,對B進行初始化。

對于問題2,instantiate(實例化)其實就是理解成new一個對象的過程,而new的時候肯定要執行構造方法,所以猜想對于應該是A在instantiate(實例化)時,進行B的初始化。

有了分析和猜想之后呢,圍繞關鍵的屬性,根據從上圖的doGetBean方法開始到populateBean所有的代碼,我整理了如下圖:

上圖是整個過程中關鍵的代碼路徑,感興趣的可以自己debug幾回,最關鍵的解決循環依賴的是如上的兩個標紅的方法,第一個方法getSingleton會從singletonFactories里面拿Singleton,而addSingletonFactory會把Singleton放入singletonFactories。

對于問題1:單例的設值注入bean是如何解決循環依賴問題呢?如果A中注入了B,那么他們初始化的順序是什么樣子的?

假設循環注入是A-B-A:A依賴B(A中autowire了B),B又依賴A(B中又autowire了A):

本質就是三級緩存發揮作用,解決了循環。

對于當時問題2,instantiate(實例化)其實就是理解成new一個對象的過程,而new的時候肯定要執行構造方法,所以猜想對于應該是A在instantiate(實例化)時,進行B的初始化。

答案也很簡單,因為A中構造器注入了B,那么A在關鍵的方法addSingletonFactory()之前就去初始化了B,導致三級緩存中根本沒有A,所以會發生死循環,Spring發現之后就拋出異常了。至于Spring是如何發現異常的呢,本質上是根據Bean的狀態給Bean進行mark,如果遞歸調用時發現bean當時正在創建中,那么久拋出循環依賴的異常即可。

那么prototype的Bean是如何初始化的呢?

prototypeBean有一個關鍵的屬性:

/** Names of beans that are currently in creation */
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
    new NamedThreadLocal<Object>("Prototype beans currently in creation");

保存著正在創建的prototype的beanName,在流程上并沒有暴露任何factory之類的緩存。并且在beforePrototypeCreation(String beanName)方法時,把每個正在創建的prototype的BeanName放入一個set中:

protected void beforePrototypeCreation(String beanName) {
        Object curVal = this.prototypesCurrentlyInCreation.get();
        if (curVal == null) {
            this.prototypesCurrentlyInCreation.set(beanName);
        }
        else if (curVal instanceof String) {
            Set<String> beanNameSet = new HashSet<String>(2);
            beanNameSet.add((String) curVal);
            beanNameSet.add(beanName);
            this.prototypesCurrentlyInCreation.set(beanNameSet);
        }
        else {
            Set<String> beanNameSet = (Set<String>) curVal;
            beanNameSet.add(beanName);
        }
}

并且會循環依賴時檢查beanName是否處于創建狀態,如果是就拋出異常:

protected boolean isPrototypeCurrentlyInCreation(String beanName) {
    Object curVal = this.prototypesCurrentlyInCreation.get();
    return (curVal != null &&
    (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}

從流程上就可以查看,無論是構造注入還是設值注入,第二次進入同一個Bean的getBean方法是,一定會在校驗部分拋出異常,因此不能完成注入,也就不能實現循環引用。

總結:Spring在InstantiateBean時執行構造器方法,構造出實例,如果是單例的話,會將它放入一個singletonBeanFactory的緩存中,再進行populateBean方法,設置屬性。通過一個singletonBeanFactory的緩存解決了循環依賴的問題。


再解決一個問題

現在大家已經對Spring整個流程有點感覺了,我們再來解決一個簡單的常見的問題:

考慮一下如下的singleton代碼:

    @Service
    public class SingletonBean{
       @Autowired 
       private PrototypeBean prototypeBean;

       public void doSomething(){
         System.out.println(prototypeBean.toString());       
       }
    }

     @Component 
     @Scope(value="prototype")
     public class PrototypeBean{
     }

一個Singleton的Bean中Autowired了一個prototype的Bean,那么問題來了,每次調用SingletonBean.doSomething()時打印的對象是不是同一個呢?

有了之前的知識儲備,我們簡單分析一下:因為Singleton是單例的,所以在項目啟動時就會初始化,prototypeBean本質上只是它的一個Property,那么ApplicationContex中只存在一個SingletonBean和一個初始化SingletonBean時創建的一個prototype類型的PrototypeBean。

那么每次調用SingletonBean.doSomething()時,Spring會從ApplicationContex中獲取SingletonBean,每次獲取的SingletonBean是同一個,所以即便PrototypeBean是prototype的,但PrototypeBean仍然是同一個。每次打印出來的內存地址肯定是同一個。

那這個問題如何解決呢?

解決辦法也很簡單,這種情況我們不能通過注入的方式注入一個prototypeBean,只能在程序運行時手動調用getBean("prototypeBean")方法,我寫了一個簡單的工具類:

@Service
public class SpringBeanUtils implements ApplicationContextAware {  
    private static ApplicationContext appContext;  
    @Override  
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 
       SpringBeanUtils.appContext=applicationContext;  
    }  
    public static ApplicationContext getAppContext() {    
        return appContext;  
    }  
    public static Object getBean(String beanName) {    
        checkApplicationContext();    
        return appContext.getBean(beanName);  
    }  
    private static void checkApplicationContext() {    
        if (null == appContext) {      
            throw new IllegalStateException("applicaitonContext未注入");   
         }  
    }  
    @SuppressWarnings("unchecked")  
    public static <T> T getBean(Class<T> clazz) {    
        checkApplicationContext();    
        Map<?, ?> map = appContext.getBeansOfType(clazz);    
        return map.isEmpty() ? null : (T) map.values().iterator().next();  
    }
 }

對于這個ApplicationContextAware接口:

在某些特殊的情況下,Bean需要實現某個功能,但該功能必須借助于Spring容器才能實現,此時就必須讓該Bean先獲取Spring容器,然后借助于Spring容器實現該功能。為了讓Bean獲取它所在的Spring容器,可以讓該Bean實現ApplicationContextAware接口。

感興趣的讀者自己可以試試。

總結:

回到循環依賴的問題,有的人可能會問singletonBeanFactory只是一個三級緩存,那么一級緩存和二級緩存有什么用呢?

其實大家只要理解整個流程就可以切入了,Spring在初始化Singleton的時候大致可以分幾步,初始化——設值——銷毀,循環依賴的場景下只有A——B——A這樣的順序,但在并發的場景下,每一步在執行時,都有可能調用getBean方法,而單例的Bean需要保證只有一個instance,那么Spring就是通過這些個緩存外加對象鎖去解決這類問題,同時也可以省去不必要的重復操作。Spring的鎖的粒度選取也是很吊的,這里暫時不深入研究了。

解決此類問題的關鍵是要對SpringIOC和DI的整個流程做到心中有數,看源碼一般情況下不要求每一行代碼都了解透徹,但是對于整個的流程和每個流程中在做什么事需要了然,這樣實際遇到問題時才可以很快的切入進行分析解決。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容