一、Spring 中的循環依賴問題

1、Spring 中的循環依賴概述

Spring 循環依賴指的是 SpringBean 對象之間的依賴關係形成一個閉環。即在代碼中,把兩個或者多個 Bean 相互之間去持有對方的引用,就會發生循環依賴,循環依賴會導致注入出現死循環,這是 Spring 發生循環依賴的主要原因之一。

Spring 循環依賴主要有三種情況,即:自身依賴自身,兩者互相依賴,多者循環依賴

  1. 自身依賴自身:自己依賴自己的直接依賴
  2. 兩者互相依賴:兩個對象之間的直接依賴
  3. 多者循環依賴:多個對象之間的間接依賴

SpringFramework:循環依賴與三級緩存_緩存

自身依賴自身,兩者互相依賴 兩者互相依賴的情況比較直觀,很好辨識,但是我們工作中最有可能觸發的還是多者循環依賴,多者循環依賴的情況有時候因為業務代碼調用層級很深,不容易識別出來。但無論循環依賴的數量有多少,循環依賴的本質是一樣的。就是你的完整創建依賴於我,而我的完整創建也依賴於你,但我們互相沒法解耦,最終導致依賴創建失敗。

2、Spring 中的循環依賴的 5 種場景

Spring 中出現循環依賴主要有着 5 種場景: ①、單例的 setter 注入(能解決);②、多例的 setter 注入(不能解決);③、構造器注入(不能解決);④、單例的代理對象 setter 注入(有可能解決);⑤、DependsOn 循環依賴(不能解決)。接下來我們逐一來看。

SpringFramework:循環依賴與三級緩存_三級緩存_02

二、Spring 三級緩存

1、spring 創建 bean 的流程

在開始理解 Spring 三級緩存如何讓解決循環依賴問題前我們先來温習一下 spring 創建 bean 的流程:

  1. Spring 啓動時會根據配置文件或啓動類把所有的 bean 註冊成 bean 定義(就是映射 <bean> 標籤屬性的 Java 類)
  2. 遍歷 bean 定義中的 beanName,調用 BeanFactory#getBean(beanName) 方法創建、初始化並返回 bean 實例

其中 getBean 方法:

  1. 先從緩存(一層到三層依次獲取)拿,沒有就去創建;
  2. 創建 Bean 時,把 beanName 標記為正在創建中,通過其定義裏的 class 找到構造器方法反射創建實例,並把其對象工廠放入第三層緩存;
  3. 對實例初始化,移除正在創建中的標記,把實例放入第一層緩存,移除第二、三層中的緩存,最後返回實例

Ps1:實例初始化過程:獲取此 bean 中有 @Autowired 等註解的成員變量,從所有 bean 定義中找出此類型的 beanName,又通過 BeanFactory#getBean 方法獲取實例,然後反射設值成員變量。

Ps2:上述流程中 斜體 部分為觸發循環依賴時多出主流程的步驟。

位於 org.springframework.beans.factory.support.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 ConcurrentHashMap<>(16);
2、場景一:單例的 setter 注入

這種注入方式應該是 Spring 中最常見的,Demo 如下:

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
}

@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

在上述代碼中,就是一個經典的循環依賴,其中 TestService1 依賴 TestService2,TestService2 依賴 TestService1 構成了一個簡單了兩者互相依賴關係,但是我們在使用類似代碼時,並沒有感知過該類型的循環依賴存在,因為此種類型已經被 Spring 默默解決了。

3、三級緩存

Spring 內部有三級緩存:

  • 一級緩存(singletonObjects),用於保存實例化、注入、初始化完成的 Bean 實例
  • 二級緩存(earlySingletonObjects),用於保存實例化完成的 Bean 實例
  • 三級緩存(singletonFactories),用於保存 Bean 的創建工廠,以便於後面擴展有機會創建代理對象。

以上面 Demo 為例,現在項目啓動,spring 開始創建 bean,比如先創建 TestService1:

  1. 標記 TestService1 為正在創建中,反射創建其實例,其對象工廠放入第三層緩存
  2. 初始化 TestService1 實例化時發現需要依賴注入 TestService2,則獲取 TestService2 的實例
  3. 標記 TestService2 為正在創建中,反射創建其實例,其對象工廠放入第三層緩存
  4. 初始化 TestService2 實例化時發現需要依賴注入 TestService1,則獲取 TestService1 的實例
  5. 這時候從緩存中獲取時,TestService1 為正在創建中且第三層緩存有 TestService1 的值了,所以調用緩存的對象工廠的 getObject 方法,把返回的 TestService1 實例放入第二層緩存,刪除第三層緩存
  6. TestService2 實例初始化完成,放入第一層緩存,移除第二、三層中的緩存
  7. 回到第 2 步,TestService1 實例初始化完成,放入第一層緩存,移除第二、三層中的緩存

SpringFramework:循環依賴與三級緩存_緩存_03

下面是 getBean(beanName) 方法最先調用的從這三層緩存中獲取 bean 實例的邏輯(即上面第5步)

/**
  * Return the (raw) singleton object registered under the given name.
  * <p>Checks already instantiated singletons and also allows for an early
  * reference to a currently created singleton (resolving a circular reference).
  * @param beanName the name of the bean to look for
  * @param allowEarlyReference whether early references should be created or not
  * @return the registered singleton object, or {@code null} if none found
  */
	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  // Quick check for existing instance without full singleton lock
  Object singletonObject = this.singletonObjects.get(beanName);
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
  	singletonObject = this.earlySingletonObjects.get(beanName);
  	if (singletonObject == null && allowEarlyReference) {
    synchronized (this.singletonObjects) {
    	// Consistent creation of early reference within full singleton lock
    	singletonObject = this.singletonObjects.get(beanName);
    	if (singletonObject == null) {
      singletonObject = this.earlySingletonObjects.get(beanName);
      if (singletonObject == null) {
      	ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
      	if (singletonFactory != null) {
        singletonObject = singletonFactory.getObject();
        this.earlySingletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
      	}
      }
    	}
    }
  	}
  }
  return singletonObject;
	}

以及一直提到的對象工廠,及其 getObject 方法的實現:

/**
  * Obtain a reference for early access to the specified bean,
  * typically for the purpose of resolving a circular reference.
  * @param beanName the name of the bean (for error handling purposes)
  * @param mbd the merged bean definition for the bean
  * @param bean the raw bean instance
  * @return the object to expose as bean reference
  */
	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;
	}
4、關於二級緩存

細心的朋友可能會發現在這種場景中第二級緩存作用不大。那麼問題來了,為什麼要用第二級緩存呢?

試想一下,如果出現以下這種情況,我們要如何處理?

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;
    public void test1() {
    }
}
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}
@Service
public class TestService3 {
    @Autowired
    private TestService1 testService1;
    public void test3() {
    }
}

TestService1 依賴於 TestService2 和 TestService3,而 TestService2 依賴於 TestService1,同時 TestService3 也依賴於 TestService1。按照上圖的流程可以把 TestService1 注入到 TestService2,並且 TestService1 的實例是從第三級緩存中獲取的。

假設不用第二級緩存,TestService1 注入到 TestService3 的流程如圖:

SpringFramework:循環依賴與三級緩存_初始化_04

TestService1 注入到 TestService3 又需要從第三級緩存中獲取實例,而第三級緩存裏保存的並非真正的實例對象,而是 ObjectFactory對象。説白了,兩次從三級緩存中獲取都是 ObjectFactory 對象,而通過它創建的實例對象每次可能都不一樣的。這樣不是有問題?

為了解決這個問題,Spring 引入的第二級緩存。上面其實 TestService1 對象的實例已經被添加到第二級緩存中了,而在 TestService1 注入到 TestService3 時,只用從第二級緩存中獲取該對象即可。

SpringFramework:循環依賴與三級緩存_緩存_05

還有個問題,第三級緩存中為什麼要添加 ObjectFactory 對象,直接保存實例對象不行嗎?答:不行,因為假如你想對添加到三級緩存中的實例對象進行增強,直接用實例對象是行不通的。

三、循環依賴的其他 4 種場景

1、多例的 setter 注入

這種注入方法偶然會有,特別是在多線程的場景下,具體代碼如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

在上述多例的 setter 注入情況下,Spring 程序也是能夠正常啓動啓動的,其實在 AbstractApplicationContext 類的 refresh方法中告訴了我們答案,它會調用 finishBeanFactoryInitialization 方法,該方法的作用是為了 Spring 容器啓動的時候提前初始化一些 Bean。該方法的內部又調用了 preInstantiateSingletons 方法

@Override
	public void preInstantiateSingletons() throws BeansException {
  if (logger.isTraceEnabled()) {
  	logger.trace("Pre-instantiating singletons in " + this);
  }

  // Iterate over a copy to allow for init methods which in turn register new bean definitions.
  // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
  List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

  // Trigger initialization of all non-lazy singleton beans...
  for (String beanName : beanNames) {
  	RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
  	if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
    if (isFactoryBean(beanName)) {
    	Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
    	if (bean instanceof FactoryBean) {
      FactoryBean<?> factory = (FactoryBean<?>) bean;
      boolean isEagerInit;
      if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
      	isEagerInit = AccessController.doPrivileged(
        	(PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
        	getAccessControlContext());
      }
      else {
      	isEagerInit = (factory instanceof SmartFactoryBean &&
        	((SmartFactoryBean<?>) factory).isEagerInit());
      }
      if (isEagerInit) {
      	getBean(beanName);
      }
    	}
    }
    else {
    	getBean(beanName);
    }
  	}
  }

其中 非抽象、單例 並且非懶加載的類才能被提前初始 Bean。,而多例即 SCOPE_PROTOTYPE 類型的類,非單例,不會被提前初始化 Bean,所以程序能夠正常啓動。如何讓他提前初始化bean呢?

只需要再在 DEMO 中定義一個單例的類,在它裏面注入 TestService1

@Service
public class TestService3 {
    @Autowired
    private TestService1 testService1;
}

重新啓動程序,執行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出現了循環依賴。

Ps:這種循環依賴問題是無法解決的,因為它沒有用緩存,每次都會生成一個新對象。

2、構造器注入

這種注入方式現在其實用的已經非常少了,但是我們還是有必要了解一下,如下代碼:

@Service
public class TestService1 {
    public TestService1(TestService2 testService2) {
    }
}
@Service
public class TestService2 {
    public TestService2(TestService1 testService1) {
    }
}

運行結果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出現了循環依賴,為什麼呢?

SpringFramework:循環依賴與三級緩存_緩存_06

從圖中的流程看出構造器注入沒能添加到三級緩存,也沒有使用緩存,所以也無法解決循環依賴問題。

3、單例的代理對象 setter 注入

這種注入方式其實也比較常用,比如平時使用:@Async 註解的場景,會通過 AOP 自動生成代理對象。

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    @Async
    public void test1() {
    }
}
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

從前面得知程序啓動會報錯,出現了循環依賴,為什麼會循環依賴呢?答案就在下面這張圖中:

SpringFramework:循環依賴與三級緩存_三級緩存_07

説白了,Bean 初始化完成之後,後面還有一步去檢查:第二級緩存和原始對象是否相等。由於它對前面流程來説無關緊要,所以前面的流程圖中省略了,但是在這裏是關鍵點,我們重點説説:

if (earlySingletonExposure) {
  	Object earlySingletonReference = getSingleton(beanName, false);
  	if (earlySingletonReference != null) {
    if (exposedObject == bean) {
    	exposedObject = earlySingletonReference;
    }
    else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
    	String[] dependentBeans = getDependentBeans(beanName);
    	Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
    	for (String dependentBean : dependentBeans) {
      if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
      	actualDependentBeans.add(dependentBean);
      }
    	}
    	if (!actualDependentBeans.isEmpty()) {
      throw new BeanCurrentlyInCreationException(beanName,
        "Bean with name '" + beanName + "' has been injected into other beans [" +
        StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
        "] in its raw version as part of a circular reference, but has eventually been " +
        "wrapped. This means that said other beans do not use the final version of the " +
        "bean. This is often the result of over-eager type matching - consider using " +
        "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
    	}
    }
  	}
  }

正好是走到這段代碼,發現第二級緩存和原始對象不相等,所以拋出了循環依賴的異常。如果這時候把 TestService1 改個名字,改成:TestService6,其他的都不變。

@Service
public class TestService6 {
    @Autowired
    private TestService2 testService2;
    @Async
    public void test1() {
    }
}

再重新啓動一下程序,神奇般的好了。這是為什麼?這就要從 Spring Bean 加載順序説起了,默認情況下,Spring 是按照文件完整路徑遞歸查找的,按路徑+文件名排序,排在前面的先加載。所以 TestService1 比T estService2 先加載,而改了文件名稱之後,TestService2 比 TestService6 先加載。

為什麼 TestService2 比 TestService6 先加載就沒問題呢?答案在下面這張圖中:

SpringFramework:循環依賴與三級緩存_三級緩存_08

這種情況 testService6 中其實第二級緩存是空的,不需要跟原始對象判斷,所以不會拋出循環依賴。

4、DependsOn 循環依賴

還有一種有些特殊的場景,比如我們需要在實例化 Bean A 之前,先實例化 Bean B,這個時候就可以使用 @DependsOn 註解。

@DependsOn(value = "testService2")
@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }
}

程序啓動之後,執行結果:

Circular depends-on relationship between 'testService2' and 'testService1'

這個例子中本來如果 TestService1 和 TestService2 都沒有加 @DependsOn 註解是沒問題的,反而加了這個註解會出現循環依賴問題。

這又是為什麼?答案在 AbstractBeanFactory 類的 doGetBean 方法的這段代碼中:

// Guarantee initialization of beans that the current bean depends on.
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
    	for (String dep : dependsOn) {
      if (isDependent(beanName, dep)) {
      	throw new BeanCreationException(mbd.getResourceDescription(), beanName,
        	"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
      }
      registerDependentBean(dep, beanName);
      try {
      	getBean(dep);
      }
      catch (NoSuchBeanDefinitionException ex) {
      	throw new BeanCreationException(mbd.getResourceDescription(), beanName,
        	"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
      }
    	}
    }

它會檢查 dependsOn 的實例有沒有循環依賴,如果有循環依賴則拋異常。

三、出現循環依賴如何解決?

項目中如果出現循環依賴問題,説明是 Spring 默認無法解決的循環依賴,要看項目的打印日誌,屬於哪種循環依賴。目前包含下面幾種情況:

SpringFramework:循環依賴與三級緩存_三級緩存_09

解決方式:

生成代理對象產生的循環依賴: ①、 使用 @Lazy 註解,延遲加載 ②、使用 @DependsOn 註解,指定加載先後關係 ③、修改文件名稱,改變循環依賴類的加載順序

多例循環依賴: 可以通過把 Bean 改成單例的解決

構造器循環依賴: 可以通過使用 @Lazy 註解解決

使用 @DependsOn 產生的循環依賴: 要找到@DependsOn註解循環依賴的地方,迫使它不循環依賴就可以解決問題