1. 程式人生 > >通過迴圈依賴問題徹底理解SpringIOC

通過迴圈依賴問題徹底理解SpringIOC

 

前言

你可能會有如下問題:
1、想看Spring原始碼,但是不知道應當如何入手去看,對整個Bean的流程沒有概念,碰到相關問題也沒有頭緒如何下手
2、看過幾遍原始碼,沒辦法徹底理解,沒什麼感覺,沒過一陣子又忘了
本文將結合實際問題,由問題引出原始碼,並在解釋時會盡量以圖表的形式讓你一步一步徹底理解Spring Bean的IOC、DI、生命週期、作用域等。

先看一個迴圈依賴問題

 

現象

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

如何理解“依賴”呢,在Spring中有:

  • 構造器迴圈依賴

  • field屬性注入迴圈依賴

 

直接上程式碼:

 

構造器迴圈依賴

@Service
public class A {  
   public A(B b) {  }
}
@Service
public class B {  
   public B(C c) {  
   }
}
@Service
public class C {  
   public C(A a) {  }
}

結果:專案啟動失敗,發現了一個cycle。

 2.field屬性注入迴圈依賴

@Service
public class A1 {  
   @Autowired  
   private B1 b1;
}

@Service
public class B1 {  
   @Autowired  
   public C1 c1;
}

@Service
public class C1 {  
   @Autowired  public A1 a1;
}

結果:專案啟動成功

3.field屬性注入迴圈依賴(prototype)

@Service
@Scope("prototype")
public class A1 {  
   @Autowired  
   private B1 b1;
}

@Service
@Scope("prototype")
public class B1 {  
   @Autowired  
   public C1 c1;
}

@Service
@Scope("prototype")
public class C1 {  
   @Autowired  public A1 a1;
}

結果:專案啟動失敗,發現了一個cycle。

現象總結:同樣對於迴圈依賴的場景,構造器注入和prototype型別的屬性注入都會初始化Bean失敗。因為@Service預設是單例的,所以單例的屬性注入是可以成功的。

 

分析原因

分析原因也就是在發現SpringIOC的過程,如果對原始碼不感興趣可以關注每段原始碼分析之後的總結和迴圈依賴問題的分析即可。

 

SpringBean的載入流程(原始碼分析)

 

簡單一段程式碼作為入口

ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
ac.getBean(XXX.class);

ClassPathXmlApplicationContext是一個載入XML配置檔案的類,與之相對的還有AnnotationConfigWebApplicationContext,這兩個類大差不差的,只是ClassPathXmlApplicationContext的Resource是XML檔案而AnnotationConfigWebApplicationContext是Scan註解獲得的。

 

看到第二行就已經可以直接獲取bean的例項了,所以第一行構造方法時,就已經完成了對所有bean的載入。

ClassPathXmlApplicationContext舉例,他裡面儲存的東西如下:

物件名

類  型

作  用

歸屬類

configResources

Resource[]

配置檔案資源物件陣列

ClassPathXmlApplicationContext

configLocations

String[]

配置檔案字串陣列,儲存配置檔案路徑

AbstractRefreshableConfigApplicationContext

beanFactory

DefaultListableBeanFactory

上下文使用的Bean工廠

AbstractRefreshableApplicationContext

beanFactoryMonitor

Object

Bean工廠使用的同步監視器

AbstractRefreshableApplicationContext

id

String

上下文使用的唯一Id,標識此ApplicationContext

AbstractApplicationContext

parent

ApplicationContext

父級ApplicationContext

AbstractApplicationContext

beanFactoryPostProcessors

List

儲存BeanFactoryPostProcessor介面,Spring提供的一個擴充套件點

AbstractApplicationContext

startupShutdownMonitor

Object

refresh方法和destory方法公用的一個監視器,避免兩個方法同時執行

AbstractApplicationContext

shutdownHook

Thread

Spring提供的一個鉤子,JVM停止執行時會執行Thread裡面的方法

AbstractApplicationContext

resourcePatternResolver

ResourcePatternResolver

上下文使用的資源格式解析器

AbstractApplicationContext

lifecycleProcessor

LifecycleProcessor

用於管理Bean生命週期的生命週期處理器介面

AbstractApplicationContext

messageSource

MessageSource

用於實現國際化的一個介面

AbstractApplicationContext

applicationEventMulticaster

ApplicationEventMulticaster

Spring提供的事件管理機制中的事件多播器介面

AbstractApplicationContext

applicationListeners

Set

Spring提供的事件管理機制中的應用監聽器

AbstractApplicationContext

構造方法如下:

接下來大概看看refresh方法:

子方法先不看,先看看refresh方法的結構,其實就有幾點值得學習:

 

1、方法為什麼加鎖? 是為了避免多執行緒的場景下同時重新整理Spring上下文

2、雖然整個方法是加鎖的,但是卻用了Synchronized關鍵字的物件鎖startUpShutdownMonitor,這樣做有兩個好處:

 

(1)關閉資源的時候會呼叫close()方法,close()方法也使用了同樣的物件鎖,而關閉資源的close和refresh的兩個衝突的方法,這樣可以避免衝突

 

(2)此處物件鎖相對於整個方法加鎖的話,同步的範圍更小了,鎖的粒度更小,效率更高

 

3、這個方法refresh定義了整個Spring IOC的流程,每一個方法名字都清晰易懂,可維護性、可讀性很強

 

總結:看原始碼需要找准入口,看的時候多思考,學習Spring的巧妙的設計。ApplicationContext的構造方法中最關鍵是方法是refresh,其中有一些比價好的設計。

 

 

obtainFreshBeanFactory方法

 

這個方法作用是獲取重新整理Spring上下文的Bean工廠:

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {  
   this.refreshBeanFactory();  
   return this.getBeanFactory();
}
protected final void refreshBeanFactory() throws BeansException {  
   if (this.hasBeanFactory()) {    
       this.destroyBeans();    
       this.closeBeanFactory();  
   }  
   try {    
       DefaultListableBeanFactory beanFactory = this.createBeanFactory();    
       beanFactory.setSerializationId(this.getId());    
       this.customizeBeanFactory(beanFactory);    
       this.loadBeanDefinitions(beanFactory);    
       synchronized(this.beanFactoryMonitor) {      
           this.beanFactory = beanFactory;    }  
       } catch (IOException var5) {    
           throw new ApplicationContextException("I/O error parsing bean definition source for " + this.getDisplayName(), var5);  
       }
}

這斷程式碼的核心是DefaultListableBeanFactory,核心類我們再整理一下,以圖表格式:

 

下面有三個加粗的Map,這些個Map是解決問題的關鍵。。。我們之後詳細分析

物件名

類  型

作    用

歸屬類

aliasMap

Map

儲存Bean名稱->Bean別名對映關係

SimpleAliasRegistry

singletonObjects

Map

儲存單例Bean名稱->單例Bean實現對映關係

DefaultSingletonBeanRegistry

singletonFactories

Map

儲存Bean名稱->ObjectFactory實現對映關係

DefaultSingletonBeanRegistry

earlySingletonObjects

Map

儲存Bean名稱->預載入Bean實現對映關係  

DefaultSingletonBeanRegistry

registeredSingletons

Set

儲存註冊過的Bean名

DefaultSingletonBeanRegistry

singletonsCurrentlyInCreation

Set

儲存當前正在建立的Bean名

DefaultSingletonBeanRegistry

disposableBeans

Map

儲存Bean名稱->Disposable介面實現Bean實現對映關係

DefaultSingletonBeanRegistry

factoryBeanObjectCache

Map

儲存Bean名稱->FactoryBean介面Bean實現對映關係

FactoryBeanRegistrySupport

propertyEditorRegistrars

Set

儲存PropertyEditorRegistrar介面實現集合

AbstractBeanFactory

embeddedValueResolvers

List

儲存StringValueResolver(字串解析器)介面實現列表

AbstractBeanFactory

beanPostProcessors

List

儲存 BeanPostProcessor介面實現列表

AbstractBeanFactory

mergedBeanDefinitions

Map

儲存Bean名稱->合併過的根Bean定義對映關係

AbstractBeanFactory

alreadyCreated

Set

儲存至少被建立過一次的Bean名集合

AbstractBeanFactory

ignoredDependencyInterfaces

Set

儲存不自動裝配的介面Class物件集合

AbstractAutowireCapableBeanFactory

resolvableDependencies

Map

儲存修正過的依賴對映關係

DefaultListableBeanFactory

beanDefinitionMap

Map

儲存Bean名稱-->Bean定義對映關係

DefaultListableBeanFactory

beanDefinitionNames

List

儲存Bean定義名稱列表

DefaultListableBeanFactory  

BeanDefinition在IOC容器中的註冊

 

接下來簡要分析一下loadBeanDefinitions。

 

對於這個BeanDefinition,我是這麼理解的: 它是SpringIOC過程中間的一個產物,可以看成是對Bean定義的抽象,裡面封裝的資料都是與Bean定義相關的,封裝了一些基本的bean的Property、initi-method、destroy-method等。

 

這裡的主要方法是loadBeanDefinitions,這裡不詳細展開說,它主要做了幾件事:

 

1、初始化了BeanDefinitionReader

2、通過BeanDefinitionReader獲取Resource,也就是xml配置檔案的位置,並且把檔案轉換成一個叫Document的物件

3、接下來需要將Document物件轉化成容器內部的資料結構(也就是BeanDefinition),也即是將Bean定義的List、Map、Set等各種元素進行解析,轉換成Managed類(Spring對BeanDefinition資料的封裝)放在BeanDefinition中;這個方法是RegisterBeanDefinition(),也就是解析的過程。

4、解析完成後,會把解析的結果放到BeanDefinition物件中並設定到一個Map中

以上這個過程就是BeanDefinition在IOC容器中的註冊。

再回到Refresh方法,總結每一步如下圖:

 

總結:這一部分步驟主要是Spring如何載入Xml檔案或者註解,並把它解析成BeanDefinition。

 

 

Spring建立Bean的過程

 

先回到之前的refresh方法(也就是在構造ApplicationContext時的方法),我們跳過不重要的部分:

我們直接看finishBeanFactoryInitialization裡面的preInstantiateSingletons方法,顧名思義初始化所有的單例bean,擷取部分如下:

現在來看核心的getBean方法,對於所有獲取Bean物件是例項,都是用這個getBean方法,這個方法最終呼叫的是doGetBean方法,這個方法就是所謂的DI(依賴注入)發生的地方。

程式=資料+演算法,之前的BeanDefinition就是“資料”,依賴注入也就是在BeanDefinition準備好情況下進行進行的,這個過程不簡單,因為Spring提供了很多引數配置,每一個引數都代表了IOC容器的特性,這些特性的實現需要在Bean的生命週期中完成。

程式碼比較多,就不貼了,大家可以自行檢視AbstractBeanFactory裡面的doGetBean方法,這裡直接上圖,這個圖就是依賴注入的整個過程:

總結:Spring建立好了BeanDefinition之後呢,會開始例項化Bean,並且對Bean的依賴屬性進行填充。例項化時底層使用了CGLIB或Java反射技術。上圖中instantiateBean核PupulateBean方法很重要!

迴圈依賴問題分析

我們先總結一下之前的結論:

1、構造器注入和prototype型別的field注入發生迴圈依賴時都無法初始化

2、field注入單例的bean時,儘管有迴圈依賴,但bean仍然可以被成功初始化

針對這幾個結論,提出問題

  1. 單例的設值注入bean是如何解決迴圈依賴問題呢?如果A中注入了B,那麼他們初始化的順序是什麼樣子的?

  2. 為什麼prototype型別的和構造器型別的Spring無法解決迴圈依賴呢?

之前在DefaultListableBeanFactory類中,列出了一個表格;現在我把關鍵的精華屬性列出來:

一級快取:
/** 儲存所有的singletonBean的例項 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

二級快取:
/** 儲存所有早期建立的Bean物件,這個Bean還沒有完成依賴注入 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
三級快取:
/** singletonBean的生產工廠*/
private final Map<String, ObjectFactory> singletonFactories = new HashMap<String, ObjectFactory>(16);

/** 儲存所有已經完成初始化的Bean的名字(name) */
private final Set<String> registeredSingletons = new LinkedHashSet<String>(64);

/** 標識指定name的Bean物件是否處於建立狀態  這個狀態非常重要 */
private final Set<String> singletonsCurrentlyInCreation =
 Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16))

前面三個Map,我們稱為單例初始化的三級快取,理解這個問題,我們目前只需關注“三級”,也就是singletonFactories

分析:

對於問題1,單例的設值注入,如果A中注入了B,B應該是A中的一個屬性,那麼猜想應該是A已經被instantiate(例項化)之後,在populateBean(填充A中的屬性)時,對B進行初始化。

對於問題2,instantiate(例項化)其實就是理解成new一個物件的過程,而new的時候肯定要執行構造方法,所以猜想對於應該是A在instantiate(例項化)時,進行B的初始化。

有了分析和猜想之後呢,圍繞關鍵的屬性,根據從上圖的doGetBean方法開始到populateBean所有的程式碼,我整理了如下圖:

上圖是整個過程中關鍵的程式碼路徑,感興趣的可以自己debug幾回,最關鍵的解決迴圈依賴的是如上的兩個標紅的方法,第一個方法getSingleton會從singletonFactories裡面拿Singleton,而addSingletonFactory會把Singleton放入singletonFactories。

對於問題1:單例的設值注入bean是如何解決迴圈依賴問題呢?如果A中注入了B,那麼他們初始化的順序是什麼樣子的?

假設迴圈注入是A-B-A:A依賴B(A中autowire了B),B又依賴A(B中又autowire了A):

本質就是三級快取發揮作用,解決了迴圈。

對於當時問題2,instantiate(例項化)其實就是理解成new一個物件的過程,而new的時候肯定要執行構造方法,所以猜想對於應該是A在instantiate(例項化)時,進行B的初始化。

答案也很簡單,因為A中構造器注入了B,那麼A在關鍵的方法addSingletonFactory()之前就去初始化了B,導致三級快取中根本沒有A,所以會發生死迴圈,Spring發現之後就丟擲異常了。至於Spring是如何發現異常的呢,本質上是根據Bean的狀態給Bean進行mark,如果遞迴呼叫時發現bean當時正在建立中,那麼久丟擲迴圈依賴的異常即可。

那麼prototype的Bean是如何初始化的呢?

prototypeBean有一個關鍵的屬性:

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

儲存著正在建立的prototype的beanName,在流程上並沒有暴露任何factory之類的快取。並且在beforePrototypeCreation(String beanName)方法時,把每個正在建立的prototype的BeanName放入一個set中:

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

並且會迴圈依賴時檢查beanName是否處於建立狀態,如果是就丟擲異常:

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

從流程上就可以檢視,無論是構造注入還是設值注入,第二次進入同一個Bean的getBean方法是,一定會在校驗部分丟擲異常,因此不能完成注入,也就不能實現迴圈引用。

總結:Spring在InstantiateBean時執行構造器方法,構造出例項,如果是單例的話,會將它放入一個singletonBeanFactory的快取中,再進行populateBean方法,設定屬性。通過一個singletonBeanFactory的快取解決了迴圈依賴的問題。

 

再解決一個問題

 

現在大家已經對Spring整個流程有點感覺了,我們再來解決一個簡單的常見的問題:

 

考慮一下如下的singleton程式碼:

   @Service
   public class SingletonBean{

      @Autowired 
      private PrototypeBean prototypeBean;

      public void doSomething(){
        System.out.println(prototypeBean.toString());       
      }

   }
@Component 
    @Scope(value="prototype")
    public class PrototypeBean{
    }

一個Singleton的Bean中Autowired了一個prototype的Bean,那麼問題來了,每次呼叫SingletonBean.doSomething()時列印的物件是不是同一個呢?

 

有了之前的知識儲備,我們簡單分析一下:因為Singleton是單例的,所以在專案啟動時就會初始化,prototypeBean本質上只是它的一個Property,那麼ApplicationContex中只存在一個SingletonBean和一個初始化SingletonBean時建立的一個prototype型別的PrototypeBean。

那麼每次呼叫SingletonBean.doSomething()時,Spring會從ApplicationContex中獲取SingletonBean,每次獲取的SingletonBean是同一個,所以即便PrototypeBean是prototype的,但PrototypeBean仍然是同一個。每次打印出來的記憶體地址肯定是同一個。

 

那這個問題如何解決呢?

 

解決辦法也很簡單,這種情況我們不能通過注入的方式注入一個prototypeBean,只能在程式執行時手動呼叫getBean("prototypeBean")方法,我寫了一個簡單的工具類:

@Service
public class SpringBeanUtils implements ApplicationContextAware {  
   private static ApplicationContext appContext;  
   @Override  
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 
      SpringBeanUtils.appContext=applicationContext;  
   }  
   public static ApplicationContext getAppContext() {    
       return appContext;  
   }  
   public static Object getBean(String beanName) {    
       checkApplicationContext();    
       return appContext.getBean(beanName);  
   }  
   private static void checkApplicationContext() {    
       if (null == appContext) {      
           throw new IllegalStateException("applicaitonContext未注入");   
        }  
   }  
   @SuppressWarnings("unchecked")  
   public static T getBean(Classclazz) {    
       checkApplicationContext();    
       Map map = appContext.getBeansOfType(clazz);    
       return map.isEmpty() ? null : (T) map.values().iterator().next();  
   }
}

對於這個ApplicationContextAware介面:

在某些特殊的情況下,Bean需要實現某個功能,但該功能必須藉助於Spring容器才能實現,此時就必須讓該Bean先獲取Spring容器,然後藉助於Spring容器實現該功能。為了讓Bean獲取它所在的Spring容器,可以讓該Bean實現ApplicationContextAware介面。

感興趣的讀者自己可以試試。

 

 

 

總結:

 

回到迴圈依賴的問題,有的人可能會問singletonBeanFactory只是一個三級快取,那麼一級快取和二級快取有什麼用呢?

 

其實大家只要理解整個流程就可以切入了,Spring在初始化Singleton的時候大致可以分幾步,初始化——設值——銷燬,迴圈依賴的場景下只有A——B——A這樣的順序,但在併發的場景下,每一步在執行時,都有可能呼叫getBean方法,而單例的Bean需要保證只有一個instance,那麼Spring就是通過這些個快取外加物件鎖去解決這類問題,同時也可以省去不必要的重複操作。Spring的鎖的粒度選取也是很吊的,這裡暫時不深入研究了。

 

解決此類問題的關鍵是要對SpringIOC和DI的整個流程做到心中有數,看原始碼一般情況下不要求每一行程式碼都瞭解透徹,但是對於整個的流程和每個流程中在做什麼事需要了然,這樣實際遇到問題時才可以很快的切入進行分析解決。

 

希望這篇文章可以幫助你對Spring的IOC和DI的流程有一個更深刻的認識!