首頁 > 軟體

關於spring迴圈依賴問題及解決方案

2022-06-27 14:00:57

一、三種迴圈依賴的情況

①構造器的迴圈依賴:

  • 這種依賴spring是處理不了的,直接丟擲BeanCurrentlylnCreationException異常。

②單例模式下的setter迴圈依賴:

  • 通過“三級快取”處理迴圈依賴,能處理。

③非單例迴圈依賴:

  • 無法處理。原型(Prototype)的場景是不支援迴圈依賴的,通常會走到AbstractBeanFactory類中下面的判斷,丟擲異常。
if (isPrototypeCurrentlyInCreation(beanName)) 
{  throw new BeanCurrentlyInCreationException(beanName);}

原因很好理解,建立新的A時,發現要注入原型欄位B,又建立新的B發現要注入原型欄位A…這就套娃了, 你猜是先StackOverflow還是OutOfMemory?

Spring怕你不好猜,就先丟擲了BeanCurrentlyInCreationException

出現的背景:

比如幾個Bean之間的互相參照 

甚至自己“迴圈”依賴自己

二、解決方案

首先,Spring內部維護了三個Map,也就是我們通常說的三級快取。

筆者翻閱Spring檔案倒是沒有找到三級快取的概念,可能也是本土為了方便理解的詞彙。

在Spring的DefaultSingletonBeanRegistry類中,你會赫然發現類上方掛著這三個Map:

  • singletonObjects (一級快取)它是我們最熟悉的朋友,俗稱“單例池”“容器”,快取建立完成單例Bean的地方。
  • earlySingletonObjects(二級快取)對映Bean的早期參照,也就是說在這個Map裡的Bean不是完整的,甚至還不能稱之為“Bean”,只是一個Instance.
  • singletonFactories(三級快取) 對映建立Bean的原始工廠

後兩個Map其實是“墊腳石”級別的,只是建立Bean的時候,用來藉助了一下,建立完成就清掉了。

那麼Spring 是如何通過上面介紹的三級快取來解決迴圈依賴的呢?

這裡只用 A,B 形成的迴圈依賴來舉例:

  • 範例化 A,此時 A 還未完成屬性填充和初始化方法(@PostConstruct)的執行,A 只是一個半成品。
  • 為 A 建立一個 Bean工廠,並放入到 singletonFactories 中。
  • 發現 A 需要注入 B 物件,但是一級、二級、三級快取均為發現物件 B。
  • 範例化 B,此時 B 還未完成屬性填充和初始化方法(@PostConstruct)的執行,B 只是一個半成品。
  • 為 B 建立一個 Bean工廠,並放入到 singletonFactories 中。
  • 發現 B 需要注入 A 物件,此時在一級、二級未發現物件A,但是在三級快取中發現了物件 A,從三級快取中得到物件 A,並將物件 A 放入二級快取中,同時刪除三級快取中的物件 A。(注意,此時的 A還是一個半成品,並沒有完成屬性填充和執行初始化方法)
  • 將物件 A 注入到物件 B 中。
  • 物件 B 完成屬性填充,執行初始化方法,並放入到一級快取中,同時刪除二級快取中的物件 B。(此時物件 B 已經是一個成品)
  • 物件 A 得到物件B,將物件 B 注入到物件 A 中。(物件 A 得到的是一個完整的物件 B)
  • 物件 A完成屬性填充,執行初始化方法,並放入到一級快取中,同時刪除二級快取中的物件 A。

我們從原始碼的角度來看一下這個過程:

建立 Bean 的方法在 AbstractAutowireCapableBeanFactory::doCreateBean()

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
	
    if (instanceWrapper == null) {
        // ① 範例化物件
        instanceWrapper = this.createBeanInstance(beanName, mbd, args);
    }
    final Object bean = instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null;
    Class<?> beanType = instanceWrapper != null ? instanceWrapper.getWrappedClass() : null;
   
    // ② 判斷是否允許提前暴露物件,如果允許,則直接新增一個 ObjectFactory 到三級快取
	boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        // 新增三級快取的方法詳情在下方
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    // ③ 填充屬性
    this.populateBean(beanName, mbd, instanceWrapper);
    // ④ 執行初始化方法,並建立代理
    exposedObject = initializeBean(beanName, exposedObject, mbd);
   
    return exposedObject;
}

新增三級快取的方法如下:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) { // 判斷一級快取中不存在此物件
            this.singletonFactories.put(beanName, singletonFactory); // 新增至三級快取
            this.earlySingletonObjects.remove(beanName); // 確保二級快取沒有此物件
            this.registeredSingletons.add(beanName);
        }
    }
}
@FunctionalInterface
public interface ObjectFactory<T> {
	T getObject() throws BeansException;
}

通過這段程式碼,我們可以知道 Spring 在範例化物件的之後,就會為其建立一個 Bean 工廠,並將此工廠加入到三級快取中。

因此,Spring 一開始提前暴露的並不是範例化的 Bean,而是將 Bean 包裝起來的 ObjectFactory。為什麼要這麼做呢?

這實際上涉及到 AOP,如果建立的 Bean 是有代理的,那麼注入的就應該是代理 Bean,而不是原始的 Bean。但是 Spring 一開始並不知道 Bean 是否會有迴圈依賴,通常情況下(沒有迴圈依賴的情況下),Spring 都會在完成填充屬性,並且執行完初始化方法之後再為其建立代理。但是,如果出現了迴圈依賴的話,Spring 就不得不為其提前建立代理物件,否則注入的就是一個原始物件,而不是代理物件。因此,這裡就涉及到應該在哪裡提前建立代理物件?

Spring 的做法就是在 ObjectFactory 中去提前建立代理物件。它會執行 getObject() 方法來獲取到 Bean。實際上,它真正執行的方法如下:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                // 如果需要代理,這裡會返回代理物件;否則返回原始物件
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

因為提前進行了代理,避免對後面重複建立代理物件,會在 earlyProxyReferences 中記錄已被代理的物件。

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 記錄已被代理的物件
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
}

通過上面的解析,我們可以知道 Spring 需要三級快取的目的是為了在沒有迴圈依賴的情況下,延遲代理物件的建立,使 Bean 的建立符合 Spring 的設計原則。

如何獲取依賴

我們目前已經知道了 Spring 的三級依賴的作用,但是 Spring 在注入屬性的時候是如何去獲取依賴的呢?

他是通過一個getSingleton()方法去獲取所需要的 Bean 的。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 一級快取
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 二級快取
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 三級快取
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // Bean 工廠中獲取 Bean
                    singletonObject = singletonFactory.getObject();
                    // 放入到二級快取中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

當 Spring 為某個 Bean 填充屬性的時候,它首先會尋找需要注入物件的名稱,然後依次執行 getSingleton() 方法得到所需注入的物件,而獲取物件的過程就是先從一級快取中獲取,一級快取中沒有就從二級快取中獲取,二級快取中沒有就從三級快取中獲取,如果三級快取中也沒有,那麼就會去執行 doCreateBean() 方法建立這個 Bean。

流程圖總結:

三、解決迴圈依賴必須要三級快取嗎

我們現在已經知道,第三級快取的目的是為了延遲代理物件的建立,因為如果沒有依賴迴圈的話,那麼就不需要為其提前建立代理,可以將它延遲到初始化完成之後再建立。

既然目的只是延遲的話,那麼我們是不是可以不延遲建立,而是在範例化完成之後,就為其建立代理物件,這樣我們就不需要第三級快取了。因此,我們可以將addSingletonFactory() 方法進行改造。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) { // 判斷一級快取中不存在此物件
            object o = singletonFactory.getObject(); // 直接從工廠中獲取 Bean
            this.earlySingletonObjects.put(beanName, o); // 新增至二級快取中
            this.registeredSingletons.add(beanName);
        }
    }
}

這樣的話,每次範例化完 Bean 之後就直接去建立代理物件,並新增到二級快取中。測試結果是完全正常的,Spring 的初始化時間應該也是不會有太大的影響,因為如果 Bean 本身不需要代理的話,是直接返回原始 Bean 的,並不需要走複雜的建立代理 Bean 的流程。

結論

測試證明,二級快取也是可以解決迴圈依賴的。為什麼 Spring 不選擇二級快取,而要額外多新增一層快取呢?

如果 Spring 選擇二級快取來解決迴圈依賴的話,那麼就意味著所有 Bean 都需要在範例化完成之後就立馬為其建立代理,而Spring 的設計原則是在 Bean 初始化完成之後才為其建立代理。所以,Spring 選擇了三級快取。但是因為迴圈依賴的出現,導致了 Spring 不得不提前去建立代理,因為如果不提前建立代理物件,那麼注入的就是原始物件,這樣就會產生錯誤。

四、無法解決的迴圈依賴問題

1.在主bean中通過建構函式注入所依賴的bean

如下controller為主bean,service為所依賴的bean:

@RestController
public class AccountController {
    private static final Logger LOG = LoggerFactory.getLogger(AccountController.class);
    private AccountService accountService;
    // 建構函式依賴注入
    // 不管是否設定為required為true,都會出現迴圈依賴問題
    @Autowire
    // @Autowired(required = false)
    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }
    
}
@Service
public class AccountService {
    private static final Logger LOG = LoggerFactory.getLogger(AccountService.class);
    
    // 屬性值依賴注入
    @Autowired
    private AccountController accountController;
   } 

啟動列印如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  accountController defined in file [/Users/xieyizun/study/personal-projects/easy-web/target/classes/com/yzxie/easy/log/web/controller/AccountController.class]
↑     ↓
|  accountService (field private com.yzxie.easy.log.web.controller.AccountController com.yzxie.easy.log.web.service.AccountService.accountController)
└─────┘

如果是在主bean中通過屬性值或者setter方法注入所依賴的bean,而在所依賴的bean使用了建構函式注入主bean物件,這種情況則不會出現迴圈依賴問題。

@RestController
public class AccountController {
    private static final Logger LOG = LoggerFactory.getLogger(AccountController.class);
    // 屬性值注入
    @Autowired
    private AccountService accountService;
    
}
@Service
public class AccountService {
    private AccountController accountController;
    // 建構函式注入
    @Autowired
    public AccountService(AccountController accountController) {
        this.accountController = accountController;
    }
    
}

2.總結

  • 當存在迴圈依賴時,主bean物件不能通過建構函式的方式注入所依賴的bean物件,而所依賴的bean物件則不受限制,即可以通過三種注入方式的任意一種注入主bean物件。
  • 如果主bean物件通過建構函式方式注入所依賴的bean物件,則無論所依賴的bean物件通過何種方式注入主bean,都無法解決迴圈依賴問題,程式無法啟動。(其實在主bean加上@Lazy也能解決)

原因主要是主bean物件通過建構函式注入所依賴bean物件時,無法建立該所依賴的bean物件,獲取該所依賴bean物件的參照。因為如下程式碼所示。

建立主bean物件,呼叫順序為:

  • 1.呼叫建構函式
  • 2. 放到三級快取
  • 3. 屬性賦值。其中呼叫建構函式時會觸發所依賴的bean物件的建立。
    // bean物件範例建立的核心實現方法
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    		throws BeanCreationException {
    		// 省略其他程式碼
    		// 1. 呼叫建構函式建立該bean物件,若不存在建構函式注入,順利通過
    		instanceWrapper = createBeanInstance(beanName, mbd, args);
    		// 2. 在singletonFactories快取中,放入該bean物件,以便解決迴圈依賴問題
    		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    		// 3. populateBean方法:bean物件的屬性賦值
    		populateBean(beanName, mbd, instanceWrapper); 		
    		// 省略其他程式碼
    	return exposedObject;
    }

createBeanInstance是呼叫建構函式建立主bean物件,在裡面會注入建構函式中所依賴的bean,而此時並沒有執行到addSingletonFactory方法來新增主bean物件的建立工廠到三級快取singletonFactories中。故在createBeanInstance內部,注入和建立該主bean物件時,如果在建構函式中存在對其他bean物件的依賴,並且該bean物件也存在對主bean物件的依賴,則會出現迴圈依賴問題,原理如下:

主bean物件為A,A物件依賴於B物件,B物件也存在對A物件的依賴,建立A物件時,會觸發B物件的建立,則B無法通過三級快取機制獲取主bean物件A的參照(即B如果通過建構函式注入A,則無法建立B物件;如果通過屬性注入或者setter方法注入A,則建立B物件後,對B物件進行屬性賦值,會卡在populateBean方法也無法返回)。 故無法建立主bean物件所依賴的B,建立主bean物件A時,createBeanInstance方法無法返回,出現程式碼死鎖,程式報迴圈依賴錯誤。

注意:spring的迴圈依賴其實是可以關閉的,設定allowCircularReference=false

以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com