Spring原始碼分析之迴圈依賴及解決方案

雕爺的架構之路發表於2020-11-16

Spring原始碼分析之迴圈依賴及解決方案

往期文章:

  1. Spring原始碼分析之預啟動流程
  2. Spring原始碼分析之BeanFactory體系結構
  3. Spring原始碼分析之BeanFactoryPostProcessor呼叫過程詳解
  4. Spring原始碼分析之Bean的建立過程詳解

正文:

首先,我們需要明白什麼是迴圈依賴?簡單來說就是A物件建立過程中需要依賴B物件,而B物件建立過程中同樣也需要A物件,所以A建立時需要先去把B建立出來,但B建立時又要先把A建立出來...死迴圈有木有...

迴圈依賴

那麼在Spring中,有多少種迴圈依賴的情況呢?大部分人只知道兩個普通的Bean之間的迴圈依賴,而Spring中其實存在三種物件(普通Bean,工廠Bean,代理物件),他們之間都會存在迴圈依賴,這裡我給列舉出來,大致分別以下幾種:

  • 普通Bean與普通Bean之間
  • 普通Bean與代理物件之間
  • 代理物件與代理物件之間
  • 普通Bean與工廠Bean之間
  • 工廠Bean與工廠Bean之間
  • 工廠Bean與代理物件之間

那麼,在Spring中是如何解決這個問題的呢?

1. 普通Bean與普通Bean

首先,我們先設想一下,如果讓我們自己來編碼,我們會如何解決這個問題?

栗子

現在我們有兩個互相依賴的物件A和B

public class NormalBeanA {

	private NormalBeanB normalBeanB;

	public void setNormalBeanB(NormalBeanB normalBeanB) {
		this.normalBeanB = normalBeanB;
	}
}
public class NormalBeanB {

	private NormalBeanA normalBeanA;

	public void setNormalBeanA(NormalBeanA normalBeanA) {
		this.normalBeanA = normalBeanA;
	}
}

然後我們想要讓他們彼此都含有物件

public class Main {

	public static void main(String[] args) {
		// 先建立A物件
		NormalBeanA normalBeanA = new NormalBeanA();
		// 建立B物件
		NormalBeanB normalBeanB = new NormalBeanB();
		// 將A物件的引用賦給B
		normalBeanB.setNormalBeanA(normalBeanA);
		// 再將B賦給A
		normalBeanA.setNormalBeanB(normalBeanB);
	}
}

發現了嗎?我們並沒有先建立一個完整的A物件,而是先建立了一個空殼物件(Spring中稱為早期物件),將這個早期物件A先賦給了B,使得得到了一個完整的B物件,再將這個完整的B物件賦給A,從而解決了這個迴圈依賴問題,so easy!

那麼Spring中是不是也這樣做的呢?我們就來看看吧~

Spring中的解決方案

由於上一篇已經分析過Bean的建立過程了,其中的某些部分就不再細講了

先來到建立Bean的方法

AbstractAutowireCapableBeanFactory#doCreateBean

假設此時在建立A

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
  // beanName -> A
  // 例項化A
  BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
  // 是否允許暴露早期物件
  boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                    isSingletonCurrentlyInCreation(beanName));
  if (earlySingletonExposure) {
    // 將獲取早期物件的回撥方法放到三級快取中
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
  }
}

addSingletonFactory

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		synchronized (this.singletonObjects) {
			// 單例快取池中沒有該Bean
			if (!this.singletonObjects.containsKey(beanName)) {
				// 將回撥函式放入三級快取
				this.singletonFactories.put(beanName, singletonFactory);
				this.earlySingletonObjects.remove(beanName);
				this.registeredSingletons.add(beanName);
			}
		}
	}

ObjectFactory是一個函式式介面

在這裡,我們發現在建立Bean時,Spring不管三七二十一,直接將一個獲取早期物件的回撥方法放進了一個三級快取中,我們再來看一下回撥方法的邏輯

getEarlyBeanReference

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
  Object exposedObject = bean;
  // 呼叫BeanPostProcessor對早期物件進行處理,在Spring的內建處理器中,並無相關的處理邏輯
  // 如果開啟了AOP,將引入一個AnnotationAwareAspectJAutoProxyCreator,此時將可能對Bean進行動態代理
  if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
      if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
        SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
        exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
      }
    }
  }
  return exposedObject;
}

在這裡,如果沒有開啟AOP,或者該物件不需要動態代理,會直接返回原物件

此時,已經將A的早期物件快取起來了,接下來在填充屬性時會發生什麼呢?

相信大家也應該想到了,A物件填充屬性時必然發現依賴了B物件,此時就將轉頭建立B,在建立B時同樣會經歷以上步驟,此時就該B物件填充屬性了,這時,又將要轉頭建立A,那麼,現在會有什麼不一樣的地方呢?我們看看getBean的邏輯吧

doGetBean

protected <T> T doGetBean(
			String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly){
  // 此時beanName為A
  String beanName = transformedBeanName(name);
  // 嘗試從三級快取中獲取bean,這裡很關鍵
  Object sharedInstance = getSingleton(beanName);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // 從單例快取池中獲取,此時仍然是取不到的
  Object singletonObject = this.singletonObjects.get(beanName);
  // 獲取不到,判斷bean是否正在建立,沒錯,此時A確實正在建立
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    // 由於現在仍然是在同一個執行緒,基於同步鎖的可重入性,此時不會阻塞
    synchronized (this.singletonObjects) {
      // 從早期物件快取池中獲取,這裡是沒有的
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null && allowEarlyReference) {
        // 從三級快取中獲取回撥函式,此時就獲取到了我們在建立A時放入的回撥函式
        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
        if (singletonFactory != null) {
          // 呼叫回撥方法獲取早期bean,由於我們現在討論的是普通物件,所以返回原物件
          singletonObject = singletonFactory.getObject();
          // 將早期物件放到二級快取,移除三級快取
          this.earlySingletonObjects.put(beanName, singletonObject);
          this.singletonFactories.remove(beanName);
        }
      }
    }
  }
  // 返回早期物件A
  return singletonObject;
}

震驚!此時我們就拿到了A的早期物件進行返回,所以B得以被填充屬性,B建立完畢後,又將返回到A填充屬性的過程,A也得以被填充屬性,A也建立完畢,這時,A和B都建立好了,迴圈依賴問題得以收場~

普通Bean和普通Bean之間的問題就到這裡了,不知道小夥伴們有沒有暈呢~

2. 普通Bean和代理物件

普通Bean和代理物件之間的迴圈依賴與兩個普通Bean的迴圈依賴其實大致相同,只不過是多了一次動態代理的過程,我們假設A物件是需要代理的物件,B物件仍然是一個普通物件,然後,我們開始建立A物件。

剛開始建立A的過程與上面的例子是一模一樣的,緊接著自然是需要建立B,然後B依賴了A,於是又倒回去建立A,此時,再次走到去快取池獲取的過程。

// 從三級快取中獲取回撥函式
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
  // 呼叫回撥方法獲取早期bean,此時返回的是一個A的代理物件
  singletonObject = singletonFactory.getObject();
  // 將早期物件放到二級快取,移除三級快取
  this.earlySingletonObjects.put(beanName, singletonObject);
  this.singletonFactories.remove(beanName);
}

這時就不太一樣了,在singletonFactory.getObject()時,由於此時A是需要代理的物件,在呼叫回撥函式時,就會觸發動態代理的過程

AbstractAutoProxyCreator#getEarlyBeanReference

public Object getEarlyBeanReference(Object bean, String beanName) {
  // 生成一個快取Key
  Object cacheKey = getCacheKey(bean.getClass(), beanName);
  // 放入快取中,用於在初始化後呼叫該後置處理器時判斷是否進行動態代理過
  this.earlyProxyReferences.put(cacheKey, bean);
  // 將物件進行動態代理
  return wrapIfNecessary(bean, beanName, cacheKey);
}

此時,B在建立時填充的屬性就是A的代理物件了,B建立完畢,返回到A的建立過程,但此時的A仍然是一個普通物件,可B引用的A已經是個代理物件了,不知道小夥伴看到這裡有沒有迷惑呢?

不急,讓我們繼續往下走,填充完屬性自然是需要初始化的,在初始化後,會呼叫一次後置處理器,我們看看會不會有答案吧

初始化

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
	//...省略前面的步驟...
  // 呼叫初始化方法
  invokeInitMethods(beanName, wrappedBean, mbd);
  // 處理初始化後的bean
  wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

在處理初始化後的bean,又會呼叫動態代理的後置處理器了

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
  if (bean != null) {
    Object cacheKey = getCacheKey(bean.getClass(), beanName);
    // 判斷快取中是否有該物件,有則說明該物件已被動態代理,跳過
    if (this.earlyProxyReferences.remove(cacheKey) != bean) {
      return wrapIfNecessary(bean, beanName, cacheKey);
    }
  }
  return bean;
}

不知道小夥伴發現沒有,earlyProxyReferences這個快取可不就是我們在填充B的屬性,進而從快取中獲取A時放進去的嗎?不信您往上翻到getEarlyBeanReference的步驟看看~

所以,此時並未進行任何處理,依舊返回了我們的原物件A,看來這裡並沒有我們要的答案,那就繼續吧~

// 是否允許暴露早期物件
if (earlySingletonExposure) {
  // 從快取池中獲取早期物件
  Object earlySingletonReference = getSingleton(beanName, false);
  if (earlySingletonReference != null) {
    // bean為初始化前的物件,exposedObject為初始化後的物件
    // 判斷兩物件是否相等,基於上面的分析,這兩者是相等的
    if (exposedObject == bean) {
      // 將早期物件賦給exposedObject
      exposedObject = earlySingletonReference;
    }
  }
}

我們來分析一下上面的邏輯,getSingleton從快取池中獲取早期物件返回的是什麼呢?

synchronized (this.singletonObjects) {
  // 從早期物件快取池中獲取,此時就拿到了我們填充B屬性時放入的A的代理物件
  singletonObject = this.earlySingletonObjects.get(beanName);
  if (singletonObject == null && allowEarlyReference) {
    // 從三級快取中獲取回撥函式
    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
    if (singletonFactory != null) {
      // 呼叫回撥方法獲取早期bean
      singletonObject = singletonFactory.getObject();
      // 將早期物件放到二級快取,移除三級快取
      this.earlySingletonObjects.put(beanName, singletonObject);
      this.singletonFactories.remove(beanName);
    }
  }
}

發現了嗎?此時我們就獲取到了A的代理物件,然後我們又把這個物件賦給了exposedObject,此時建立物件的流程走完,我們得到的A不就是個代理物件了嗎~

此次栗子是先建立需要代理的物件A,假設我們先建立普通物件B會發生什麼呢?

3. 代理物件與代理物件

代理物件與代理物件的迴圈依賴是怎麼樣的呢?解決過程又是如何呢?這裡就留給小夥伴自己思考了,其實和普通Bean與代理物件是一模一樣的,小夥伴想想是不是呢,這裡我就不做分析了。

4. 普通Bean與工廠Bean

這裡所說的普通Bean與工廠Bean並非指bean與FactoryBean,這將毫無意義,而是指普通Bean與FactoryBean的getObject方法產生了迴圈依賴,因為FactoryBean最終產生的物件是由getObject方法所產出。我們先來看看栗子吧~

假設工廠物件A依賴普通物件B,普通物件B依賴普通物件A。

小夥伴看到這裡就可能問了,誒~你這不對呀,怎麼成了「普通物件B依賴普通物件A」呢?不應該是工廠物件A嗎?是這樣的,在Spring中,由於普通物件A是由工廠物件A產生,所有在普通物件B想要獲取普通物件A時,其實最終尋找呼叫的是工廠物件A的getObject方法,所以只要普通物件B依賴普通物件A就可以了,Spring會自動幫我們把普通物件B和工廠物件A聯絡在一起。

小夥伴,哦~

普通物件A

public class NormalBeanA {

	private NormalBeanB normalBeanB;

	public void setNormalBeanB(NormalBeanB normalBeanB) {
		this.normalBeanB = normalBeanB;
	}
}

工廠物件A

@Component
public class FactoryBeanA implements FactoryBean<NormalBeanA> {
	@Autowired
	private ApplicationContext context;

	@Override
	public NormalBeanA getObject() throws Exception {
		NormalBeanA normalBeanA = new NormalBeanA();
		NormalBeanB normalBeanB = context.getBean("normalBeanB", NormalBeanB.class);
		normalBeanA.setNormalBeanB(normalBeanB);
		return normalBeanA;
	}

	@Override
	public Class<?> getObjectType() {
		return NormalBeanA.class;
	}
}

普通物件B

@Component
public class NormalBeanB {
	@Autowired
	private NormalBeanA normalBeanA;
}

假設我們先建立物件A

由於FactoryBean和Bean的建立過程是一樣的,只是多了步getObject,所以我們直接定位到呼叫getObject入口

if (mbd.isSingleton()) {
  // 開始建立bean
  sharedInstance = getSingleton(beanName, () -> {
    // 建立bean
    return createBean(beanName, mbd, args);
  });
  // 處理FactoryBean
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
protected Object getObjectForBeanInstance(
			Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {
	// 先嚐試從快取中獲取,保證多次從工廠bean獲取的bean是同一個bean
  object = getCachedObjectForFactoryBean(beanName);
  if (object == null) {
    // 從FactoryBean獲取物件
    object = getObjectFromFactoryBean(factory, beanName, !synthetic);
  }
}
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
	// 加鎖,防止多執行緒時重複建立bean
  synchronized (getSingletonMutex()) {
    // 這裡是Double Check
    Object object = this.factoryBeanObjectCache.get(beanName);
    if (object == null) {
      // 獲取bean,呼叫factoryBean的getObject()
      object = doGetObjectFromFactoryBean(factory, beanName);
    }
    // 又從快取中取了一次,why? 我們慢慢分析
    Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
    if (alreadyThere != null) {
      object = alreadyThere;
    }else{
      // ...省略初始化bean的邏輯...
      // 將獲取到的bean放入快取
      this.factoryBeanObjectCache.put(beanName, object);
    }
  }
}
private Object doGetObjectFromFactoryBean(FactoryBean<?> factory, String beanName){
  return factory.getObject();
}

現在,就走到了我們自定義的getObject方法,由於我們呼叫了context.getBean("normalBeanB", NormalBeanB.class),此時,將會去建立B物件,在建立過程中,先將B的早期物件放入三級快取,緊接著填充屬性,發現依賴了A物件,又要倒回來建立A物件,從而又回到上面的邏輯,再次呼叫我們自定義的getObject方法,這個時候會發生什麼呢?

又要去建立B物件...(Spring:心好累)

但是!此時我們在建立B時,是直接通過getBean在快取中獲取到了B的早期物件,得以返回了!於是我們自定義的getObject呼叫成功,返回了一個完整的A物件!

但是此時FactoryBean的緩衝中還是什麼都沒有的。

// 又從快取中取了一次
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
  object = alreadyThere;
}

這一次取alreadyThere必然是null,流程繼續執行,將此時將獲取到的bean放入快取

this.factoryBeanObjectCache.put(beanName, object);

從FactoryBean獲取物件的流程結束,返回到建立B的過程中,B物件此時的屬性也得以填充,再返回到第一次建立A的過程,也就是我們第一次呼叫自定義的getObject方法,呼叫完畢,返回到這裡

// 獲取bean,呼叫factoryBean的getObject()
object = doGetObjectFromFactoryBean(factory, beanName);
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
  object = alreadyThere;

那麼,此時this.factoryBeanObjectCache.get(beanName)能從緩衝中拿到物件了嗎?有沒有發現,拿到了剛剛B物件填充屬性時再次建立A物件放進去的!

所以,明白這裡為什麼要再次從快取中獲取了吧?就是為了解決由於迴圈依賴時呼叫了兩次自定義的getObject方法,從而建立了兩個不相同的A物件,保證我們返回出去的A物件唯一!

怕小夥伴暈了,畫個圖給大家

5. 工廠Bean與工廠Bean之間

我們已經舉例4種迴圈依賴的栗子,Spring都有所解決,那麼有沒有Spring也無法解決的迴圈依賴問題呢?

有的!就是這個FactoryBeanFactoryBean的迴圈依賴!

假設工廠物件A依賴工廠物件B,工廠物件B依賴工廠物件A,那麼,這次的栗子會是什麼樣呢?

普通物件

public class NormalBeanA {

	private NormalBeanB normalBeanB;

	public void setNormalBeanB(NormalBeanB normalBeanB) {
		this.normalBeanB = normalBeanB;
	}
}
public class NormalBeanB {

	private NormalBeanA normalBeanA;

	public void setNormalBeanA(NormalBeanA normalBeanA) {
		this.normalBeanA = normalBeanA;
	}
}

工廠物件

@Component
public class FactoryBeanA implements FactoryBean<NormalBeanA> {
	@Autowired
	private ApplicationContext context;

	@Override
	public NormalBeanA getObject() throws Exception {
		NormalBeanA normalBeanA = new NormalBeanA();
		NormalBeanB normalBeanB = context.getBean("factoryBeanB", NormalBeanB.class);
		normalBeanA.setNormalBeanB(normalBeanB);
		return normalBeanA;
	}

	@Override
	public Class<?> getObjectType() {
		return NormalBeanA.class;
	}
}
@Component
public class FactoryBeanB implements FactoryBean<NormalBeanB> {
	@Autowired
	private ApplicationContext context;
	@Override
	public NormalBeanB getObject() throws Exception {
		NormalBeanB normalBeanB = new NormalBeanB();
		NormalBeanA normalBeanA = context.getBean("factoryBeanA", NormalBeanA.class);
		normalBeanB.setNormalBeanA(normalBeanA);
		return normalBeanB;
	}

	@Override
	public Class<?> getObjectType() {
		return NormalBeanB.class;
	}
}

首先,我們開始建立物件A,此時為呼叫工廠物件A的getObject方法,轉而去獲取物件B,便會走到工廠物件B的getObject方法,然後又去獲取物件A,又將呼叫工廠物件A的getObject,再次去獲取物件B,於是再次走到工廠物件B的getObject方法......此時,已經歷了一輪迴圈,卻沒有跳出迴圈的跡象,妥妥的死迴圈了。

我們畫個圖吧~

沒錯!這個圖就是這麼簡單,由於始終無法建立出一個物件,不管是早期物件或者完整物件,使得兩個工廠物件反覆的去獲取對方,導致陷入了死迴圈。

那麼,我們是否有辦法解決這個問題呢?

我的答案是無法解決,如果有想法的小夥伴也可以自己想一想哦~

我們發現,在發生迴圈依賴時,只要迴圈鏈中的某一個點可以先建立出一個早期物件,那麼在下一次迴圈時,就會使得我們能夠獲取到早期物件從而跳出迴圈!

而由於工廠物件與工廠物件間是無法建立出這個早期物件的,無法滿足跳出迴圈的條件,導致變成了死迴圈。

那麼此時Spring中會丟擲一個什麼樣的異常呢?

當然是棧溢位異常啦!兩個工廠物件一直相互呼叫,不斷開闢棧幀,可不就是棧溢位有木有~

6. 工廠物件與代理物件

上面的情況是無法解決迴圈依賴的,那麼這個情況可以解決嗎?

答案是可以的!

我們分析了,一個迴圈鏈是否能夠得到終止,關鍵在於是否能夠在某個點建立出一個早期物件(臨時物件),而代理物件在doCreateBean時,是會生成一個早期物件放入三級快取的,於是該迴圈鏈得以終結。

具體過程我這裡就不再細分析了,就交由小夥伴自己動手吧~

總結

以上我們一共舉例了6種情況,通過分析,總結出這樣一條定律:

在發生迴圈依賴時,判斷一個迴圈鏈是否能夠得到終止,關鍵在於是否能夠在某個點建立出一個早期物件(臨時物件),那麼在下一次迴圈時,我們就能通過該早期物件進而跳出(打破)迴圈!

通過這樣的定律,我們得出工廠Bean與工廠Bean之間是無法解決迴圈依賴的,那麼還有其他情況無法解決迴圈依賴嗎?

有的!以上的例子舉的都是單例的物件,並且都是通過set方法形成的迴圈依賴。

假使我們是由於構造方法形成的迴圈依賴呢?是否有解決辦法嗎?

沒有,因為這並不滿足我們得出的定律

無法執行完畢構造方法,自然無法建立出一個早期物件。

假使我們的物件是多例的呢?

也不能,因為多例的物件在每次建立時都是建立新的物件,即使能夠建立出早期物件,也不能為下一次迴圈所用!

好了,本文就到這裡結束了,希望小夥伴們有所收穫~

Spring IOC的核心部分到此篇就結束了,下一篇就讓我們進行AOP之旅吧~

下文預告:Spring原始碼分析之AOP從解析到呼叫

Spring 原始碼系列
  1. Spring原始碼分析之 IOC 容器預啟動流程(已完結)
  2. Spring原始碼分析之BeanFactory體系結構(已完結)
  3. Spring原始碼分析之BeanFactoryPostProcessor呼叫過程(已完結)
  4. Spring原始碼分析之Bean的建立過程(已完結)
  5. Spring原始碼分析之什麼是迴圈依賴及解決方案
  6. Spring原始碼分析之AOP從解析到呼叫
  7. Spring原始碼分析之事務管理(上),事物管理是spring作為容器的一個特點,總結一下他的基本實現與原理吧
  8. Spring原始碼分析之事務管理(下) ,關於他的底層事物隔離與事物傳播原理,重點分析一下
Spring Mvc 原始碼系列
  1. SpringMvc體系結構
  2. SpringMvc原始碼分析之Handler解析過程
  3. SpringMvc原始碼分析之請求鏈過程

另外筆者公眾號:奇客時間,有更多精彩的文章,有興趣的同學,可以關注

相關文章