循環依賴
- 0. 聲明
- 1. 循環依賴簡介
- 2. 正文
-
- 2.1 注解屬性注入
- 2.2 構造器注入
- 3. 小結
0. 聲明
循環依賴可謂是面試的重點,恰好看到這篇循環依賴講解極為清楚的一篇文章,故轉載。Spring源碼-循環依賴(附25張調試截圖)
1. 循環依賴簡介
在介紹循環依賴之前,我們先介紹一下為什麼會産生循環依賴?在
Spring
中,我們完成
Bean
的注入有三種方式.
- 構造方法注入
- setter 注入
- 注解注入
例如有兩個
Bean
,分别為
A
和
B
。他們互相為對方的屬性,彼此依賴,這就産生了循環依賴。
A
依賴
B
,但是
B
還沒有完成執行個體化,不能注入給
A
。而
B
又依賴于
A
,
A
同樣沒有完成執行個體化,不能注入給
B
,由此彼此等待對方執行個體化的問題便是循環依賴。
需要注意的是,
Spring
可以解決屬性注入的循環依賴,但是并不能解決構造方法的循環依賴問題。
2. 正文
Spring
在哪些情況下會出現循環依賴錯誤?哪些情況下能自身解決循環依賴,又是如何解決的?本文将介紹筆者通過本地調試
Spring
源碼來觀察循環依賴的過程。
2.1 注解屬性注入
首先本地準備好一份
Spring
源碼,筆者是從
Github
上
Clone
下來的一份,然後用
IDEA
導入,再建立一個
module
用于存放調試的代碼。
本次調試有三個類,
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
的構造方法如下:
其中
refresh
是容器的啟動方法,點進去,然後找到我們需要的那一步,即執行個體化
A
、
B
的步驟:
finishBeanFactoryInitialization
會先完成工廠的執行個體化,然後在最後一步執行個體化
A
、
B
:
preInstantiateSingletons
将依次對
non-lazy singleton
依次執行個體化,其中就有
A
、
B
:
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
:
這裡為
getSingleton-F
傳入了個
Lambda
表達式給
ObjectFactory
接口類型的
singletonFatory
。
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
是以将會建立一個
ObjectFactory
的匿名類對象
singletonFactory
,而其
getObject
方法的實作将調用
createBean
。
getObject
=>
createBean
=>
doCreateBean
,建立
A
的 Bean,關于
doCreateBean
,在 《如何記憶 Spring 的生命周期》 中有進行介紹。主要有 4 個步驟:(1)執行個體化,(2)屬性指派,(3)初始化,(4)注冊回調函數。下面看下 B 是在哪一步注入到 A 中。
首先看下是不是執行個體化。
在執行個體化完成後,
bean
中的
b
仍為
null
,說明不是執行個體化。那再看下一步,屬性指派。
在
populateBean
執行後,
bean
中的
b
不再是
null
了,而已經是
B
的對象了,而且
b
的
a
屬性也不是
null
,是此時正在建立的
bean
,說明已經成功完成了依賴注入。是以 "
@Autowired
标注的屬性是如何注入的" 和 “
Spring
如何解決循環依賴” 兩個問題的答案都在
populateBean
這一步中。那再重新進入
populateBean
看下。
其中會依次調用
BeanPostProcessor
的
postProcessProperties
方法。在
getBeanPostProcessors
傳回的
List
中有
AutowiredAnnotationBeanPostProcessor
,将負責
@Autowired
的注入。
AutowiredAnnotationBeanPostProcessor
的
postProcessProperties
方法如下所示:
先找到被
@Autowired
标注的
b
屬性,再通過
inject
注入。
進入
inject
方法,由于
A
依賴
B
,這裡将通過
beanFactory.resolveDependency
獲得
B
的
bean
。
在成功擷取
B
的
Bean
後,再通過反射注入。
現在需要關注的就是
resolveDependency
,這裡解決
A => B
的依賴,需要去擷取
B
,将仍然通過
getBean
擷取,和之前說
getBean
擷取
A
的過程類似,隻是這次換成了
B
,調用棧如下:
仍然将通過
doCreateBean
來建立
B
的
bean
。
那麼問題來了,之前說過
doCreateBean
會進行屬性指派,那麼由于
B
又依賴
A
,是以需要将
A
注入到
B
中,可是
A
也還正在進行
doGetBean
。那麼
Spring
是怎麼解決的循環依賴,關注
B
的
populateBean
就能知道答案了。
由于
B
依賴于
A
,是以需要将
A
注入
B
,該過程和前面說的 ”将
B
注入
A
“類似,通過
getBean
來獲得
A
。
getBean => doGetBean => getSingleton
,又是熟悉的步驟,但這次
getSingleton-C
中發生了不一樣的事,能夠成功獲得
A
的緩存。
首先嘗試在
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
。
getEarlyBeanReference
會傳回
A
的引用,雖然
A
還在建立,未完成。
讓我們想下後面會發生的事:
-
成功擷取到了B
的引用,完成屬性指派;A
-
完成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;
}
}
構造器的注入将發生在
doCreateBean
的第一步
createBeanInstance
,具體方法如下:
擷取
A
的構造器,執行
autowireConstructor
。然後調用
ConstructorResolver
的
createArgument
方法處理構造函數的參數,由于構造器被
@Autowired
标注,将使用
resolveAutowiredArgument
處理注入參數,接着又是熟悉的步驟,調用棧如下:
處理依賴注入,會通過
getBean
獲得
B
,在
doCreateBean
中進行
B
執行個體化。
那我們就再進入
B
執行個體化的第一步
createBeanInstance
方法,調用棧如下:
B
的構造方法依賴
A
,則嘗試通過
doGetBean
擷取
A
。由于
A
沒有在
doCreateBean
中完成執行個體化,是以
getSingleton-C
中無法獲得
A
的緩存,則隻能通過
getSingleton-F
方法嘗試獲得
A
。
但在
getSingleton-F
中的
beforeSingletonCreation
方法将對循環依賴進行檢查。
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
檢查抛出循環依賴異常。