簡單說說Spring的循環依賴

本文首發於 blog.cc123.ccjava

前言

本文最耗時間的點就在於想一個好的標題, 既要燦爛奪目,又要光華內斂,事實證實這比砍需求還要難!算法

因爲對象之間的依賴關係常常是錯綜複雜,使用不當會引起不少意想不到的問題, 一個很典型的問題就是循環依賴 (也能夠稱之爲循環引用)。緩存

Spring 爲咱們提供了依賴注入,而且在某些情景(單例 Bean 的注入)下支持循環依賴的注入app

本文的主要目的是分析 Spring 在 Bean 的建立中是如何處理循環依賴的。ide

我會從循環依賴是什麼,以及它的壞處,到最後經過Spring的源碼來看它是如何處理這個問題的。測試

循環依賴不只僅是 Spring 的 Bean 之間會產生, 往大了看,系統模塊之間會產生循環依賴, 系統與系統之間也會產生循環依賴,這是一個典型的壞味道,咱們應該儘可能避免。ui

什麼是循環依賴

循環依賴指的是多個對象之間的依賴關係造成一個閉環this

下圖展現了兩個對象 A 和 B 造成的一個循環依賴spa

下圖展現了多個對象造成的一個循環依賴prototype

現實中因爲依賴層次深、關係複雜等因素, 致使循環依賴可能並非那麼一目瞭然。

爲何要避免循環依賴

循環依賴會爲系統帶來不少意想不到的問題,下面咱們來簡單討論一下

1、循環依賴會產生多米諾骨牌效應

換句話說就是牽一髮而動全身,想象一下平靜的湖面落入一顆石子,漣漪會瞬間向周圍擴散。

循環依賴造成了一個環狀依賴關係, 這個環中的某一點產生不穩定變化,都會致使整個環產生不穩定變化

實際的體驗就是

  • 難覺得代碼編寫測試,由於易變致使寫的測試也不穩定
  • 難以重構,由於互相依賴,你改動一個天然會影響其餘依賴對象
  • 難以維護,你根本不敢想象你的改動會形成什麼樣的後果
  • ......

2、循環依賴會致使內存溢出

參考下面的代碼

public class AService {
  private BService bService = new BService();
}

public class BService {
  private AService aService = new AService();
}
複製代碼

當你經過 new AService() 建立一個對象時你會得到一個棧溢出的錯誤。

若是你瞭解 Java 的初始化順序就應該知道爲何會出現這樣的問題。

由於調用 new AService() 時會先去執行屬性 bService 的初始化, 而 bService 的初始化又會去執行 AService 的初始化, 這樣就造成了一個循環調用,最終致使調用棧內存溢出。

Spring的循環依賴示例

下面咱們經過簡單的示例來展現 Spring 中的循環依賴注入, 我分別展現了一個構造器注入和 Field 注入的循環依賴示例

  • 構造器注入

    @Service
    public class AService {
      
      private final BService bService;
      
      @Autowired
      public AService(BService bService) {
        this.BService = bService
      }
      
    }
    複製代碼
    @Service
    public class BService {
      
      private final AService aService;
      
      @Autowired
      public BService(AService aService) {
        this.aService = aService;
      }
      
    }
    複製代碼
  • Field注入

    @Service
    public class AService {
      
      @Autowired
      private BService bService;
      
    }
    複製代碼
    @Service
    public class BService {
      
      @Autowired
      private AService aService;
      
    }
    複製代碼

    Setter注入和 Feild注入 相似

若是你啓動 Spring 容器的話, 構造器注入的方式會拋出異常 BeanCreationException , 提示你出現了循環依賴。

可是 Field 注入的方式就會正常啓動,並注入成功。

這說明 Spring 雖然可以處理循環依賴,但前提條件時你得按照它可以處理的方式去作才行。

好比 prototype 的 Bean 也不能處理循環依賴的注入,這點咱們須要注意。

一個檢測循環依賴的方法

在咱們具體分析 Spring 的 Field 注入是如何解決循環依賴時, 咱們來看看如何到檢測循環依賴

在一個循環依賴的場景中,咱們能夠肯定如下約束

  1. 依賴關係是一個圖的結構
  2. 依賴是有向的
  3. 循環依賴說明依賴關係產生了環

明確後,咱們就能知道檢測循環依賴本質就是在檢測一個圖中是否出現了環, 這是一個很簡單的算法問題。

利用一個 HashSet 依次記錄這個依賴關係方向中出現的元素, 當出現重複元素時就說明產生了, 並且這個重複元素就是環的起點。

參考下圖, 紅色的節點就表明是循環出現的點

以第一個圖爲例,依賴方向爲 A->B->C->A ,很容易檢測到 A 就是環狀點。

Spring是如何處理循環依賴的

Spring 可以處理 單例Bean 的循環依賴(Field注入方式),本節咱們就經過紙上談兵的方式來看看它是如何作到的

首先,咱們將 Spring 建立 Bean 的生命週期簡化爲兩個步驟:實例化 -> 依賴注入, 以下圖所示

實例化就至關於經過 new 建立了一個具體的對象, 而依賴注入就至關於爲對象的屬性進行賦值操做

咱們再將這個過程擴展到兩個相互依賴 Bean 的建立過程上去, 以下圖所示

A 在執行依賴注入時須要實例化 B, 而 B 在執行依賴注入時又會實例化 A ,造成了一個很典型的依賴環。

產生環的節點就是 B 在執行依賴注入的階段, 若是咱們將其"砍」掉, 就沒有環了, 以下圖所示

這樣作確實沒有循環依賴了,但卻帶來了另外一個問題,B 是沒有通過依賴注入的, 也就是說 B 是不完整的, 這怎麼辦呢?

此時 A 已經建立完成並維護在 Spring 容器內,A 持有 B 的引用, 而且 Spring 維護着未進行依賴注入的 B 的引用

當 Spring 主動建立 B 時能夠直接取得 B 的引用 (省去了實例化的過程), 當執行依賴注入時, 也能夠直接從容器內取得 A 的引用, 這樣 B 就建立完成了

A 持有的未進行依賴注入的 B,和後面單首創建 B 流程裏面是同一個引用對象, 當 B 執行完依賴注入後,A 持有的 B 也就是一個完整的 Bean了。

Show me the code

沒有代碼的泛泛而談是沒有靈魂的

我畫了一個簡化的流程圖來展現一個 Bean 的建立(省略了 Spring 的 BeanPostProcessor,Aware 等事件)過程, 但願你過一遍,而後咱們再去看源碼。

入口直接從 getBean(String) 方法開始, 以 populateBean 結束, 用於分析循環依賴的處理是足夠的了

getBean(String)AbstractBeanFactory 的方法, 它內部調用了 doGetBean 方法, 下面是源碼:

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
 	@Override
	public Object getBean(String name) throws BeansException {
		return doGetBean(name, null, null, false);
	}
  
  protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly){
    ...
    // #1
    Object sharedInstance = getSingleton(beanName);
    ...
    final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
    if (mbd.isSingleton()) {
      // #2
    	sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
						@Override
						public Object getObject() throws BeansException {
              	// #3
								return createBean(beanName, mbd, args);
						}
					});
    }
    ...
    return (T)bean;
  }
}
複製代碼

我簡化了 doGetBean 的方法體,與流程圖對應起來,使得咱們能夠輕鬆找到下面的調用流程

doGetBean -> getSingleton(String) -> getSingleton(String, ObjectFactory)
複製代碼

getSingletonDefaultSingletonBeanRegistry 的重載方法

DefaultSingletonBeanRegistry 維護了三個 Map 用於緩存不一樣狀態的 Bean, 稍後咱們分析 getSingleton 時會用到

/** 維護着全部建立完成的Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** 維護着建立中Bean的ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** 維護着全部半成品的Bean */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
複製代碼

getSingleton(String) 調用了重載方法 getSingleton(String, boolean), 而該方法實際就是一個查詢 Bean 的實現, 先看圖再看代碼:

從圖中咱們能夠看見以下查詢層次

singletonObjects =>  earlySingletonObjects => singletonFactories
複製代碼

再結合源碼

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
  @Override
	public Object getSingleton(String beanName) {
		return getSingleton(beanName, true);
	}
  
  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 從singletonObjects獲取已建立的Bean
		Object singletonObject = this.singletonObjects.get(beanName);
    
    // 若是沒有已建立的Bean, 可是該Bean正在建立中
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 從earlySingletonObjects獲取已經實例化的Bean
				singletonObject = this.earlySingletonObjects.get(beanName);
      
      	// 若是沒有實例化的Bean, 可是參數allowEarlyReference爲true
				if (singletonObject == null && allowEarlyReference) {
          // 從singletonFactories獲取ObjectFactory
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
            // 使用ObjectFactory獲取Bean實例
						singletonObject = singletonFactory.getObject();
            
            // 保存實例, 並清理ObjectFactory
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
		}
		return (singletonObject != NULL_OBJECT ? singletonObject : null);
	}
  
}

複製代碼

經過 getSingleton(String) 沒有找到Bean的話就會繼續往下調用 getSingleton(String, ObjectFactory) , 這也是個重載方法, 源碼以下

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		...	
    // 獲取緩存的Bean
    Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {
				...
        // 標記Bean在建立中
        beforeSingletonCreation(beanName);
				boolean newSingleton = false;
				...
        // 建立新的Bean, 實際就是調用createBean方法
        singletonObject = singletonFactory.getObject();
        newSingleton = true;
				...
				if (newSingleton) {
          // 緩存bean
					addSingleton(beanName, singletonObject);
				}
			}
			return (singletonObject != NULL_OBJECT ? singletonObject : null);
	}
複製代碼

流程很清晰,就不必再畫圖了,簡單來講就是根據 beanName 找不到 Bean 的話就使用傳入的 ObjectFactory 建立一個 Bean。

從最開始的代碼片斷咱們能夠知道這個 ObjectFactory 的 getObject 方法實際就是調用了 createBean 方法

sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
						@Override
						public Object getObject() throws BeansException {
              	// #3
								return createBean(beanName, mbd, args);
						}
					});
複製代碼

createBeanAbstractAutowireCapableBeanFactory 實現的,內部調用了 doCreateBean 方法

doCreateBean 承擔了 bean 的實例化,依賴注入等職責。

參考下圖

createBeanInstance 負責實例化一個 Bean 對象。

addSingletonFactory 會將單例對象的引用經過 ObjectFactory 保存下來, 而後將該 ObjectFactory 緩存在 Map 中(該方法在依賴注入以前執行)。

populateBean 主要是執行依賴注入。

下面是源碼, 基本與上面的流程圖保持一致, 細節的地方我也標了註釋了

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
	@Override
	protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
		...
		return doCreateBean(beanName, mbdToUse, args);
	}
	
	protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
		...
		BeanWrapper instanceWrapper = null;
		if (instanceWrapper == null) {
			// 實例化Bean
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
		final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
		// 容許單例Bean的提早暴露
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			// 新建並緩存ObjectFactory
			addSingletonFactory(beanName, new ObjectFactory<Object>() {
				@Override
				public Object getObject() throws BeansException {
					// 若是忽略BeanPostProcessor邏輯, 該方法實際就是直接返回bean對象
					// 而這裏的bean對象就是前面實例化的對象
					return getEarlyBeanReference(beanName, mbd, bean);
				}
			});
		}

		...
		// 依賴注入
    populateBean(beanName, mbd, instanceWrapper);
		...
	}
}
複製代碼

若是你仔細看了上面的代碼片斷,相信你已經找到 Spring 處理循環依賴的關鍵點了

咱們以 A,B 循環依賴注入爲例,畫了一個完整的注入流程圖

注意上圖的黃色節點, 咱們再來過一下這個流程

  1. 在建立 A 的時候,會將 實例化的A 經過 addSingleFactory(黃色節點)方法緩存, 而後執行依賴注入B。
  2. 注入會走建立流程, 最後B又會執行依賴注入A。
  3. 因爲第一步已經緩存了 A 的引用, 再次建立 A 時能夠經過 getSingleton 方法獲得這個 A 的提早引用(拿到最開始緩存的 objectFactory, 經過它取得對象引用), 這樣 B 的依賴注入就完成了。
  4. B 建立完成後, 表明 A 的依賴注入也完成了,那麼 A 也建立成功了 (實際上 Spring 還有 initial 等步驟,不過與咱們此次的討論主題相關性不大)

這樣整個依賴注入的流程就完成了

總結

又到了總結的時候了,雖然全文鋪的有點長,可是 Spring 處理單例 Bean 的循環依賴卻並不複雜,並且稍微擴展一下,咱們還能夠將這樣的處理思路借鑑一下從而處理相似的問題。

不可避免的文章仍是留下了很多坑,好比

  • 我沒有詳細解釋構造器注入爲何不能處理循環依賴
  • 我沒有詳細說明 Spring 如何檢測循環依賴的細節
  • 我也沒有說明 prototype 的 Bean 爲何不能處理循環依賴
  • .....

固然這些都能在 Spring 建立 Bean 的流程裏面找到(getBean(String) 方法),細節的東西就留給讀者本身去源碼裏面發現了哦

參考

  1. Circular_dependency
相關文章
相關標籤/搜索