高頻面試題:一張圖徹底搞懂Spring迴圈依賴

Java大將軍發表於2021-10-30

1 什麼是迴圈依賴?

如下圖所示:

圖片

點選並拖拽以移動

BeanA類依賴了BeanB類,同時BeanB類又依賴了BeanA類。這種依賴關係形成了一個閉環,我們把這種依賴關係就稱之為迴圈依賴。同理,再如下圖的情況:

圖片

點選並拖拽以移動

上圖中,BeanA類依賴了BeanB類,BeanB類依賴了BeanC類,BeanC類依賴了BeanA類,如此,也形成了一個依賴閉環。再比如:

圖片

點選並拖拽以移動

上圖中,自己引用了自己,自己和自己形成了依賴關係。同樣也是一個依賴閉環。那麼,如果出現此類迴圈依賴的情況,會出現什麼問題呢?

2 迴圈依賴問題復現

2.1 定義依賴關係

我們繼續擴充套件前面的內容,給ModifyService增加一個屬性,程式碼如下:

@GPService
public class ModifyService implements IModifyService {
​
    @GPAutowired private QueryService queryService;
​
    ...
​
}

點選並拖拽以移動

給QueryService增加一個屬性,程式碼如下:

@GPService
@Slf4j
public class QueryService implements IQueryService {
​
    @GPAutowired private ModifyService modifyService;
​
    ...
​
}

點選並拖拽以移動

如此,ModifyService依賴了QueryService,同時QueryService也依賴了ModifyService,形成了依賴閉環。那麼這種情況下會出現什麼問題呢?

2.2 問題復現

我們來執行除錯一下之前的程式碼,在GPApplicationContext初始化後打上斷點,我們來跟蹤一下IoC容器裡面的情況,如下圖:

圖片

點選並拖拽以移動

啟動專案,我們發現只要是有迴圈依賴關係的屬性並沒有自動賦值,而沒有迴圈依賴關係的屬性均有自動賦值,如下圖所示:

圖片

點選並拖拽以移動

這種情況是怎麼造成的呢?我們分析原因之後發現,因為,IoC容器對Bean的初始化是根據BeanDefinition迴圈迭代,有一定的順序。這樣,在執行依賴注入時,需要自動賦值的屬性對應的物件有可能還沒初始化,沒有初始化也就沒有對應的例項可以注入。於是,就出現我們看到的情況。

3 使用快取解決迴圈依賴問題

圖片

點選並拖拽以移動

3.1 定義快取

具體程式碼如下:

// 迴圈依賴的標識---當前正在建立的例項bean
    private Set<String> singletonsCurrectlyInCreation = new HashSet<String>();
​
    //一級快取
    private Map<String, Object> singletonObjects = new HashMap<String, Object>();
​
    // 二級快取: 為了將成熟的bean和純淨的bean分離. 避免讀取到不完整的bean.
private Map<String, Object> earlySingletonObjects = new HashMap<String, Object>();

點選並拖拽以移動

3.2 判斷迴圈依賴

增加getSingleton()方法:

/**
     * 判斷是否是迴圈引用的出口.
     * @param beanName
     * @return
     */
    private Object getSingleton(String beanName,GPBeanDefinition beanDefinition) {
​
        //先去一級快取裡拿,
        Object bean = singletonObjects.get(beanName);
        // 一級快取中沒有, 但是正在建立的bean標識中有, 說明是迴圈依賴
        if (bean == null && singletonsCurrentlyInCreation.contains(beanName)) {
​
            bean = earlySingletonObjects.get(beanName);
            // 如果二級快取中沒有, 就從三級快取中拿
            if (bean == null) {
                // 從三級快取中取
                Object object = instantiateBean(beanName,beanDefinition);
​
                // 然後將其放入到二級快取中. 因為如果有多次依賴, 就去二級快取中判斷. 已經有了就不在再次建立了
                earlySingletonObjects.put(beanName, object);
​
​
            }
        }
        return bean;
    }

點選並拖拽以移動

3.3 新增快取

修改getBean()方法,在getBean()方法中新增如下程式碼:

         //Bean的例項化,DI是從而這個方法開始的
    public Object getBean(String beanName){
​
        //1、先拿到BeanDefinition配置資訊
        GPBeanDefinition beanDefinition = regitry.beanDefinitionMap.get(beanName);
​
        // 增加一個出口. 判斷實體類是否已經被載入過了
        Object singleton = getSingleton(beanName,beanDefinition);
        if (singleton != null) { return singleton; }
​
        // 標記bean正在建立
        if (!singletonsCurrentlyInCreation.contains(beanName)) {
            singletonsCurrentlyInCreation.add(beanName);
        }
​
        //2、反射例項化newInstance();
        Object instance = instantiateBean(beanName,beanDefinition);
​
        //放入一級快取
        this.singletonObjects.put(beanName, instance);
​
        //3、封裝成一個叫做BeanWrapper
        GPBeanWrapper beanWrapper = new GPBeanWrapper(instance);
        //4、執行依賴注入
        populateBean(beanName,beanDefinition,beanWrapper);
        //5、儲存到IoC容器
        factoryBeanInstanceCache.put(beanName,beanWrapper);
​
        return beanWrapper.getWrapperInstance();
​
        }

點選並拖拽以移動

3.4 新增依賴注入

修改populateBean()方法,程式碼如下:

    private void populateBean(String beanName, GPBeanDefinition beanDefinition, GPBeanWrapper beanWrapper) {
​
        ...
​
            try {
​
                //ioc.get(beanName) 相當於通過介面的全名拿到介面的實現的例項
                field.set(instance,getBean(autowiredBeanName));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                continue;
            }
        ...
​
    }

點選並拖拽以移動

4 迴圈依賴對AOP建立代理物件的影響

4.1 迴圈依賴下的代理物件建立過程

我們都知道Spring AOP、事務等都是通過代理物件來實現的,而事務的代理物件是由自動代理建立器來自動完成的。也就是說Spring最終給我們放進容器裡面的是一個代理物件,而非原始物件。

這裡我們結合迴圈依賴,再分析一下AOP代理物件的建立過程和最終放進容器內的動作,看如下程式碼:

@Service
public class MyServiceImpl implements MyService {
    @Autowired
    private MyService myService;
​
    @Transactional
    @Override
    public Object hello(Integer id) {
        return "service hello";
    }
}

點選並拖拽以移動

此Service類使用到了事務,所以最終會生成一個JDK動態代理物件Proxy。剛好它又存在自己引用自己的迴圈依賴的情況。跟進到Spring建立Bean的原始碼部分,來看doCreateBean()方法:

protected Object doCreateBean( ... ){
​
        ...
​
        // 如果允許迴圈依賴,此處會新增一個ObjectFactory到三級快取裡面,以備建立物件並且提前暴露引用
        // 此處Tips:getEarlyBeanReference是後置處理器SmartInstantiationAwareBeanPostProcessor的一個方法,
        // 主要是保證自己被迴圈依賴的時候,即使被別的Bean @Autowire進去的也是代理物件
        // AOP自動代理建立器此方法裡會建立的代理物件
​
        // 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) { // 需要提前暴露(支援迴圈依賴),註冊一個ObjectFactory到三級快取
                addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }
​
        // 如果發現自己被迴圈依賴,會執行上面的getEarlyBeanReference()方法,從而建立一個代理物件從三級快取轉移到二級快取裡
        // 注意此時候物件還在二級快取裡,並沒有在一級快取。並且此時可以知道exposedObject仍舊是原始物件    populateBean(beanName, mbd, instanceWrapper);
        exposedObject = initializeBean(beanName, exposedObject, mbd);
​
        // 經過這兩大步後,exposedObject還是原始物件
        // 注意:此處是以事務的AOP為例
        // 因為事務的AOP自動代理建立器在getEarlyBeanReference()建立代理後,
     // initializeBean() 就不會再重複建立了,二選一,下面會有詳細描述)
​
        ...
​
        // 迴圈依賴校驗(非常重要)
        if (earlySingletonExposure) {
                // 前面講到因為自己被迴圈依賴了,所以此時候代理物件還存放在二級快取中
                // 因此,此處getSingleton(),就會把代理物件拿出來
                // 然後賦值給exposedObject物件並返回,最終被addSingleton()新增進一級快取中
                // 這樣就保證了我們容器裡快取的物件實際上是代理物件,而非原始物件
​
                Object earlySingletonReference = getSingleton(beanName, false);
                if (earlySingletonReference != null) {
​
                        // 這個判斷不可少(因為initializeBean()方法中給exposedObject物件重新賦過值,否則就是是兩個不同的物件例項)
                        if (exposedObject == bean) {                 
                                exposedObject = earlySingletonReference;
                        }
                }
                ...
        }
​
}

點選並拖拽以移動

以上程式碼分析的是代理物件有自己存在迴圈依賴的情況,Spring用三級快取很巧妙的進行解決了這個問題。

4.2 非迴圈依賴下的代理物件建立過程

如果自己並不存在迴圈依賴的情況,Spring的處理過程就稍微不同,繼續跟進原始碼:

protected Object doCreateBean( ... ) {
        ...

        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

        ...

        // 此處注意,因為沒有迴圈引用,所以上面getEarlyBeanReference()方法不會執行
        // 也就是說此時二級快取裡並不會存在
        populateBean(beanName, mbd, instanceWrapper);

        // 重點在此
        //AnnotationAwareAspectJAutoProxyCreator自動代理建立器此處的postProcessAfterInitialization()方法裡,會給建立一個代理物件返回
        // 所以此部分執行完成後,exposedObject() 容器中快取的已經是代理物件,不再是原始物件
     // 此時二級快取裡依舊無它,更別提一級快取了
     exposedObject = initializeBean(beanName, exposedObject, mbd);

        ...

        // 迴圈依賴校驗
        if (earlySingletonExposure) {
                // 前面講到一級、二級快取裡都沒有快取,然後這裡傳引數是false,表示不從三級快取中取值
                // 因此,此時earlySingletonReference = null ,並直接返回

                // 然後執行addSingleton()方法,由此可知,容器裡最終存在的也還是代理物件

                Object earlySingletonReference = getSingleton(beanName, false);
                if (earlySingletonReference != null) {
                        if (exposedObject == bean) { 
                                exposedObject = earlySingletonReference;
                        }
                }
             ...
}

點選並拖拽以移動

根據以上程式碼分析可知,只要用到代理,沒有被迴圈引用的,最終存在Spring容器裡快取的仍舊是代理物件。如果我們關閉Spring容器的迴圈依賴,也就是把allowCircularReferences設值為false,那麼會不會出現問題呢?先關閉迴圈依賴開關。

// 它用於關閉迴圈引用(關閉後只要有迴圈引用現象將報錯)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        ((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(false);

    }
}

點選並拖拽以移動

關閉迴圈依賴後,上面程式碼中存在A、B迴圈依賴的情況,執行程式會出現如下異常:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)

點選並拖拽以移動

此處異常型別也是BeanCurrentlyInCreationException異常,但報錯位置在DefaultSingletonBeanRegistry.beforeSingletonCreation

我們來分析一下,在例項化A後給其屬性賦值時,Spring會去例項化B。B例項化完成後會繼續給B屬性賦值,由於我們關閉了迴圈依賴,所以不存在提前暴露引用。因此B無法直接拿到A的引用地址,只能又去建立A的例項。而此時我們知道A其實已經正在建立中了,不能再建立了。所有就出現了異常。對照演示程式碼,來分析一下程式執行過程:

@Service
public class MyServiceImpl implements MyService {
​
    // 因為關閉了迴圈依賴,所以此處不能再依賴自己
    // 但是MyService需要建立AOP代理物件
    //@Autowired
    //private MyService myService;
​
    @Transactional
    @Override
    public Object hello(Integer id) {
        return "service hello";
    }
}

點選並拖拽以移動

其大致執行步驟如下:

protected Object doCreateBean( ... ) {
​
        // earlySingletonExposure = false  也就是Bean都不會提前暴露引用,因此不能被迴圈依賴
​
        boolean earlySingletonExposure = (mbd.isSingleton() && 
                                                    this.allowCircularReferences && 
                                                    isSingletonCurrentlyInCreation(beanName));
        ...
​
        populateBean(beanName, mbd, instanceWrapper);
​
        // 若是開啟事務,此處會為原生Bean建立代理物件
        exposedObject = initializeBean(beanName, exposedObject, mbd);
​
        if (earlySingletonExposure) {
                ... 
​
                // 因為上面沒有提前暴露代理物件,所以上面的代理物件exposedObject直接返回。
​
        }
}

點選並拖拽以移動

由上面程式碼可知,即使關閉迴圈依賴開關,最終快取到容器中的物件仍舊是代理物件,顯然@Autowired給屬性賦值的也一定是代理物件。

最後,以AbstractAutoProxyCreator為例看看自動代理建立器實現迴圈依賴代理物件的細節。

AbstractAutoProxyCreator是抽象類,它的三大實現子類InfrastructureAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator、AnnotationAwareAspectJAutoProxyCreator小夥伴們應該比較熟悉,該抽象類實現了建立代理的動作:

// 該類實現了SmartInstantiationAwareBeanPostProcessor介面 ,通過getEarlyBeanReference()方法解決迴圈引用問題
​
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
​
    ...
​
    // 下面兩個方法是自動代理建立器建立代理物件的唯二的兩個節點:
​
    // 提前暴露代理物件的引用,在postProcessAfterInitialization之前執行
    // 建立好後放進快取earlyProxyReferences中,注意此處value是原始Bean
​
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
​
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
            this.earlyProxyReferences.put(cacheKey, bean);
            return wrapIfNecessary(bean, beanName, cacheKey);
​
    }
​
    // 因為它會在getEarlyBeanReference之後執行,這個方法最重要的是下面的邏輯判斷
    @Override
    public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
​
        if (bean != null) {
​
            Object cacheKey = getCacheKey(bean.getClass(), beanName);
​
            // 下面的remove()方法返回被移除的value,也就是原始Bean
            // 判斷如果存在迴圈引用,也就是執行了上面的getEarlyBeanReference()方法,
            // 此時remove() 返回值肯定是原始物件
​
            // 若沒有被迴圈引用,getEarlyBeanReference()不執行
            // 所以remove() 方法返回null,此時進入if執行邏輯,呼叫建立代理物件方法
            if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                    return wrapIfNecessary(bean, beanName, cacheKey);
            }
        }
​
        return bean;
​
    }
    ...
}

點選並拖拽以移動

根據以上分析可得知,自動代理建立器它保證了代理物件只會被建立一次,而且支援迴圈依賴的自動注入的依舊是代理物件。由上面分析得出結論,在Spring容器中,不論是否存在迴圈依賴的情況,甚至關閉Spring容器的迴圈依賴功能,它對Spring AOP代理的建立流程有影響,但對結果是無影響的。也就是說Spring很好地遮蔽了容器中物件的建立細節,讓使用者完全無感知。

相關文章