Spring 中 bean 的迴圈依賴

xtyuns發表於2024-05-09

什麼是迴圈依賴

A 直接或間接依賴 B 的同時 B 又間接或直接依賴 A,此時我們可以稱 A 和 B 之間存在迴圈依賴關係。在使用 Spring 的過程中應該儘量避免迴圈引用關係的出現。

生命週期簡述

在閱讀下面的樣例之前,需要先了解一下 Spring 中 bean 的生命週期,簡單來說 bean 的生命週期分為:

  1. 例項化
  2. 屬性填充 (屬性注入發生在這個階段)
  3. 初始化
  4. 使用階段
  5. 銷燬

其中的初始化階段又可以細分為:

  1. 初始化前置處理 (各種 Aware 通常在這個階段呼叫 set 方法, 也可以自定義 BeanPostProcessor 來替換已經例項化且完成屬性填充的 bean)
  2. 初始化處理 (可以自定義 bean 初始化程式碼)
  3. 初始化後置處理 (正常情況下 AOP proxy 發生在這個階段, 後面會講提前發生 proxy 的場景, 這裡也可以自定義 BeanPostProcessor 來替換已經例項化、完成屬性填充且基本初始化完畢的 bean)

Spring 中 IoC 的初始化就是圍繞著 bean 的生命週期流程來完成的,bean 的生命週期也是 Spring 中的核心內容。

迴圈依賴樣例分析

透過以下樣例可以更進一步的認識什麼是迴圈依賴以及如何解決迴圈依賴

一,例項化階段依賴

這裡的例項化依賴是指:A 在例項化的階段中依賴 B, 同時 B 又依賴 A

示例程式碼

class InstDepApplication

// c1 構造引數依賴 c2
open class C1(private val c2: C2)

// c2 構造引數依賴 c1
open class C2(private val c1: C1)

// 或者 c2 屬性依賴 c1, 此時和構造引數依賴 c1 的效果是一樣的
//open class C2 {
//    @Autowired
//    private lateinit var c1: C1
//}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
}

執行結果: 應用因迴圈依賴啟動失敗
流程分析:

  1. 將 c1 記錄到當前建立流程中 並開始建立 c1
  2. 例項化 c1 時,C1 的構造引數依賴 c2,因此會在 IoC 中 獲取依賴項 c2
  3. 在查詢後發現 c2 還未建立,則嘗試建立 c2,即重複第一步:將 c2 記錄到當前建立流程中 並開始建立 c2
  4. 例項化 c2 時,C2 的構造引數依賴 c1,因此會在 IoC 中 獲取依賴項 c1
  5. 在查詢後發現 c1 還未建立,則嘗試建立 c1,但是再次 將 c1 記錄到當前建立流程中 時,因為 c1 已經存在於當前建立流程中,導致新記錄新增失敗,所以判定當前建立流程 存在無法處理的迴圈依賴關係,原因是:正在建立過程中的 bean 又需要進行建立,從而導致應用啟動失敗。

解決方案

此時只能透過 @Lazy 註解為依賴項生成代理物件間接獲取被依賴的 bean

open class C2(@Lazy private val c1: C1)

實際上在任意一處依賴項上新增 @Lazy 都可以解決迴圈依賴問題

@Lazy 的原理和注意事項

以上面的程式碼為例,對 C2 的依賴型 C1 新增 @Lazy 註解後,在 C2 的例項化階段不會直接從 IoC 中獲取依賴項 c1,取而代之的是先建立一個 C1 的代理物件 cp1 來作為 C2 的構造引數完成 c2 的例項化。
cp1 是透過 Proxy 或 CGLIB 生成的 Class 且利用反射來建立的物件,它只是一個臨時物件,作用是延遲從 IoC 中獲取 c1 的時機:當 cp1 被再次訪問時才會觸發從 IoC 中獲取 c1 的動作。

空參例項化 C2 是透過反射來實現的:

org.springframework.objenesis.instantiator.sun.SunReflectionFactoryInstantiator
sun.reflect.ReflectionFactory

生成 cp1 的相關程式碼:

protected Object buildLazyResourceProxy(LookupElement element, @Nullable String requestingBeanName) {
		TargetSource ts = new TargetSource() {
			@Override
			public Class<?> getTargetClass() {
				return element.lookupType;
			}
			@Override
			public Object getTarget() {
				return getResource(element, requestingBeanName);
			}
		};

		ProxyFactory pf = new ProxyFactory();
		pf.setTargetSource(ts);
		if (element.lookupType.isInterface()) {
			pf.addInterface(element.lookupType);
		}
		ClassLoader classLoader = (this.beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory ?
				configurableBeanFactory.getBeanClassLoader() : null);
		return pf.getProxy(classLoader);
	}

這個方案可以解決 C2 構造引數中依賴正在建立過程中但還為例項化的 bean 的問題,但是此時存在一個前提是:在 c1 放入 IoC 之前不能訪問 @Lazy 為其生成的代理物件 cp1,否則會觸發對 c1 的獲取,導致繼續建立 c1,從而重複上述的失敗流程。所以不要在 c2 例項化完成後的初始化階段訪問它的 c1 屬性,反例:

class InstDepApplication

open class C1(val c2: C2)

open class C2(@Lazy val c1: C1) {
    @PostConstruct
    fun postConstruct() {
        println(c1)
    }
}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
    println(applicationContext.getBean(C2::class.java).c1)
}

InitializingBeaninit-method@PostConstruct 同理

二,初始化階段依賴

這裡的初始化階段依賴是指:A 在例項化完成後的階段(包括屬性注入階段和初始化階段)中依賴 B, 同時 B 又依賴 A。
通常初始化依賴不屬於無法處理的迴圈依賴關係,因為在 spring 中預設會透過三級快取機制來調解迴圈依賴關係。

示例程式碼

class InstDepApplication

// c1 構造引數依賴 c2
open class C1() {
    @Autowired
    lateinit var c2: C2
}

// c2 構造引數依賴 c1
open class C2(val c1: C1)

// 或者 c2 屬性依賴 c1, 此時和構造引數依賴 c1 的效果是一樣的
//open class C2 {
//    @Autowired
//    private lateinit var c1: C1
//}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
    println(applicationContext.getBean(C2::class.java).c1)
}

執行結果: 應用啟動成功
流程分析:

  1. 將 c1 記錄到當前建立流程中 並開始建立 c1
  2. 例項化 c1 後,將 c1 的 singletonFactory 放入 IoC 的第三級快取(前提是 IoC 的 allowCircularReferences 為 true,預設為 true)
  3. 接下來為 c1 進行屬性填充,因為 c1 的屬性注入依賴 c2,因此會在 IoC 中 獲取依賴項 c2
  4. 在查詢後發現 c2 還未建立,則嘗試建立 c2,即重複第一步:將 c2 記錄到當前建立流程中 並開始建立 c2
  5. 例項化 c2 時,C2 的構造引數依賴 c1,因此會在 IoC 中 獲取依賴項 c1,此時將從 IoC 的第三級快取中獲取到 c1 的 singletonFactory,從而觸發 singletonFactory 的 getEarlyBeanReference,最終將三級快取中 c1 的 singletonFactory 提升為 earlySingletonObject 並作為引數完成 C2 的例項化
  6. 隨後 c2 完成建立,則 c1 回到屬性注入階段繼續完成建立,最終 IoC 初始化完成,應用成功啟動。

Q:為什麼第三級快取中存放的是 singletonFactory 而不是剛例項化完成的 c1?
A:因為在 c1 的初始化階段中 c1 所指向的物件是可以被修改掉的,所以 Spring 的正常預期是在 c1 初始化完成後再執行 wrapIfNecessary 進行 AOP 代理(如果過早的進行 AOP 會出現 AOP 內部引用的 c1 和最終要新增到 IoC 中的 c1 不是同一個物件的情況),但是如果有其他 bean 在 c1 例項化之後且初始化之前就需要訪問 c1 的話,就需要將 wrapIfNecessary 這個操作提前(在這之後 c1 的初始化不允許再修改 c1 所指向的物件,否則應用將啟動失敗),所以第三級快取中存放的 singletonFactory 其實就是對 wrapIfNecessary 的呼叫

Q:為什麼二級快取存放的物件叫做 earlySingletonObjects?
A:因為他們是在執行初始化階段完成前就被 AOP 的物件,它們後續還需要繼續執行未完成的初始化。

Q:第二級快取中的物件什麼時候提升到第一級,那些沒有被提前訪問的 singletonFactory 呢?
A:在 bean 的建立方法執行結束之後,其返回值會被新增到快取的第一級,同時清空該 bean 對應的 beanName 在第二級和第三級中存放的物件。因此它們並不會被直接提升到第一級,但是第二級快取中的物件會被用來檢驗 bean 在初始化階段是否發生的物件替換。

IoC 的三級快取:

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						singletonObject = this.earlySingletonObjects.get(beanName);
						if (singletonObject == null) {
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							if (singletonFactory != null) {
								singletonObject = singletonFactory.getObject();
								this.earlySingletonObjects.put(beanName, singletonObject);
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

解決方案

spring 中預設會將 allowCircularReferences 設定為 true 來開啟三級快取機制調解迴圈依賴關係。

注意事項

透過上面的流程分析我們知道 earlySingletonObject 是不可以在後續的初始化階段被修改所指向物件的,否則該單例 bean 就會出現兩份不同的物件,反例:

class InstDepApplication : BeanPostProcessor {
    override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
        // 此時 c1 已經被注入到 c2 中
        if (beanName == "c1") {
            // c1 所指向的 物件被修改了
            return C1().also {
                it.c2 = (bean as C1).c2
            }
        }
        return bean
    }
}

// c1 構造引數依賴 c2
open class C1() {
    @Autowired
    lateinit var c2: C2
}

// c2 構造引數依賴 c1
open class C2(val c1: C1)

// 或者 c2 屬性依賴 c1, 此時和構造引數依賴 c1 的效果是一樣的
//open class C2 {
//    @Autowired
//    private lateinit var c1: C1
//}

fun main() {
    val applicationContext = AnnotationConfigApplicationContext(
        InstDepApplication::class.java,
        C1::class.java,
        C2::class.java,
    )
    println(applicationContext.getBean(C2::class.java).c1)
}

相關文章