Spring 迴圈引用(三)原始碼深入分析版

xun發表於2020-05-10

@

前言

關於Spring 迴圈引用 網上的分析文章很多,寫的水平良莠不齊,雖然看完了 知道怎麼個回事 但是過段時間還是忘記了,主要本人沒過目不忘的本領哈,但是隻要記住主要的點就好了

但是如果你自己想更深入的瞭解,還是要自己去看原始碼分析一波,因為別人分析的時候,有些知識點你是get不到的,只有當自己走進原始碼去看的時候,才有get到更多的!比如網上很多文章都分析Springs是怎麼解決迴圈依賴的 但是為什麼只有單類的才可以,Prototype的就不行呢,在哪裡不行,或者說構造器的注入為什麼也不可以,最後如果解決迴圈依賴,或者說 怎麼去換中寫法去解決問題。

紙上得來終覺淺 絕知此事要躬行! 這句話獻給正在讀文章的你,看完記得點贊,還有就是自己去下載Spring 原始碼 去看看

正文

OK,進入正文,當然上面也不是廢話啦,Spring 的迴圈引用 我想讀者們應該知道,不知道的話,算了 來個code把!

@Component
public class CycleTestServiceA {  
  private CycleTestServiceB b;
  public void setB(CycleTestServiceB b) {
    this.b = b;
  }
}

@Component
public class CycleTestServiceB {  
  private CycleTestServiceA a;
  public void setA(CycleTestServiceA a) {
    this.a = a;
  }
}

上面的 程式碼 就是一個普通的set注入的方式,A裡面依賴B,B裡面依賴A,這樣就導致了迴圈依賴,Component預設是Singleton的

分析

我們從Spring Beanc建立開始作為入口,在Spring IoC 容器中一個完整的Bean 要進過例項化 和初始化的階段

Spring Bean 例項化就getBean的過程

那我們接進入原始碼去看下getBean的過程

doGetBean

getBean方法時 BeanFactory 介面的方法 他的實現類有很多,我們跟進去他的抽象實現類org/springframework/beans/factory/support/AbstractBeanFactory.java 類,其實都是呼叫了doGetBean方法

下面是我擷取的核心程式碼

   protected <T> T doGetBean(
   		final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
   		throws BeansException {

   	final String beanName = transformedBeanName(name);
   	Object bean;

      	/*
       * 檢測是否 有快取物件  這個方法時處理迴圈依賴的關鍵入口
       * 記住這個的程式碼 我還會回來的
      	* */
   	Object sharedInstance = getSingleton(beanName);
   	if (sharedInstance != null && args == null) {
   		if (logger.isDebugEnabled()) {
   			if (isSingletonCurrentlyInCreation(beanName)) {
   				logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
   						"' that is not fully initialized yet - a consequence of a circular reference");
   			}
   			else {
   				logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
   			}
   		}
   		bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
   	}
   	else {
        	/*
   		*Prototype bean 是否在建立當中 如果存在 說明產生了迴圈依賴  處理Bean 迴圈依賴的地方
   		*這個地方就是為什麼Scope 是Prototype的時候 會報迴圈依賴的錯誤,慢慢看 後面會解釋
   		* */
   		if (isPrototypeCurrentlyInCreation(beanName)) {
   			throw new BeanCurrentlyInCreationException(beanName);
   		}

   	    ...

   		if (!typeCheckOnly) {
   			markBeanAsCreated(beanName);//這個方法就是把當前的bean 加入到alreadyCreated的set集合中 後面有些判斷需要
   		}

   		try {
   		    ...
   			
   			/*
   			* 獲取Bean 的依賴項 這邊的依賴 是我們在xml 有時候可以配置的depends-on的依賴 和我們本次講的迴圈依賴不是同一個
   			* 我特別說明下
   			* */
   			String[] dependsOn = mbd.getDependsOn();
   			if (dependsOn != null) {
   				for (String dep : dependsOn) {
   				  //註冊依賴 建立Bean 等
   				}
   			}

   			/*
   			* 如果是單列 建立createBean  記住這個的程式碼 我還會回來的
   			* */
   			if (mbd.isSingleton()) {
   				sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
   					@Override
   					public Object getObject() throws BeansException {
   						try {
   							return createBean(beanName, mbd, args);
   						}
   						catch (BeansException ex) {
   						  ...
   						}
   					}
   				});
   				bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
   			}

   			/*
   			* Prototype物件  
   			* */
   			else if (mbd.isPrototype()) {
   				// It's a prototype -> create a new instance.
   				Object prototypeInstance = null;
   				try {
   					beforePrototypeCreation(beanName);
   					prototypeInstance = createBean(beanName, mbd, args);
   				}
   				finally {
   					afterPrototypeCreation(beanName);
   				}
   				bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
   			}
   			/*
   			* 不是Singleton也不是Prototype,可能是自定義scope的物件
   			* */
   			else {
   			   ...
   			}
   		}
   	}
       ...
   	return (T) bean;
   }

上面是dogetBean()的核心方法

為什麼Prototype不可以

帶著這個問題 我們可以從上面的程式碼中 看下 Spring在處理麼Prototype的時候 有2個方法beforePrototypeCreation(),afterPrototypeCreation(),
上下程式碼

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

    protected void beforePrototypeCreation(String beanName) {
		Object curVal = this.prototypesCurrentlyInCreation.get();
		if (curVal == null) {
			this.prototypesCurrentlyInCreation.set(beanName);
		}
		else if (curVal instanceof String) {
			Set<String> beanNameSet = new HashSet<String>(2);
			beanNameSet.add((String) curVal);
			beanNameSet.add(beanName);
			this.prototypesCurrentlyInCreation.set(beanNameSet);
		}
		else {
			Set<String> beanNameSet = (Set<String>) curVal;
			beanNameSet.add(beanName);
		}
	}
	
	protected void afterPrototypeCreation(String beanName) {
		Object curVal = this.prototypesCurrentlyInCreation.get();
		if (curVal instanceof String) {
			this.prototypesCurrentlyInCreation.remove();
		}
		else if (curVal instanceof Set) {
			Set<String> beanNameSet = (Set<String>) curVal;
			beanNameSet.remove(beanName);
			if (beanNameSet.isEmpty()) {
				this.prototypesCurrentlyInCreation.remove();
			}
		}
	}

上面的程式碼 我相信小夥伴都能看的懂,就是用一個set集合儲存當前正在建立的Bean的BeanName,而且是用ThreadLocal去儲存Set集合的 ThreadLocal是每個執行緒私有的。看到這個 我們再把目光往程式碼上面看一看 isPrototypeCurrentlyInCreation這個方法的判斷

protected boolean isPrototypeCurrentlyInCreation(String beanName) {
		Object curVal = this.prototypesCurrentlyInCreation.get();
		return (curVal != null &&
				(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
	}

看到了麼 這邊就是用這個ThreadLocal裡面的set集合去判斷的,為什麼用ThreadLocal想下,你想呀,A依賴B,而B依賴A,AB都是Prototype的,A建立的時候 A會加入到這個set集合中,然後A去填充例項的時候,因為要依賴B,所以去getB,發現B又依賴A,這個時候有要getA,你看 當執行到 最上面的判斷isPrototypeCurrentlyInCreation的時候,是不報了迴圈引用的錯,因為A已經在prototypesCurrentlyInCreation的Set集合中了,因為整個流程一定是一個執行緒走下去的,所以存入ThreadLocal中,一點問題沒有,而且還不受其他執行緒影響~

createBean

不管是哪種Scope 都是要呼叫createBean方法的,我們跟進去程式碼 發現唯一重寫的實現在org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java 中
我們進入程式碼看下

    protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
		RootBeanDefinition mbdToUse = mbd;
	    ...
		try {
			// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
			// 該函式的作用是給 BeanPostProcessors 後置處理器返回一個代理物件的機會
			// 這裡是實現AOP處理的重要地方
			// AOP是通過BeanPostProcessor機制實現的,而介面InstantiationAwareBeanPostProcessor是實現代理的重點
			Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
			if (bean != null) {
				return bean;
			}
		}
		...

		/*
		* 後置處理器 沒有返回有效的bean 就建立
		* */
		Object beanInstance = doCreateBean(beanName, mbdToUse, args);
		if (logger.isDebugEnabled()) {
			logger.debug("Finished creating instance of bean '" + beanName + "'");
		}
		return beanInstance;
	}

這邊 我看到一句英文註釋,都沒捨得替換中文,Give BeanPostProcessors a chance to return a proxy instead of the target bean instance. 哈哈 給後置處理器一個返回代理bean的機會,這邊就是Spring 中實現AOP的重點,動態代理 其實就是使用後置處理器 替換了target Bean 的例項,從而達到代理的作用,這個以後聊到AOP 的時候在慢慢聊吧!這個最核心的程式碼還在再doCreateBean中,繼續跟進

doCreateBean

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
			throws BeanCreationException {

        // Instantiate the bean.
		BeanWrapper instanceWrapper = null;//BeanWrapper 是Bean 的包裝類  方便對Bean 例項的操作
		if (mbd.isSingleton()) {
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		if (instanceWrapper == null) {
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
		final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
		Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
		mbd.resolvedTargetType = beanType;

		....
		
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));//滿足三個條件  單列  執行迴圈引用  bean 是否正在建立中
		if (earlySingletonExposure) {
			addSingletonFactory(beanName, new ObjectFactory<Object>() {
				@Override
				public Object getObject() throws BeansException {
					return getEarlyBeanReference(beanName, mbd, bean);//提前暴露引用  獲取早期的引用
				}
			});
		}

		// Initialize the bean instance. 初始化Bean 例項
		Object exposedObject = bean;
		try {
			populateBean(beanName, mbd, instanceWrapper);//填充Bean
			if (exposedObject != null) {
				exposedObject = initializeBean(beanName, exposedObject, mbd);//執行初始化Bean裡面的方法
			}
		}
		...

		if (earlySingletonExposure) {
			Object earlySingletonReference = getSingleton(beanName, false);
			if (earlySingletonReference != null) {
			   /*
			   *這邊其實還是做了一個判斷,exposedObject是經過了 initializeBean方法方法的 
			   *而bean還是那個提前暴露的Bean,
			   *為什麼要做這個判斷你,是因為exposedObject經過了initializeBean裡面的後置處理器的修改 可能Object 已經改變了 
			   **/
				if (exposedObject == bean) {
					exposedObject = earlySingletonReference;
				}
				else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
					String[] dependentBeans = getDependentBeans(beanName);
					Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
					for (String dependentBean : dependentBeans) {
						if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
							actualDependentBeans.add(dependentBean);
						}
					}
					/*
					*有興趣的可以根據到上面的每一個方法看下 ,這邊就是判斷如果提前暴露的bean已經和在後置處理器裡面修改了並且不一樣了,就丟擲異常,因為提前暴露的Bean 可能作為了另外的bean的依賴 這樣就會導致單類的bean在容器中有2個例項的出現,這是非法的!
					*/
					if (!actualDependentBeans.isEmpty()) {
					  //丟擲一個異常 由於很多文字我就刪掉了
					}
				}
			}
		}
	   ...
		return exposedObject;
	}

earlySingletonExposure這個主要關注的是earlySingletonExposure 這邊的程式碼,這個就是在Bean 例項化完成後,開始填充屬性之間發的程式碼
earlySingletonExposure為true 要滿足三個條件

  • 單類
  • 允許迴圈引用
  • 當時單類正在建立中

前面2個可以理解 那最後一個又是什麼呢?話不多說 進入方法看下

private final Set<String> singletonsCurrentlyInCreation =
			Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));
public boolean isSingletonCurrentlyInCreation(String beanName) {
		return this.singletonsCurrentlyInCreation.contains(beanName);
	}

這個方法 一看很簡答 就是判斷當前的bean是否在singletonsCurrentlyInCreation的set集合中 那這個集合又是什麼時候加入的呢?帶著這個想法 我又重頭掃描了一篇程式碼 還記的org/springframework/beans/factory/support/AbstractBeanFactory.java程式碼中的doGetBean()方法裡面Singleton的Bean 在建立Instance的時候是呼叫了getSingleton方法麼,不清楚的話 可以往上看下

getEarlyBeanReference

這個方法 是 addSingletonFactory 方法 構建ObjectFactory的引數的時候 裡面返回使用方法
看下程式碼:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
					if (exposedObject == null) {
						return null;
					}
				}
			}
		}
		return exposedObject;
	}

裡面最主要的就是看下getEarlyBeanReference方法 這個方法時SmartInstantiationAwareBeanPostProcessor裡面的方法,他的實現有2個 一個是org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessorAdapter.java 還有一個是動態代理使用的 我就不列舉了,

public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
		return bean;
	}

看了下 其實InstantiationAwareBeanPostProcessorAdapter的重寫就是 返回了當前的bean 沒有做任何操作。這邊其實就是做了一個引用的儲存。

getSingleton

程式碼位於org/springframework/beans/factory/support/DefaultListableBeanFactory.java中

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		synchronized (this.singletonObjects) {
			Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {
			    ...
				beforeSingletonCreation(beanName);
				boolean newSingleton = false;
			    ...
				try {
					singletonObject = singletonFactory.getObject();
					newSingleton = true;
				}
				finally {
					afterSingletonCreation(beanName);
				}
				if (newSingleton) {
					addSingleton(beanName, singletonObject);
				}
			}
			return (singletonObject != NULL_OBJECT ? singletonObject : null);
		}
	}

這個方法其所就是SingletonBean的核心建立流程

beforeSingletonCreation

protected void beforeSingletonCreation(String beanName) {
		if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
			throw new BeanCurrentlyInCreationException(beanName);
		}
	}

當我看到這個singletonsCurrentlyInCreation.add的時候 我很欣慰 因為我之前的問題解決了 就是這個方法把Bean 放入到之前的singletonsCurrentlyInCreation的集合中的

singletonFactory.getObject

這個應該都很清楚了 就是我們方法傳入的匿名的ObjectFactory物件,當之前getObject的時候 才會執行我們剛才的看的createBean方法

afterSingletonCreation

protected void afterSingletonCreation(String beanName) {
		if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
			throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
		}
	}

看下afterSingletonCreation方法裡面的東西也很簡單,就是從singletonsCurrentlyInCreation集合中移除

addSingleton

先看下程式碼

protected void addSingleton(String beanName, Object singletonObject) {
		synchronized (this.singletonObjects) {
			this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
			this.singletonFactories.remove(beanName);
			this.earlySingletonObjects.remove(beanName);
			this.registeredSingletons.add(beanName);
		}
	}

看到 這邊 就不得表介紹下三級快取了

  • singletonObjects 例項化 初始化都完成的bean 快取
  • earlySingletonObjects 可提前引用的 Bean 快取,這裡面的Bean 是一個非完整的bean,屬性填充 後置處理器都未執行的bean
  • singletonFactories 單類bean的建立工廠函式物件

說道這裡 我們就清楚了 這個方法其所就是在二級快取和三級快取中刪除當前的Bean,把當前的Bean 放入到一級快取中,因為到了這一步 bean 的例項化,屬性填充,後置處理器執行,初始化等方法都已經執行了。

addSingletonFactory

這個方法 哪裡用的呢 那我們有要回到上面的程式碼doCreateBean中 當earlySingletonExposure為true的時候 會呼叫這個方法addSingletonFactory
這個方法就是 當前的Bean可以提前引用的話執行的方法
看下程式碼也很簡答

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);
			}
		}
	}

看下這個方法 說白了就是往三級快取裡面存放bean的ObjectFactory物件 這個地方也是處理迴圈引用的關鍵,這個時候Bean 剛剛進行了例項化 還沒有進行bean的屬性填充和初始化等一些列方法

那怎麼去解決提前引用的呢?可以看下ObjectFactory返回的是getEarlyBeanReference物件

getSingleton(beanName)

這個方法是在doGetBean方法中 從快取中獲取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) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return (singletonObject != NULL_OBJECT ? singletonObject : null);
	}

最終呼叫的方法如上,allowEarlyReference是為true的,我們還是用最上面的ServiceA和ServiceB 為例,第一次ServiceA 進入的時候 是沒法進入下面的判斷的 應為當前ServiceA不在SingletonCurrentlyInCreation中,但是當第二次進來,第二次是什麼時候呢,就是在填充ServiceB的時候 需要依賴 ServiceB,這個時候ServiceB也要執行getBean的流程,發現又依賴ServiceA,這個時候 ServiceA就是在SingletonCurrentlyInCreation的集合中了,而且在三級快取中,這個時候會進行判斷條件裡面的方法,先找一級快取,找不到就找二級快取,最後找三級快取,然後將取出三級快取裡面的ObjectFactory執行getObject方法 就是獲取我們上面提到的提前引用的bean,最後將bean 放入到二級快取,從三級快取中移除~

核心說明

看完了 上面的一推 也許很懵逼,可能也是我文字組織能力差,只能以後慢慢改變

快取的說明

上面涉及到幾個快取 我在邊在重寫描述一下

名稱 型別 使用說明 所屬類
singletonObjects Map<String, Object> 例項化 初始化都完成的bean的快取 DefaultSingletonBeanRegistry.java
earlySingletonObjects Map<String, Object> 可提前引用的 Bean 快取,這裡面的Bean 是一個非完整的bean,屬性填充 後置處理器都未執行的bean DefaultSingletonBeanRegistry.java
singletonFactories Map<String, ObjectFactory<?>> 單類bean的建立工廠函式物件 DefaultSingletonBeanRegistry.java
singletonsCurrentlyInCreation Set 馬上要建立的單類Bean的集合 DefaultSingletonBeanRegistry.java
prototypesCurrentlyInCreation ThreadLocal object 是一個Set 馬上要建立的prototype的Bean的集合 AbstractBeanFactory.java
alreadyCreated Set 至少建立過一次的Bean 在提前暴露的bean修改了導致不一致時 判斷會用到 AbstractBeanFactory.java

執行流程圖

最終我還是用一個方法執行的流程圖 來描述下 迴圈依賴的處理

執行流程圖1
執行流程圖2

構造器的注入解決

那麼為什麼構造器的注入方式不行呢?原因是因為 Bean在例項化階段的時候createBeanInstance的時候就會去建立依賴的B,這樣的話A根本就走不到提前暴露的程式碼塊,所以會報一個迴圈引用的錯誤,報錯的地方就是建構函式引數bean 建立的地方,自己可以寫個demo,除錯下 在哪一步報錯,博主可是看了半天 才找到,哈哈!

解決方法

關於如果解決構造器的迴圈注入
https://www.baeldung.com/circular-dependencies-in-spring
這是一篇外國博文,小夥伴們可以看下

  • 使用懶載入
  • 修改使用setter注入的方式
  • 使用PostConstruct註解
  • InitializingBean 後置處理器的方式

總結

Spring 處理迴圈依賴的核心就是 三級快取,讓Bean 提前暴露出來,可以提前引用,讓互相依賴的Bean 可以流程上執行下去,從而解決了迴圈依賴的問題

最後的最後 還是自己對照原始碼 自己理解一遍,我相信一定會加深你的理解,一定會有收穫

碼字不易,花了一個週末的時間,各位看官喜歡的話點個贊,鼓勵下博主,繼續創造,多謝~

相關文章