一萬字深度剖析Spring循環依賴

開篇詞

距離上一篇文章更新已經有一年多時間了,之前寫的大都是偏向于基礎性的知識,也沒有摻雜過多個人的思考。而自己一直以來都想寫一些更加有深度的內容,這篇文章就是基于這樣一個想法的嘗試,希望讀者能夠有更多的收獲。

為什么要讀源碼?

源碼的學習其實是一個非常枯燥的過程,也是一個難以持之以恒的事情。然而越是難的事情,越是能讓人成長。在此之前我一直沒有勇氣深入閱讀和研究源碼,覺得以自己目前一兩年的經驗去接觸源碼為時過早,相信很多小伙伴也有同樣的想法。然而事實卻并非如此,這只是我們內心懼怕源碼而找的一種借口罷了,源碼遠沒有大家想的那么可怕。

那么讀源碼究竟有什么好處呢?

  1. 可以幫助你更深刻地理解內部設計原理,提升你的系統架構能力和代碼功力
  2. 可以幫你快速定位問題并制定調優方案,減少解決問題的時間成本
  3. 如果你足夠優秀,你還能參加技術開源社區,成為一名代碼貢獻者
  4. 大廠面試的“加分項”甚至是“必選項”

今天我以這篇文章帶你一起走進源碼的世界,初步感受探索源碼的樂趣。在java面試中一道常被問到的經典Spring面試題:Spring究竟是如何解決的循環依賴?相信很多看到相關博客、文章的小伙伴都能從容的給出答案:Spring通過提前曝光機制,使用三級緩存解決了循環依賴。

那么僅僅知道這個就足夠了嗎?為了了解你對Spring框架的掌握程序,聰明的面試官還會繼續追問:

  1. 你能從源碼角度來講解下Spring具體的解決流程嗎?
  2. 為什么要使用三級緩存呢,兩級不可以嗎?
  3. 為什么spring官方更推薦使用構造注入呢?

類似的問題還有很多,這些問題其實背后蘊含著Spring的一些核心技術點,包括:

  • Spring Bean的創建流程
  • Spring的三級緩存機制
  • Spring AOP的相關原理

下面我會對這些知識點進行深入的講解,相信通過這篇文章的學習你一定能夠從容的回答類似的問題了。

Spring Bean的生命周期

說到Spring Bean的創建流程,很多人可能會有這樣的疑問:創建Bean不就是調用構造方法實例化對象嗎?答案顯然是否定的,Spring Bean是整個Spring IOC容器的核心,是有完整的生命周期的。學過Spring的小伙伴對下面這張圖應該都不陌生,圖中展示的就是Spring Bean生命周期的各個階段了。

那么如何進行驗證呢?很簡單,我們寫個Spring的測試程序即可。

Spring測試程序

首先使用開發工具創建一個普通的Maven工程,然后按照下面步驟完善整個測試程序。

  1. 引入Maven依賴
        <!-- 引入spring ioc容器功能 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.15.RELEASE</version>
        </dependency>
        <!-- 引入java注解 -->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- 添加junit測試 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
  1. 創建測試Bean
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class RuoxiyuanBean implements BeanNameAware, BeanFactoryAware, 
    ApplicationContextAware, InitializingBean, DisposableBean {

    private String status;

    public RuoxiyuanBean() {
        System.out.println("實例化Bean...");
    }
    @Override
    public void setBeanName(String name) {
        System.out.println("調用BeanNameAware的setBeanName方法...");
    }
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("調用BeanFactoryAware的setBeanFactory方法...");
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("調用ApplicationContextAware的setApplicationContext方法...");
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("調用InitializingBean的afterPropertiesSet方法...");
    }
    public void initMethod(){
        System.out.println("調用定制的初始化方法init-method...");
    }
    //實際使用較多一些
    @PostConstruct
    public void postConstruct(){
        System.out.println("調用postConstruct方法....");
    }
    @PreDestroy
    public void preDestroy(){
        System.out.println("調用preDestroy方法....");
    }
    @Override
    public void destroy() throws Exception {
        System.out.println("調用DisposableBean的destroy方法");
    }
    public void destroyMethod(){
        System.out.println("調用定制的銷毀方法destroy-method...");
    }
}

繼續創建一個BeanPostProcessor類

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if("ruoxiyuanBean".equalsIgnoreCase(beanName)){
            System.out.println("調用BeanPostProcessor的預初始化方法...");
        }
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if("ruoxiyuanBean".equalsIgnoreCase(beanName)){
            System.out.println("調用BeanPostProcessor的后初始化方法...");
        }
        return bean;
    }
}
  1. 在resources目錄下新建配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="ruoxiyuanBean" class="com.rxy.RuoxiyuanBean" init-method="initMethod" destroy-method="destroyMethod"></bean>
    <bean id="myBeanPostProcessor" class="com.rxy.MyBeanPostProcessor"></bean>
    <!-- 開啟注解掃描 -->
    <context:component-scan base-package="com.rxy" />
</beans>
  1. 在test/java目錄下創建測試類
import com.rxy.RuoxiyuanBean;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringTest {
    @Test
    public void testIoC() {
        ClassPathXmlApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        RuoxiyuanBean ruoxiyuanBean = applicationContext.getBean(RuoxiyuanBean.class);
        System.out.println(ruoxiyuanBean);
        applicationContext.close();  //關閉容器
    }
}

選中測試類運行,打印情況如下圖所示,和我們的預期結果是一致的。細心的小伙伴可能會發現【設置屬性】這個步驟并沒有體現,別著急,我們把它放到源碼階段來揭曉

值的說明的是 @PostConstruct@PreDestroy 注解并不是由Spring提供,而是從Java EE5規范開始新增的兩個影響Servlet生命周期的注解。其中 @PostConstruct 修飾的方法會在服務器加載Servlet的時候運行,并且只會被服務器執行一次,實際開發中我們常使用該注解來對項目做一些初始化操作。

源碼分析Spring Bean的創建流程

有了前面的鋪墊我們就可以順利的進入到源碼分析階段了,我們想知道Spring Bean的創建流程是怎樣的,并且都是在什么時候哪個方法里調用這些生命周期方法的?下面我們逐步進行源碼分析。

1. Spring源碼構建

為了更好的閱讀和研究源碼,我們需要構建Spring源碼工程,Spring源碼構建過程并不復雜,但是比較耗費時間,可以通過搜索引擎參考對應的文章,這里僅給出主要步驟:

  • 下載源碼( github搜索spring)
  • 安裝gradle 5.6.3(類似于maven)
  • 將源碼導入到IDEA中
  • 編譯工程(順序:core-oxm-context-beans-aspects-aop)
    如何編譯:工程—>tasks—>other—>運行compileTestJava
  • 在源碼工程上新建一個gradle類型的module,作為源碼測試工程

測試工程的配置文件,測試類和相關的Bean同我們之前編寫好的測試程序,唯一需要修改的是 build.gradle 文件,這個文件是gradle項目的構建文件(類似于pom.xml)。

sourceCompatibility = 11

repositories {
  mavenCentral()
}

dependencies {
  compile(project(":spring-context"))
  testCompile group: 'junit',name: 'junit',version: '4.12'
}
2. 源碼閱讀技巧

構建完源碼后,我們該從何入手?相信很多人第一次接觸到源碼都會有種手無足措的感覺。實際上源碼分析也是講究方法和技巧的。

  • 讀源碼原則
    定焦原則:抓主線
    宏觀原則:站在上帝視角,關注源碼結構和業務流程(淡化具體某行代碼的編寫細節)
  • 讀源碼技巧
    關鍵位置斷點(觀察調用棧)
    善用反調( Find Usages)
    積累經驗(比如spring框架中doXXX方法,真正做處理的地方)

通常來說,閱讀大型項目的源碼無外乎兩種方法:

  • 自上而下(Top-Down):從最頂層或最外層的代碼一步步深入。通俗地說,就是從 main 函數開始閱讀,逐漸向下層層深入,直到抵達最底層代碼。這個方法的好處在于,你遍歷的是完整的頂層功能路徑,這對于你了解各個功能的整體流程極有幫助。
  • 自下而上(Bottom-Up):跟自上而下相反,是指先獨立地閱讀和搞懂每個組件的代碼和實現機制,然后不斷向上延展,并最終把它們組裝起來。該方法不是沿著功能的維度向上溯源的,相反地,它更有助于你掌握底層的基礎組件代碼。
3. Bean生命周期關鍵時機點

讀源碼不能著急,首先我們要明確每次讀源碼的目的或者說要解決什么問題。那么本次我們的目的就是了解SpringBean的具體創建流程,并由此引申的問題就是SpringBean在什么地方調用的各個生命周期方法?

明確目的后我們就可以開始打斷點分析了,斷點要打在關鍵位置,那么具體應該在哪?不難想到應該是在各個生命周期方法上,我們通過對比觀察不同方法的調用棧從而找到一些線索。

舉個例子,在RuoxiyuanBean的構造方法上打上斷點,然后以debug模式運行測試程序,把詳細的調用棧信息記錄下來,如下圖所示:

調用棧的具體信息:

同樣的方式,我們去掉上一個斷點,然后在afterPropertiesSet方法上打上新的斷點,運行測試類并記錄調用棧:

其他的生命周期方法也都可以按照這種方式記錄下調用棧,這里就不在做演示了。通過這些調用棧圖對比我們能得到兩個重要的信息:

  1. 通過觀察左邊的調用棧,我們發現一個規律,這些生命周期方法的層級調用從 refresh() 方法一直到 doCreateBean() 方法都是一致的,因此我們可以斷定這些方法的調用過程就是Spring Bean的創建子流程
  2. 這些生命周期方法的調用棧從 doCreateBean() 方法后就分道揚鑣了,各自調用了不同的方法完成相關的邏輯。因此我們可以確定 doCreateBean() 方法就是完成生命周期方法調用的關鍵步驟。

下面我們針對這些信息做進一步的說明。

4. Spring IoC容器初始化主流程

根據上面的調試分析,我們發現 Bean對象創建的幾個關鍵時機點代碼層級的調用都在AbstractApplicationContext 類 的 refresh 方法中,可見這個方法對于Spring IoC 容器初始化來說相當關鍵。

在Spring IoC容器中還有一個重要的接口:BeanFactoryPostProcessor(bean工廠的后置處理容器)。我們同樣可以創建該接口的實現類并在實現方法上打斷點觀察調用棧,最終得到的匯總信息如下:

毫無意外這個接口對應實現類的實例化及方法調用也都是在refresh方法中,那么我們就通過查看 refresh 方法的源碼來俯瞰容器創建的主體流程。核心源碼具體如下:

@Override
public void refresh() throws BeansException, IllegalStateException {
    //對象鎖加鎖
    synchronized (this.startupShutdownMonitor) {
        prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        prepareBeanFactory(beanFactory);
        try {
            postProcessBeanFactory(beanFactory);
            invokeBeanFactoryPostProcessors(beanFactory);
            registerBeanPostProcessors(beanFactory);
            initMessageSource();
            initApplicationEventMulticaster();
            onRefresh();
            registerListeners();
            finishBeanFactoryInitialization(beanFactory);
            finishRefresh();
        } catch (BeansException ex) {
            ...
        }
    }
}

從源碼中可以看出refresh方法中一共調用了12個子方法來完成整個IOC容器的創建邏輯。下面用一張圖來說明這些方法的具體作用:

5. Spring IoC的容器體系

不知道大家是否注意到refresh方法中的帶參數的子方法傳遞的參數都是beanFactory對象,實際的類是DefaultListableBeanFactory,而該類實現了BeanFactory接口,講到這我們就擴展一下關于Spring IoC的容器體系。

IoC容器是Spring的核心模塊,是抽象了對象管理、依賴關系管理的框架解決方案。Spring 提供了很多的容器,其中 BeanFactory 是頂層容器(根容器),不能被實例化,它定義了所有 IoC 容器必須遵從的一套原則,具體的容器實現可以增加額外的功能,比如我們常用到的ApplicationContext,其下更具體的實現如 ClassPathXmlApplicationContext 包含了解析 xml 等一系列的內容,AnnotationConfigApplicationContext 則是包含了注解解析等一系列的內容。Spring IoC 容器繼承體系非常優雅,需要使用哪個層次用哪個層次即可,不必使用功能大而全的。

BeanFactory 頂級接口方法棧如下:

BeanFactory 容器繼承體系:

通過其接口設計,我們可以看到我們一貫使用的 ApplicationContext 除了繼承BeanFactory的子接口,還繼承了ResourceLoader、MessageSource等接口,因此其提供的功能也就更豐富了。

6. Spring Bean的創建子流程

通過最開始的關鍵時機點分析,我們知道Bean創建子流程入口在AbstractApplicationContext#refresh()方法的finishBeanFactoryInitialization(beanFactory) 處。接下來我們通過調試模式走一遍這個創建子流程。

進入finishBeanFactoryInitialization方法,關鍵邏輯在最后一行

繼續進入DefaultListableBeanFactory類的preInstantiateSingletons方法,我們找到下面部分的代碼,看到工廠Bean或者普通Bean,最終都是通過getBean的方法獲取實例

下一步進入AbstractBeanFactory#getBean(每個Bean創建的真正入口)方法,該方法又調用了doGetBean方法。

繼續跟蹤下去,我們進入到了AbstractBeanFactory類的doGetBean方法,這個方法中的代碼很多,我們直接找到核心部分,通過getSingleton方法獲取bean實例(注意參數二是一個lamda表達式)

接著進入到DefaultSingletonBeanRegistry類的getSingleton方法,這里又回調了參數二(lmbda表達式)的getObject方法(也就是上一步的createBean()方法)。

接著進入到AbstractAutowireCapableBeanFactory#createBean(真真正正創建Bean)方法,找到以下代碼部分:

最終調用doCreateBean方法獲取到了bean實例并一步步往上返回該實例對象。

由于這里Bean創建的子流程不是重點內容,因此這里并沒有將完整的源碼貼出。我們通過一張時序圖來描述整個調用過程。

前面我們通過測試程序了解了Spring Bean的生命周期,這次我們通過源碼方式來進一步驗證生命周期涉及的各個過程。關鍵方法就是:AbstractAutowireCapableBeanFactory#doCreateBean(),也就是Bean創建子流程的最后一步。
核心源碼如下:

/**
 * Spring Bean生命周期整體執行順序為:
 * Bean實例化 --> 設置屬性 --> BeanNameAware --> BeanFactoryAware 
 * --> ApplicationContextAware --> BeanPostProcessor#before --> postConstruct
 * --> afterPropertiesSet --> init-method --> BeanPostProcessor#after
 * 所有步驟由該方法完成
 */
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    throws BeanCreationException {
    // 開始實例化bean
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
        // 第一步:創建bean實例
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = instanceWrapper.getWrappedInstance();
    Class<?> beanType = instanceWrapper.getWrappedClass();
    if (beanType != NullBean.class) {
        mbd.resolvedTargetType = beanType;
    }
    //提前暴露對象,加入三級緩存
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                                      isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    // 開始初始化bean實例
    Object exposedObject = bean;
    try {
        //第二步:設置屬性值
        populateBean(beanName, mbd, instanceWrapper);
        // 調用初始化方法,應用BeanPostProcessor后置處理器
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    ...
    return exposedObject;
}
/**
 * 初始化Bean
 * 包括Bean后置處理器初始化
 * Bean的一些初始化方法的執行init-method
 * Bean的實現的聲明周期相關接口的屬性注入
 */
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
    if (System.getSecurityManager() != null) {
        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            invokeAwareMethods(beanName, bean);
            return null;
        }, getAccessControlContext());
    }else {
        // 第 3 步:調用BeanNameAware的setBeanName方法
        // 第 4 步:調用BeanFactoryAware的setBeanFactory方法
        invokeAwareMethods(beanName, bean);
    }
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        // 第 5 步:調用ApplicationContextAware的setApplicationContext方法
        // 第 6 步:調用BeanPostProcessor的預初始化方法
        // 先調用的是ApplicationContextAwareProcessor#invokeAwareInterfaces(也是一個BeanPostProcessor)
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }
    try {
        // 第 7 步:調用InitializingBean的afterPropertiesSet方法: InitializingBean#afterPropertiesSet
        // 第 8 步:調用定制的初始化方法init-method:invokeCustomInitMethod
        invokeInitMethods(beanName, wrappedBean, mbd);
    }
    if (mbd == null || !mbd.isSynthetic()) {
        // 第 9 步:調用BeanPostProcessor的后初始化方法
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }
    return wrappedBean;
}

Spring Bean生命周期源碼圖:

同樣,對于各個生命周期方法的執行細節不是本講的重點,這里不再展開。至此我們對Spring Bean的創建流程就分析完畢了。
有了前面的鋪墊后,我們就可以順序的進入到今天探討的主題——Spring循環依賴的解決方案。

Spring IOC循環依賴問題
1. 什么是循環依賴?

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

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

2. Spring循環依賴場景

我們知道依賴注入方法主要有兩種:構造器注入、Field 屬性注入(setter注入)。而Spring Bean又分為兩種類型:(singleton)單例 bean和(prototype)原型 bean。因此Spring中循環依賴場景有如下四種:

  • 單例 bean 構造器參數循環依賴(無法解決)
  • 單例bean通過setXxx或者@Autowired進行循環依賴
  • 原型 bean構造器參數循環依賴(無法解決)
  • 原型 bean通過setXxx或者@Autowired進行循環依賴(無法解決)

首先講一下原型bean,對于原型bean的初始化過程中不論是通過構造器參數循環依賴還是通過setXxx方法產生循環依賴, Spring都會直接報錯處理。

因為原型模式每次都是重新生成一個全新的bean,根本沒有緩存一說。這將導致實例化A完,填充發現需要B,實例化B完又發現需要A,而每次的A又都要不一樣,所以死循環的依賴下去。唯一的做法就是利用循環依賴檢測,發現原型模式下存在循環依賴并拋出異常。

來看一下Spring對原型Bean的循環依賴處理:核心邏輯在AbstractBeanFactory類的doGetBean方法

// 創建prototype類型對象前的檢查
// 如果是prototype類型并且正在創建中(說明產生了循環依賴),則直接拋出異常(無法處理)
if (isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
}
protected boolean isPrototypeCurrentlyInCreation(String beanName) {
    Object curVal = this.prototypesCurrentlyInCreation.get();
    return (curVal != null &&
            (curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
}
//如果檢查通過則創建prototype類型對象
if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
    Object prototypeInstance = null;
    try {
        // 創建原型bean之前標記bean正在被創建
        beforePrototypeCreation(beanName);
        // 創建原型實例
        prototypeInstance = createBean(beanName, mbd, args);
    }
    finally {
        // 創建原型bean之后移除正在被創建的標記
        afterPrototypeCreation(beanName);
    }
}

通過源碼可知在獲取bean之前如果這個原型bean正在被創建則直接拋出異常。原型bean在創建之前會進行標記這個beanName正在被創建,等創建結束之后會刪除標記。該標記定義AbstractBeanFactory類中。

// Names of beans that are currently in creation
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
            new NamedThreadLocal<>("Prototype beans currently in creation");

結論:顯然Spring 不支持原型 bean 的循環依賴。
那么單例 bean 構造器參數循環依賴為什么也不支持?下面我們會先講一下setter方式循環依賴的處理方法,然后再來回答這個問題。

3. 循環依賴處理機制分析

通常我們分析底層原理時都是基于最簡單的案例進行,這樣有助于我們快速分析出核心的邏輯。因此我們在這里創建兩個簡單的類A和B,并讓它們產生循環依賴關系。

@Component
public class A {
    @Autowired
    private B b;
    public void setB(B b) {
        this.b = b;
    }
}
@Component
public class B {
    @Autowired
    private A a;
    public void setA(A a) {
        this.a = a;
    }
}

有了前面Bean創建過程的源碼分析基礎,這里源碼分析就會跳過一些不必要的過程,直接抓住重點內容進行解讀。
循環依賴源碼調用過程:(A <—> B)
1.開始創建Bean A(入口AbstractBeanFactory#getBean)
該方法又調用了doGetBean方法,首先會嘗試從緩存中獲取Bean實例

2.進入到DefaultSingletonBeanRegistry#getSingleton單參數方法,這里會依次從一級、二級、三級緩存中去獲取,因為當前bean還未開始創建,所以不能獲取到實例。

3.實例化Bean A(AbstractAutowireCapableBeanFactory#doCreateBean)

4.實例化完Bean A后,會提前曝光對象,加入三級緩存

加入三級緩存調用的是DefaultSingletonBeanRegistry#addSingletonFactory方法

5.填充屬性b(AbstractAutowireCapableBeanFactory#populateBean)

進入applyPropertyValues方法,這里需要對屬性值進行解析

繼續進入BeanDefinitionValueResolver的resolveValueIfNecessary方法

繼續進入resolveReference方法

你會發現在這里實際調用了AbstractBeanFactory#getBean(也就是Bean創建流程的入口)
6.開始創建Bean B(6-10步過程同上)
7.嘗試從各級緩存獲取B實例,也未獲取到
8.實例化Bean B
9.提前曝光對象,將Bean B的對象工廠加入三級緩存
10.填充屬性a,顯然又來到了AbstractBeanFactory#getBean方法,此時getBean獲取的是a的實例。
11.嘗試從各級緩存獲取B實例,此時由于Bean A提前暴露在三級緩存,因此通過getSingleton方法獲取到了該實例的對象工廠,并且調用工廠的getObject()方法獲取到了Bean A的引用。(參照第2步代碼示例)
12.回到第10步,此時applyPropertyValues方法獲得了Bean A,然后對屬性a進行賦值操作。

到這里就完成了Bean B生命周期的第二步設置屬性值。
13.BeanB繼續完成后續的初始化操作,也就是createBean方法執行完畢。
14.如果你對前面講的Bean創建子流程的過程還有印象的話,應該知道下一步是回到DefaultSingletonBeanRegistry#getSingleton方法。來看關鍵代碼

查看addSingleton方法,該方法會將BeanB加入到一級緩存中。

到這里整個Bean B的創建過程才算真正完成了。
15.回到第5步(填充屬性b),此時Bean A就通過bean工廠獲得了Bean B實例并完成了屬性的設置。
16.Bean A繼續完成后續的一系列初始化操作
17.最后Bean A也加入到一級緩存中,至此整個過程結束。

整個調試過程還是相對有些復雜的,需要有一定耐心,在調試過程中關注重點,多思考,多記錄。

結論:循環依賴問題采用提前暴露策略解決,提前暴露剛完成構造器注入但是還沒有完成其他步驟的bean的對象工廠。

循環源碼調用過程的時序圖:

【補充說明】關于構造方法的循環依賴

首先對于單例bean而言,和原型bean類似也有一個創造中的標識,在bean開始創建前會先標記為創建中,在創建完成之后會移除標識。相關的源碼如下:

// DefaultSingletonBeanRegistry類
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null) {
            // 創建之前先標識該bean正在被創建,因為springbean創建過程復雜步驟很多,需要標識
            // this.singletonsCurrentlyInCreation.add(beanName)
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            try {
                // 傳過來的調用,lamda表達式使用,從ObjectFactory中獲取bean
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
            } finally {
                // 創建完成之后移除標識
                // this.singletonsCurrentlyInCreation.remove(beanName)
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                //緩存bean對象
                addSingleton(beanName, singletonObject);
            }
        }
        return singletonObject;
    }
}

標識singletonsCurrentlyInCreation定義在DefaultSingletonBeanRegistry類中,存儲了當前正在創建中的所有單例bean名稱。

/** Names of beans that are currently in creation. */
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

其實根據Spring的循環依賴處理機制你應該能明白為什么不支持構造方法的循環依賴。那么我們先通過源碼來驗證這一點并快速找到spring的處理方式。

這里如果你不想一步步調試源碼來看效果,那么就可以通過調用棧的形式快速過一遍流程。首先你需要把A和B類改為構造方法注入的形式。然后直接啟動測試線,觀察報錯信息。

通過錯誤堆棧信息我們定位到了報錯的根位置:DefaultSingletonBeanRegistry#beforeSingletonCreation,這個方法是bean創建之前的判斷,判斷通過則標記bean為創建中。也就是說構造方式的循環依賴在這里判斷未通過因此拋出了異常。
因此我們可以在該方法上打上斷點,重新運行測試類。

進入到斷點處后,你可以觀察左邊的調用棧,從下至上依次是bean A到Bean B使用構造依賴注入創建的流程,整個過程大概就是這樣:

  1. bean A開始創建,標記為創建中,調用構造方法去創建實例,但是發現需要參數b,因此需要先獲取Bean B
  2. bean B開始創建,標記為創建中,調用構造方法去創建實例,發現需要參數a,因此需要先獲取Bean A
  3. 又通過getBean方法獲取Bean A,在創建Bean A前判斷a此時是在創建中的,因此直接拋出異常。(此時實際A和B都未完成初步的實例化,也就不可能涉及到緩存操作了)
4. 循環依賴處理機制總結

Spring 的循環依賴的理論依據基于 Java 的引用傳遞,當獲得對象的引用時,對象的屬性是可以延后設置的,但是構造器必須是在獲取引用之前。(因此構造注入循環依賴無法解決)

Spring通過setXxx或者@Autowired方法解決循環依賴其實是通過提前暴露一個ObjectFactory對象來完成的,簡單來說ClassA在調用構造器完成對象初始化之后,在調用ClassA的setClassB方法之前就把ClassA實例化的對象通過ObjectFactory提前暴露到Spring容器中,供循環依賴的對象引用。

整體步驟:

  • Spring容器初始化ClassA通過構造器初始化對象后提前暴露到Spring容器
  • ClassA調用setClassB方法, Spring首先嘗試從容器中獲取ClassB,此時ClassB不存在Spring容器中
  • Spring容器初始化ClassB,同時也會將ClassB提前暴露到Spring容器中
  • ClassB調用setClassA方法, Spring從容器中獲取ClassA ,因為第一步中已經提前暴露了ClassA,因此可以獲取到ClassA實例
  • ClassB創建完成之后ClassA通過spring容器獲取到ClassB,完成了對象初始化操作
  • 這樣ClassA和ClassB都完成了對象初始化操作,解決了循環依賴問題

我們在用一張圖來具體描述這個過程:

4.三級緩存機制

在源碼層面Spring通過三級緩存機制實現上面的思路,巧妙的解決了循環依賴問題。在這里我們對三級緩存做一個匯總。

所謂的三級緩存指的是:

  • singletonObjects:一級緩存,里面放置的是已經完成所有創建動作的單例對象,也就是說這里存放的bean已經完成了所有創建的生命周期過程,在項目運行階段當執行多次getBean()時就是從一級緩存中獲取的。
  • earlySingletonObjects:二級緩存,里面存放的是提前曝光的已經實例化好的單例對象。與一級緩存的區別在于,該緩存所獲取到的bean是還沒創建完成的,比如屬性填充跟初始化動作肯定還沒有做完,因此僅作為指針提前曝光,方便被其他bean所引用
  • singletonFactories:三級緩存,里面存放的是要被實例化的對象的對象工廠(ObjectFactory實例對象),在需要引用提前曝光對象時再通過objectFactory.getObject()方法獲取真正的實例對象。

它們都定義在DefaultSingletonBeanRegistry類中:

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

三級緩存的添加時機:Bean對象完成實例化后,設置屬性前

//調用處:AbstractAutowireCapableBeanFactory#doCreateBean
// 如果允許提前曝光,則將該bean轉換成ObjectFactory并加入到三級緩存
if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> 
            getEarlyBeanReference(beanName, mbd, bean));
}
// DefaultSingletonBeanRegistry#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)) {
            // 加入三級緩存
            this.singletonFactories.put(beanName, singletonFactory);
            // 從二級緩存中移除
            this.earlySingletonObjects.remove(beanName);
            // 加入到注冊單例bean(有序的set集合)
            this.registeredSingletons.add(beanName);
        }
    }
}

一級緩存的添加時機:在bean創建完成后

// 調用處:DefaultSingletonBeanRegistry#getSingleton(String, ObjectFactory)
if (newSingleton) {
    addSingleton(beanName, singletonObject);
}
// addSingleton(beanName, Object)方法
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 添加到一級緩存
        this.singletonObjects.put(beanName, singletonObject);
        // 從三級緩存中移除
        this.singletonFactories.remove(beanName);
        // 從二級緩存中移除
        this.earlySingletonObjects.remove(beanName);
        // 添加到已注冊bean(有序set集合)
        this.registeredSingletons.add(beanName);
    }
}

二級緩存的添加時機
前面我們講解循環依賴的時候似乎并沒有提及二級緩存(如果仔細的同學應該能發現是有的),那么這時候如何知道二級緩存的添加時機呢,在教大家一個方法—反調法。

  1. 在 DefaultSingletonBeanRegistry 類中搜索 earlySingletonObjects.put,你能夠找到一個 getSingleton 方法
  2. 在IDEA中選中方法右鍵選擇【Find Usages】,你能夠找到在同類中一個單參數的重載方法 getSingleton 調用了它
  3. 最后選中這個重載方法,同樣通過【Find Usages】查看調用處,你能發現一個關鍵的方法 doGetBean,就是獲取bean的入口方法了

二級緩存的添加時機:從三級緩存中獲取到對象工廠,并且調用對象工廠的getObject方法獲取到bean實例之后

// 調用處:AbstractBeanFactory#doGetBean
// 嘗試從各級緩存中獲取bean
Object sharedInstance = getSingleton(beanName);
// DefaultSingletonBeanRegistry#getSingleton(String, boolean)方法
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 從一級緩存中獲取
    Object singletonObject = this.singletonObjects.get(beanName);
    // 判斷當前bean是否正在創建中
    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) {
                    // 獲取到了對象工廠之后,調用getObject方法獲取真正的實例
                    singletonObject = singletonFactory.getObject();
                    // 將獲取到的實例放入二級緩存中
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    // 從三級緩存中移除
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

有關三級緩存機制我們就總結到這,接下來我們再來回答一個關鍵的問題:為什么要使用三級緩存呢,兩級不可以嗎?
其實這個問題也可以轉換為別的形式,比如三級緩存為什么要緩存一個ObjectFactory對象,為什么不直接緩存實例?或者在循環依賴處理過程中二級緩存似乎沒起到什么作用,那么它的意義何在,是否可以去掉?
要回答這個問題,需要你對Spring AOP的相關原理有所了解。

Spring AOP的原理

關于Spring AOP的源碼這里并不會太過于深入,之后我會專門寫一篇關于AOP源碼的詳細分析的文章。

1. 時機點分析

首先需要準備AOP的基礎案例(省略),然后在測試類的getBean處打上斷點,運行測試類,觀察單例池中ruoxiyuanBean對象。

我們發現在getBean之前,RuoxiyuanBean對象已經產生,而且該對象是一個代理對象(Cglib代理對象),我們斷定,容器初始化過程中目標Ban已經完成了代理,返回了代理對象。

2.代理對象創建流程

通過前面對Bean的創建流程分析,我們可以大膽猜想一下代理對象產生的位置,應該是在原始Bean對象實例化和填充屬性后做一些后置處理時創建的。即AbstractAutowireCapableBeanFactory#doCreateBean方法中如下位置,打上斷點F5進入該方法。

在該方法的最后對包裝Bean進行了賦值,我們進入該方法

在該方法中循環所有的BeanPostProcessor進行一一處理,我們找到與代理相關的類

F5進入方法,來到創建代理對象的后置處理器AbstractAutoProxyCreator#postProcessAfterInitialization

下一步,進入同類下的wrapIfNecessary方法,在這里開始創建代理對象。

我們分析到這就結可以結束了,后續就是創建代理對象的具體過程了,分析這個流程我們主要想闡述兩個觀點:

  1. 按照正常情況下(沒有產生循環依賴)bean的代理對象是從填充屬性后做一些后置處理時開始創建的
  2. wrapIfNecessary方法會返回一個創建好的代理對象
3.循環依賴Bean代理對象創建時機

我們先直接給出結論,然后再通過源碼來驗證這一點。對于A 與 B類產生循環依賴的情況,A創建代理對象的時機應該是在AbstractAutowireCapableBeanFactory#getEarlyBeanReference方法。

分析:前面的過程是 A實例化 —> B實例化 —> B填充屬性,此時會調用DefaultSingletonBeanRegistry#getSingleton方法從三級緩存中獲取到A的引用,但是這里并不是直接獲取,而是取出三級緩存中存放的ObjectFactory對象,然后從該工廠對象中通過getObject獲取到對象引用。

回顧A在實例化后存入三級緩存的ObjectFactory是什么?來到代碼AbstractAutowireCapableBeanFactory#doCreateBean

這里顯示存放的是一個lamda表達式,當我們調用getObject方法時實際調用的是AbstractAutowireCapableBeanFactory#getEarlyBeanReferencegetEarlyBeanReference方法。

進入該方法,打上斷點,這個方法的邏輯就是遍歷bean所有的后置處理器,如果有指定的則調用指定的方法,否則直接返回bean實例。觀察我們獲取到的SmartInstantiationAwareBeanPostProcessor接口實現,是一個AspectJAwareAdvisorAutoProxyCreator對象,該對象繼承自AbstractAutoProxyCreator類。

我們進入AbstractAutoProxyCreator#getEarlyBeanReference方法,該方法將Bean A加入到提前曝光對象容器中,然后調用了wrapIfNecessary方法,該方法由上面流程可知會產生一個Bean A的代理對象并返回。而Bean B的代理對象還是正常流程產生。

當Bea B創建完成后,Bean A執行到AbstractAutoProxyCreator#postProcessAfterInitialization方法時,這里的緩存判斷為false表示Bean A已經創建了代理對象,就直接返回了。

至此關于AOP源碼的分析就全部結束了,那么我們來總結一下:

  1. Spring會針對需要創建代理對象的bean添加一個后置處理器,即SmartInstantiationAwareBeanPostProcessor接口的實現,那么具體的實現類就是AbstractAutoProxyCreator。通過該類的getEarlyBeanReference方法獲取到代理對象
  2. Spring使用二級緩存來存儲通過提前曝光的對象工廠獲取到的實例對象,那么該對象可能是被包裝后的代理對象,也可能是原始bean實例

不知道通過AOP源碼的分析后大家是否可以理解為什么不使用兩級緩存而要使用三級緩存呢?我這里再來解釋一下。

從Spring的角度來講,使用三級緩存先緩存一個對象工廠,如果當前bean不存在循環依賴問題,這個三級緩存就沒有什么實質的作用了。如果存在循環依賴并且該bean不需要被代理呢,那么二級緩存就沒有什么實質的作用了,因為此時從對象工廠獲得的就是bean實例化的對象。但是如果存在循環依賴并且該bean需要被代理時,就需要通過對象工廠對提前曝光的對象進行代理包裝處理了,并且需要一個緩存來存儲這個代理對象,以便后續其他的對象直接引用。

所以你可以說Spring之所以使用三級緩存的機制(而不是兩級)主要是為了解決產生循環依賴的bean同時也需要被代理的需求,或者說為了保證提前曝光的bean在被提前引用之前可以被Spring AOP進行代理。

而從框架的角度來講,多加一層緩存和對象工廠接口,可以保證整個架構的可擴展性,事實上即使沒有AOP的需求,你也可以通過重寫SmartInstantiationAwareBeanPostProcessor后置處理器對提前曝光的實例,在被提前引用時進行一些特殊的操作。這種設計思想也是非常值得我們學習和借鑒的。

擴展內容
1.spring為什么更推薦使用構造注入?

你可能會說出以下的一些原因:

  • 保證依賴不可變(final關鍵字)
  • 保證依賴不為空(省去了我們對其檢查)
  • 保證返回客戶端(調用)的代碼的時候是完全初始化的狀態

那么站在循環依賴的角度回答就是構造器注入可以有效避免循環依賴,因為在啟動時如果存在構造器的循環依賴就會直接拋出異常。

從這個角度也說明了一個問題,就是項目中如果存在大量的循環依賴的話,顯然并不是一個很好的現象,應該在代碼設計時盡可能避免出現循環依賴的情況。

2. 如何檢測項目中存在的循環依賴類

業界存在一款優秀的開源工具,它專門用于量化代碼的各種度量指標,其中就包含了代碼循環依賴分析,這款工具就是JDepend。你可以在GitHub上找到它的源碼和使用方法。它可以對指定的包結構進行分析,給出系統中存在循環依賴代碼的提示(存在循環依賴關系的包、所依賴的包和被依賴的包)。
關于這款工具的使用方法這里就不做介紹了,相信作為優秀工程師的你有能力去自學搞定它。

3. 如何解決代碼中存在的循環依賴問題呢?

這個作為最后的思考題留給大家

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377