1. 程式人生 > 程式設計 >簡單說說Spring的迴圈依賴

簡單說說Spring的迴圈依賴

本文首發於 blog.cc123.cc

前言

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

由於物件之間的依賴關係經常是錯綜複雜,使用不當會引發很多意想不到的問題, 一個很典型的問題就是迴圈依賴 (也可以稱之為迴圈引用)。

Spring 為我們提供了依賴注入,並且在某些情景(單例 Bean 的注入)下支援迴圈依賴的注入

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

我會從迴圈依賴是什麼,以及它的壞處,到最後通過Spring的原始碼來看它是如何處理這個問題的。

迴圈依賴不僅僅是 Spring 的 Bean 之間會產生, 往大了看,系統模組之間會產生迴圈依賴, 系統與系統之間也會產生迴圈依賴,這是一個典型的壞味道,我們應該儘量避免。

什麼是迴圈依賴

迴圈依賴指的是多個物件之間的依賴關係形成一個閉環

下圖展示了兩個物件 A 和 B 形成的一個迴圈依賴

下圖展示了多個物件形成的一個迴圈依賴

現實中由於依賴層次深、關係複雜等因素, 導致迴圈依賴可能並不是那麼一目瞭然。

為什麼要避免迴圈依賴

迴圈依賴會為系統帶來很多意想不到的問題,下面我們來簡單討論一下

一、迴圈依賴會產生多米諾骨牌效應

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

迴圈依賴形成了一個環狀依賴關係, 這個環中的某一點產生不穩定變化,都會導致整個環產生不穩定變化

實際的體驗就是

  • 難以為程式碼編寫測試,因為易變導致寫的測試也不穩定
  • 難以重構,因為互相依賴,你改動一個自然會影響其他依賴物件
  • 難以維護,你根本不敢想象你的改動會造成什麼樣的後果
  • ......

二、迴圈依賴會導致記憶體溢位

參考下面的程式碼

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,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,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,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,bean);
				}
			});
		}

		...
		// 依賴注入
    populateBean(beanName,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