天天看點

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

循環依賴

  • 0. 聲明
  • 1. 循環依賴簡介
  • 2. 正文
    • 2.1 注解屬性注入
    • 2.2 構造器注入
  • 3. 小結

0. 聲明

       循環依賴可謂是面試的重點,恰好看到這篇循環依賴講解極為清楚的一篇文章,故轉載。Spring源碼-循環依賴(附25張調試截圖)

1. 循環依賴簡介

       在介紹循環依賴之前,我們先介紹一下為什麼會産生循環依賴?在

Spring

中,我們完成

Bean

的注入有三種方式.

  1. 構造方法注入
  2. setter 注入
  3. 注解注入

       例如有兩個

Bean

,分别為

A

B

。他們互相為對方的屬性,彼此依賴,這就産生了循環依賴。

A

依賴

B

,但是

B

還沒有完成執行個體化,不能注入給

A

。而

B

又依賴于

A

A

同樣沒有完成執行個體化,不能注入給

B

,由此彼此等待對方執行個體化的問題便是循環依賴。

       需要注意的是,

Spring

可以解決屬性注入的循環依賴,但是并不能解決構造方法的循環依賴問題。

2. 正文

Spring

在哪些情況下會出現循環依賴錯誤?哪些情況下能自身解決循環依賴,又是如何解決的?本文将介紹筆者通過本地調試

Spring

源碼來觀察循環依賴的過程。

2.1 注解屬性注入

       首先本地準備好一份

Spring

源碼,筆者是從

Github

Clone

下來的一份,然後用

IDEA

導入,再建立一個

module

用于存放調試的代碼。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       本次調試有三個類,

A

B

通過注解

@Autowired

标注在屬性上構成循環依賴,

Main

為主函數類。

@Component("A")
public class A {
 @Autowired
 B b;
}
           
@Component("B")
public class B {
 @Autowired
 A a;
}
           
public class Main {
 public static void main(String[] args) {
  ApplicationContext context =
    new ClassPathXmlApplicationContext("spring-config.xml");
  A a = (A) context.getBean("A");
  B b = (B) context.getBean("B");
 }
}
           

       可以先試着運作下,并不會報錯,說明這種情況下的循環依賴能由

Spring

解決。

       我們要觀察如何解決循環依賴,首先需要知道

@Autowired

标注的屬性是如何注入的,如

B

是怎麼注入到

A

中的。

       由于

A

B

的 scope 是

single

,且預設

non-lazy

,是以在

ClassPathXmlApplicationContext

初始化時會預先加載

A

B

,并完成執行個體化、屬性指派、初始化等步驟。

ClassPathXmlApplicationContext

的構造方法如下:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       其中

refresh

是容器的啟動方法,點進去,然後找到我們需要的那一步,即執行個體化

A

B

的步驟:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

finishBeanFactoryInitialization

會先完成工廠的執行個體化,然後在最後一步執行個體化

A

B

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

preInstantiateSingletons

将依次對

non-lazy singleton

依次執行個體化,其中就有

A

B

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

A

B

不是工廠類,則直接通過

getBean

觸發初始化。首先會觸發

A

的初始化。

getBean

=>

doGetBean

,再通過

getSingleton

擷取

Bean

。注意在

doGetBean

中有兩個

getSingleton

方法會先後執行,本文用

getSingleton-C

getSingleton-F

來區分。第一個是嘗試從緩存中擷取,這時緩存中沒有

A

,無法獲得,則執行第二個,通過工廠獲得。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

		// ...
		Object bean;
		// 首先從緩存中擷取 Bean
		Object sharedInstance = getSingleton(beanName);
		if (sharedInstance != null && args == null) {
			// ...
		}

		else {
				// ...
				// 緩存中擷取不到,則在 BeanFactory 中擷取
				if (mbd.isSingleton()) {
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							// Explicitly remove instance from singleton cache: It might have been put there
							// eagerly by the creation process, to allow for circular reference resolution.
							// Also remove any beans that received a temporary reference to the bean.
							destroySingleton(beanName);
							throw ex;
						}
					});
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}

				// ... 
		}
		return (T) bean;
	}
           

       這裡會執行

getSingleton-F

來擷取單例

Bean

A

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       這裡為

getSingleton-F

傳入了個

Lambda

表達式給

ObjectFactory

接口類型的

singletonFatory

public interface ObjectFactory<T> {
 T getObject() throws BeansException;
}
           

       是以将會建立一個

ObjectFactory

的匿名類對象

singletonFactory

,而其

getObject

方法的實作将調用

createBean

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

getObject

=>

createBean

=>

doCreateBean

,建立

A

的 Bean,關于

doCreateBean

,在 《如何記憶 Spring 的生命周期》 中有進行介紹。主要有 4 個步驟:(1)執行個體化,(2)屬性指派,(3)初始化,(4)注冊回調函數。下面看下 B 是在哪一步注入到 A 中。

       首先看下是不是執行個體化。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       在執行個體化完成後,

bean

中的

b

仍為

null

,說明不是執行個體化。那再看下一步,屬性指派。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       在

populateBean

執行後,

bean

中的

b

不再是

null

了,而已經是

B

的對象了,而且

b

a

屬性也不是

null

,是此時正在建立的

bean

,說明已經成功完成了依賴注入。是以 "

@Autowired

标注的屬性是如何注入的" 和 “

Spring

如何解決循環依賴” 兩個問題的答案都在

populateBean

這一步中。那再重新進入

populateBean

看下。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       其中會依次調用

BeanPostProcessor

postProcessProperties

方法。在

getBeanPostProcessors

傳回的

List

中有

AutowiredAnnotationBeanPostProcessor

,将負責

@Autowired

的注入。

AutowiredAnnotationBeanPostProcessor

postProcessProperties

方法如下所示:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       先找到被

@Autowired

标注的

b

屬性,再通過

inject

注入。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       進入

inject

方法,由于

A

依賴

B

,這裡将通過

beanFactory.resolveDependency

獲得

B

bean

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       在成功擷取

B

Bean

後,再通過反射注入。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       現在需要關注的就是

resolveDependency

,這裡解決

A => B

的依賴,需要去擷取

B

,将仍然通過

getBean

擷取,和之前說

getBean

擷取

A

的過程類似,隻是這次換成了

B

,調用棧如下:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       仍然将通過

doCreateBean

來建立

B

bean

       那麼問題來了,之前說過

doCreateBean

會進行屬性指派,那麼由于

B

又依賴

A

,是以需要将

A

注入到

B

中,可是

A

也還正在進行

doGetBean

。那麼

Spring

是怎麼解決的循環依賴,關注

B

populateBean

就能知道答案了。

       由于

B

依賴于

A

,是以需要将

A

注入

B

,該過程和前面說的 ”将

B

注入

A

“類似,通過

getBean

來獲得

A

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

getBean => doGetBean => getSingleton

,又是熟悉的步驟,但這次

getSingleton-C

中發生了不一樣的事,能夠成功獲得

A

的緩存。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       首先嘗試在

singletoObjects

中擷取,失敗。接着嘗試從

earlySingletonObjects

中擷取,失敗。最後在

singletonFactories

中擷取到

singletonFactory

,并通過

getObject

擷取到

A

bean

       這三者被稱作三級緩存,在

getSingleton-C

方法中會依次從這三級緩存中嘗試擷取單例

Bean

。當從第三級緩存擷取到

A

後,會将其從第三級緩存中移除,加入到第二級緩存。

/** Cache of singleton objects: bean name to bean instance. */
// 緩存單例Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** Cache of early singleton objects: bean name to bean instance. */
// 緩存正在建立,還未建立完成的單例Bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

/** Cache of singleton factories: bean name to ObjectFactory. */
// 緩存單例bean的工廠
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
           

       我們可以看到

singletonFactories

中存有

A

B

,它們是什麼時候被加到三級緩存中的呢?就是在

doCreateBean

中做

populateBean

的前一步通過

addSingeletonFactory

beanName

ObjectFactory

的匿名工廠類加入到第三級緩存中。當調用

singletonFactory.getObject

方法時,将調用

getEarlyBeanReference

擷取

A

Bean

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

getEarlyBeanReference

會傳回

A

的引用,雖然

A

還在建立,未完成。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       讓我們想下後面會發生的事:

  1. B

    成功擷取到了

    A

    的引用,完成屬性指派;
  2. B

    完成

    doCreateBean

    ,将傳回,

    A

    成功擷取到

    B

    Bean

    ,完成屬性指派,最後完成

    A

    Bean

    的建立。

       最後

A

B

Bean

都完成建立。

       之是以通過注解屬性注入不會存在循環依賴問題,是因為

Spring

記錄了正在建立的

Bean

,并提前将正在建立的

Bean

的引用交給了需要依賴注入的

Bean

,進而完成閉環,讓

B

建立成功,不會繼續嘗試建立

A

       在這個過程中最關鍵的是

Bean

的引用,而要有

Bean

的引用便必須完成

doCreateBean

中的第一步執行個體化。

       我們這裡是将

@Autowired

标注在屬性上,而依賴注入發生在第二步屬性指派,這時才能成功擷取到引用。

       下面我們試下修改

A

B

為構造器注入,讓依賴注入發生在第一步執行個體化中。

2.2 構造器注入

@Component("A")
public class A {
 B b;

 @Autowired
 public A(B b) {
  this.b = b;
 }
}
           
@Component("B")
public class B {
 A a;

 @Autowired
 public B(A a) {
  this.a = a;
 }
}
           
Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       構造器的注入将發生在

doCreateBean

的第一步

createBeanInstance

,具體方法如下:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       擷取

A

的構造器,執行

autowireConstructor

。然後調用

ConstructorResolver

createArgument

方法處理構造函數的參數,由于構造器被

@Autowired

标注,将使用

resolveAutowiredArgument

處理注入參數,接着又是熟悉的步驟,調用棧如下:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       處理依賴注入,會通過

getBean

獲得

B

,在

doCreateBean

中進行

B

執行個體化。

       那我們就再進入

B

執行個體化的第一步

createBeanInstance

方法,調用棧如下:

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

B

的構造方法依賴

A

,則嘗試通過

doGetBean

擷取

A

。由于

A

沒有在

doCreateBean

中完成執行個體化,是以

getSingleton-C

中無法獲得

A

的緩存,則隻能通過

getSingleton-F

方法嘗試獲得

A

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

       但在

getSingleton-F

中的

beforeSingletonCreation

方法将對循環依賴進行檢查。

Spring - 循環依賴0. 聲明1. 循環依賴簡介2. 正文3. 小結

singletonsCurrentlyInCreation

是一個

set

,由于

A

已經 都在

getSingleton-F

中執行過一遍了,已經被添加到了

singletonsCurrentlyInCreation

,是以這裡第二次通過

getSingleton-F

擷取

A

時,

add

傳回

false

,将抛出

BeanCurrentlyInCreationException

異常。

3. 小結

       對比以上兩種方式 “屬性注入” 和 “構造器注入”,都是

A => B => A

,差別在于

B => A

時,“屬性注入” 在

getSingleton-C

中會通過緩存擷取到

A

的引用,而 “構造器注入”,則由于不存在

A

引用,也自然無法通過緩存獲得,便會嘗試再次通過

getSingleton-F

擷取,而及時被

beforeSingletonCreation

檢查抛出循環依賴異常。