關於spring循環依賴問題及解決方案

一、三種循環依賴的情況

①構造器的循環依賴:

  • 這種依賴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

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: