完全理解SpringIOC、DI-這篇文章就夠了

本文已收錄在公衆號:https://mp.weixin.qq.com/s/FIm84EGVV21phajCaLjgaA算法

前言

你可能會有以下問題:spring

一、想看Spring源碼,可是不知道應當如何入手去看,對整個Bean的流程沒有概念,碰到相關問題也沒有頭緒如何下手數組

二、看過幾遍源碼,沒辦法完全理解,沒什麼感受,沒過一陣子又忘了緩存

本文將結合實際問題,由問題引出源碼,並在解釋時會盡可能以圖表的形式讓你一步一步完全理解Spring Bean的IOC、DI、生命週期、做用域等。bash

先看一個循環依賴問題

現象

循環依賴其實就是循環引用,也就是兩個或則兩個以上的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。app


 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;
}複製代碼

結果:項目啓動成功ide


 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> 存儲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<ApplicationListener> Spring提供的事件管理機制中的應用監聽器 AbstractApplicationContext

構造方法以下:


接下來大概看看refresh方法:


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

一、方法爲何加鎖? 是爲了不多線程的場景下同時刷新Spring上下文

二、雖然整個方法是加鎖的,可是卻用了Synchronized關鍵字的對象鎖startUpShutdownMonitor,這樣作有兩個好處:

(1)關閉資源的時候會調用close()方法,close()方法也使用了一樣的對象鎖,而關閉資源的close和refresh的兩個衝突的方法,這樣能夠避免衝突

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

三、這個方法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<String, String> 存儲Bean名稱->Bean別名映射關係 SimpleAliasRegistry
singletonObjects Map<String, Object> 存儲單例Bean名稱->單例Bean實現映射關係 DefaultSingletonBeanRegistry
singletonFactories Map<String, ObjectFactory> 存儲Bean名稱->ObjectFactory實現映射關係 DefaultSingletonBeanRegistry
earlySingletonObjects Map<String, Object> 存儲Bean名稱->預加載Bean實現映射關係 DefaultSingletonBeanRegistry
registeredSingletons Set<String> 存儲註冊過的Bean名 DefaultSingletonBeanRegistry
singletonsCurrentlyInCreation Set<String> 存儲當前正在建立的Bean名 DefaultSingletonBeanRegistry
disposableBeans Map<String, Object>

存儲Bean名稱->Disposable接口實現Bean實現映射關係

DefaultSingletonBeanRegistry
factoryBeanObjectCache Map<String, Object> 存儲Bean名稱->FactoryBean接口Bean實現映射關係 FactoryBeanRegistrySupport
propertyEditorRegistrars Set<PropertyEditorRegistrar> 存儲PropertyEditorRegistrar接口實現集合 AbstractBeanFactory
embeddedValueResolvers List<StringValueResolver> 存儲StringValueResolver(字符串解析器)接口實現列表 AbstractBeanFactory
beanPostProcessors List<BeanPostProcessor> 存儲 BeanPostProcessor接口實現列表 AbstractBeanFactory
mergedBeanDefinitions Map<String, RootBeanDefinition> 存儲Bean名稱->合併過的根Bean定義映射關係 AbstractBeanFactory
alreadyCreated Set<String> 存儲至少被建立過一次的Bean名集合 AbstractBeanFactory
ignoredDependencyInterfaces Set<Class> 存儲不自動裝配的接口Class對象集合 AbstractAutowireCapableBeanFactory
resolvableDependencies Map<Class, Object> 存儲修正過的依賴映射關係 DefaultListableBeanFactory
beanDefinitionMap Map<String, BeanDefinition> 存儲Bean名稱-->Bean定義映射關係 DefaultListableBeanFactory
beanDefinitionNames List<String> 存儲Bean定義名稱列表 DefaultListableBeanFactory  

BeanDefinition在IOC容器中的註冊

接下來簡要分析一下loadBeanDefinitions。

對於這個BeanDefinition,我是這麼理解的: 它是SpringIOC過程當中間的一個產物,能夠當作是對Bean定義的抽象,裏面封裝的數據都是與Bean定義相關的,封裝了一些基本的bean的Property、initi-method、destroy-method等。

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

一、初始化了BeanDefinitionReader

二、經過BeanDefinitionReader獲取Resource,也就是xml配置文件的位置,而且把文件轉換成一個叫Document的對象

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

四、解析完成後,會把解析的結果放到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方法很重要!

循環依賴問題分析

咱們先總結一下以前的結論:

一、構造器注入和prototype類型的field注入發生循環依賴時都沒法初始化

二、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> T getBean(Class<T> clazz) {    
        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的流程有一個更深入的認識!

相關文章
相關標籤/搜索