spring 循環依賴以及解決方案(吊打面試官)

思考:

什麼是循環依賴?
Spring怎麼解決循環依賴
Spring對於循環依賴無法解決的場景


1. 什麼是循環依賴?


循環依賴其實就是循環引用,也就是兩個或則兩個以上的bean互相持有對方,最終形成閉環。比如A依賴於B,B依賴於C,C又依賴於A。如下圖:

注意,這裏不是函數的循環調用,是對象的相互依賴關係。循環調用其實就是一個死循環,除非有終結條件。

Spring中循環依賴場景有: 
(1)構造器的循環依賴 
(2)field屬性的循環依賴。

思考:在編碼的過程中,有時候會存在這樣的訴求,那這時候,程序應該先創建哪個對象?

 

Spring怎麼解決循環依賴


Spring的循環依賴的理論依據其實是基於Java的引用傳遞,當我們獲取到對象的引用時,對象的field或則屬性是可以延後設置的(但是構造器必須是在獲取引用之前)。

那spring一個對象產生需要以下幾個步驟

(1)createBeanInstance:實例化,其實也就是調用對象的構造方法實例化對象

(2)populateBean:填充屬性,這一步主要是多bean的依賴屬性進行填充

(3)initializeBean:調用spring xml中的init 方法。

/**
     * 實際創建指定的bean。 此時,預創建處理已經發生,
     * 例如 檢查{@code postProcessBeforeInstantiation}回調。
     * 區分默認bean實例化、使用工廠方法和自動裝配構造函數。
     */
    protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) {
        // Instantiate the bean.
        BeanWrapper instanceWrapper = null;
        .....
        
        if (instanceWrapper == null) {
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        .....
        // Eagerly cache singletons to be able to resolve circular references
        // even when triggered by lifecycle interfaces like BeanFactoryAware.
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            if (logger.isDebugEnabled()) {
                logger.debug("Eagerly caching bean '" + beanName +
                        "' to allow for resolving potential circular references");
            }
            //添加到三級緩存中
            addSingletonFactory(beanName, new ObjectFactory<Object>() {
                @Override
                public Object getObject() throws BeansException {
                    return getEarlyBeanReference(beanName, mbd, bean);
                }
            });
        }

        // Initialize the bean instance.
        Object exposedObject = bean;
        try {
            //填充依賴的bean實例。
            populateBean(beanName, mbd, instanceWrapper);
            if (exposedObject != null) {
                //調用spring xml中的init 方法。
                exposedObject = initializeBean(beanName, exposedObject, mbd);
            }
        }
        ......
        return exposedObject;
    }

其實我們簡單的思考一下就發現,出現循環依賴的問題主要在 (1)和 (2)兩個步驟,也就是也就是(1)中構造器循環依賴和(2)中field循環依賴。

注:createBeanInstance(...)該步驟會調用構造方法

/**
     *使用適當的實例化策略爲指定的bean創建一個新實例:
     * 工廠方法,構造函數自動裝配或簡單實例化。
     * @return BeanWrapper for the new instance
     */
    protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
		
        .......
        // Need to determine the constructor...(確定構造函數)
        Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
        if (ctors != null ||
                mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
                mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
            return autowireConstructor(beanName, mbd, ctors, args);
        }

        // No special handling: simply use no-arg constructor.(使用默認無參構造器,
        //編程時候要求儘量保留無參構造器,因爲你不知道哪個框架在哪會用到)
        return instantiateBean(beanName, mbd);
    }

如何解決?

那麼我們要解決循環引用也應該從初始化過程着手,對於單例來說,在Spring容器整個生命週期內,有且只有一個對象,所以很容易想到這個對象應該存在Cache中,Spring爲了解決單例的循環依賴問題,使用了三級緩存。

/** Cache of singleton objects: bean name --> bean instance */
    //單例對象的cache
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

    /** Cache of singleton factories: bean name --> ObjectFactory */
    //單例對象工廠的cache
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

    /** Cache of early singleton objects: bean name --> bean instance */
    //提前暴光的單例對象的Cache
    private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

 

步驟:

1.Spring首先從一級緩存singletonObjects中獲取。

2.如果獲取不到,並且對象正在創建中,就再從二級緩存earlySingletonObjects中獲取。

3.如果還是獲取不到且允許singletonFactories通過getObject()獲取,就從三級緩存singletonFactory.getObject()(三級緩存)獲取.

4.如果從三級緩存中獲取到就從singletonFactories中移除,並放入earlySingletonObjects中。其實也就是從三級緩存移動到了二級緩存。

/**
     * 返回指定名稱(原始)單例對象。
     * 檢查已經實例化的單例,也允許對當前創建的單例對象的早期引用(用於解決循環依賴)
     * 
     * @return the registered singleton object, or {@code null} if none found
     */
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        //Spring首先從一級緩存singletonObjects中獲取。
        Object singletonObject = this.singletonObjects.get(beanName);

        //isSingletonCurrentlyInCreation()判斷當前單例bean是否正在創建中,也就是沒有初始化
        //完成(比如A的構造器依賴了B對象所以得先去創建B對象, 或則在A的populateBean過程中依
        //賴了B對象,得先去創建B對象,這時的A就是處於創建中的狀態。)
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            synchronized (this.singletonObjects) {

                //嘗試從二級緩存earlySingletonObjects中獲取。
                singletonObject = this.earlySingletonObjects.get(beanName);

                //allowEarlyReference 是否允許從singletonFactories中通過getObject拿到對象
                if (singletonObject == null && allowEarlyReference) {
                    //嘗試從三級緩存singletonFactory.getObject()(三級緩存)獲取
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

                    //如果從三級緩存中獲取到
                    // 則從singletonFactories中移除,並放入earlySingletonObjects中。
                    // 其實也就是從三級緩存移動到了二級緩存。
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }

 

從三級緩存的分析來看,Spring解決循環依賴的訣竅就在於singletonFactories這個滴三級cache。這個cache的類型是ObjectFactory,定義如下:

/**
 * 定義一個可以返回對象實例的工廠
 * @param <T>
 */
public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

接口在這個方法中被引用,該接口在doCreateBean(...) 方法中 實例化步驟進行了調用

/**
     *  添加一個構建指定單例對象的單例工廠
     *  緊急註冊單例對象,用於解決解決循環依賴問題
     * To be called for eager registration of singletons, e.g. to be able to
     * resolve circular references.
     */
    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);
            }
        }
    }

 

這裏就是解決循環依賴的關鍵,這段代碼發生在doCreateBean(...) 方法中 createBeanInstance之後,也就是說單例對象此時已經被創建出來(調用了構造器)。這個對象已經被生產出來了,雖然還不完美(還沒有進行初始化的第二步和第三步),但是已經能被人認出來了(根據對象引用能定位到堆中的對象),所以Spring此時將這個對象提前曝光出來讓大家認識,讓大家使用。

這樣做有什麼好處呢?讓我們來分析一下“A的某個field或者setter依賴了B的實例對象,同時B的某個field或者setter依賴了A的實例對象”這種循環依賴的情況。A首先完成了初始化的第一步,並且將自己提前曝光到singletonFactories中,此時進行初始化的第二步,發現自己依賴對象B,此時就嘗試去get(B),發現B還沒有被create,所以走create流程,B在初始化第一步的時候發現自己依賴了對象A,於是嘗試get(A),嘗試一級緩存singletonObjects(肯定沒有,因爲A還沒初始化完全),嘗試二級緩存earlySingletonObjects(也沒有),嘗試三級緩存singletonFactories,由於A通過ObjectFactory將自己提前曝光了,所以B能夠通過ObjectFactory.getObject拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀),B拿到A對象後順利完成了初始化階段1、2、3,完全初始化之後將自己放入到一級緩存singletonObjects中。此時返回A中,A此時能拿到B的對象順利完成自己的初始化階段2、3,最終A也完成了初始化,進去了一級緩存singletonObjects中,而且更加幸運的是,由於B拿到了A的對象引用,所以B現在hold住的A對象完成了初始化。(簡單來說,就是spring創造了一個 循環依賴的結束點標識

怎麼樣的循環依賴無法處理?

(1)因爲加入singletonFactories三級緩存的前提是執行了構造器來創建半成品的對象,所以構造器的循環依賴沒法解決。因此,Spring不能解決“A的構造方法中依賴了B的實例對象,同時B的構造方法中依賴了A的實例對象”這類問題了!

(2)spring不支持原型(prototype)bean屬性注入循環依賴,不同於構造器注入循環依賴會在創建spring容器context時報錯,它會在用戶執行代碼如context.getBean()時拋出異常。因爲對於原型bean,spring容器只有在需要時纔會實例化,初始化它。

因爲spring容器不緩存prototype類型的bean,使得無法提前暴露出一個創建中的bean。spring容器在獲取prototype類型的bean時,如果因爲環的存在,檢測到當前線程已經正在處理該bean時,就會拋出異常。核心代碼

public abstract class AbstractBeanFactory{
	/** Names of beans that are currently in creation */
	private final ThreadLocal<Object> prototypesCurrentlyInCreation =
			new NamedThreadLocal<>("Prototype beans currently in creation");
			
	protected boolean isPrototypeCurrentlyInCreation(String beanName) {
		Object curVal = this.prototypesCurrentlyInCreation.get();
		// 如果beanName已經存在於正在處理中的prototype類型的bean集中,後面會拋出異常
		return (curVal != null &&
				(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
	}
}

 

總結:

  學習一個新的知識或者技能的時候,我們不僅僅要關注它爲什麼會存在(價值),它的出現幫助我們解決了什麼問題,帶來了怎樣的便利,同時我們也應該思考它的不足,它在哪些情況下(場景)無法使用,那這個時候我們應該怎麼辦,不要當問題真的出現的時候讓自己顯得焦慮不安.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章