這裡我們並不會詳細的分析原始碼,只是給出Spring在完成哪些工作的時候使用到了什麼類,這些類具體的職責都是什麼,如果我們要弄清楚Spring Bean例項化的內幕與詳細資訊,那麼可以看哪些原始碼? 至於具體的詳細的程式碼資訊,大家可以檢視Spring相關類的程式碼。
兩個階段這裡首先宣告一下,Spring將管理的一個個的依賴物件稱之為Bean,這從xml配置檔案中也可以看出。
Spring IOC容器就好像一個生產產品的流水線上的機器,Spring創建出來的Bean就好像是流水線的終點生產出來的一個個精美絕倫的產品。既然是機器,總要先啟動,Spring也不例外。因此Bean的一生從總體上來說可以分為兩個階段:
容器啟動階段Bean例項化階段容器的啟動階段做了很多的預熱工作,為後面Bean的例項化做好了充分的準備,我們首先看一下容器的啟動階段都做了哪些預熱工作。
容器啟動階段1、配置元資訊
我們說Spring IOC容器將物件例項的建立與物件例項的使用分離,我們的業務中需要依賴哪個物件不再依靠我們自己手動建立,只要向Spring要,Spring就會以注入的方式交給我們需要的依賴物件。但是,你不幹,我不幹,總要有人幹,既然我們將物件建立的任務交給了Spring,那麼Spring就需要知道建立一個物件所需要的一些必要的資訊。而這些必要的資訊可以是Spring過去支援最完善的xml配置檔案,或者是其他形式的例如properties的磁碟檔案,也可以是現在主流的註解,甚至是直接的程式碼硬編碼。總之,這些建立物件所需要的必要資訊稱為配置元資訊。
我們大家都知道,在Java世界中,萬物皆物件,散落於程式程式碼各處的註解以及儲存在磁碟上的xml或者其他檔案等等配置元資訊,在記憶體中總要以一種物件的形式表示,就好比我們活生生的人對應到Java世界中就是一個Person類,而Spring選擇在記憶體中表示這些配置元資訊的方式就是BeanDefination,這裡我們不會去分析BeanDefination的程式碼,感興趣的可以去看相關原始碼,這裡我們只是需要知道配置元資訊被載入到記憶體之後是以BeanDefination的形存在的即可。
3、BeanDefinationReader
大家肯定很好奇,我們是看得懂Spring中xml配置檔案中一個個的Bean定義,但是Spring是如何看懂這些配置元資訊的呢?這個就要靠我們的BeanDefinationReader了。
不同的BeanDefinationReader就像葫蘆兄弟一樣,各自擁有各自的本領。如果我們要讀取xml配置元資訊,那麼可以使用XmlBeanDefinationReader。如果我們要讀取properties配置檔案,那麼可以使用PropertiesBeanDefinitionReader載入。而如果我們要讀取註解配置元資訊,那麼可以使用 AnnotatedBeanDefinitionReader載入。我們也可以很方便的自定義BeanDefinationReader來自己控制配置元資訊的載入。例如我們的配置元資訊存在於三界之外,那麼我們可以自定義From天界之外BeanDefinationReader。
總的來說,BeanDefinationReader的作用就是載入配置元資訊,並將其轉化為記憶體形式的BeanDefination,存在某一個地方,至於這個地方在哪裡,不要著急,接著往下看!
4、BeanDefinationRegistry
執行到這裡,總算不遺餘力地將存在於各處的配置元資訊載入到記憶體,並轉化為BeanDefination的形式,這樣我們需要建立某一個物件例項的時候,找到相應的BeanDefination然後建立物件即可。那麼我們需要某一個物件的時候,去哪裡找到對應的BeanDefination呢?這種透過Bean定義的id找到物件的BeanDefination的對應關係或者說對映關係又是如何儲存的呢?這就引出了BeanDefinationRegistry了。
Spring透過BeanDefinationReader將配置元資訊載入到記憶體生成相應的BeanDefination之後,就將其註冊到BeanDefinationRegistry中,BeanDefinationRegistry就是一個存放BeanDefination的大籃子,它也是一種鍵值對的形式,透過特定的Bean定義的id,對映到相應的BeanDefination。
5、BeanFactoryPostProcessor
BeanFactoryPostProcessor是容器啟動階段Spring提供的一個擴充套件點,主要負責對註冊到BeanDefinationRegistry中的一個個的BeanDefination進行一定程度上的修改與替換。例如我們的配置元資訊中有些可能會修改的配置資訊散落到各處,不夠靈活,修改相應配置的時候比較麻煩,這時我們可以使用佔位符的方式來配置。例如配置Jdbc的DataSource連線的時候可以這樣配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="maxIdle" value="${jdbc.maxIdle}"></property> <property name="maxActive" value="${jdbc.maxActive}"></property> <property name="maxWait" value="${jdbc.maxWait}"></property> <property name="minIdle" value="${jdbc.minIdle}"></property> <property name="driverClassName" value="${jdbc.driverClassName}"> </property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> 複製程式碼
BeanFactoryPostProcessor就會對註冊到BeanDefinationRegistry中的BeanDefination做最後的修改,替換$佔位符為配置檔案中的真實的資料。
至此,整個容器啟動階段就算完成了,容器的啟動階段的最終產物就是註冊到BeanDefinationRegistry中的一個個BeanDefination了,這就是Spring為Bean例項化所做的預熱的工作。讓我們再透過一張圖的形式回顧一下容器啟動階段都是搞了什麼事吧。
Bean例項化階段需要指出,容器啟動階段與Bean例項化階段存在多少時間差,Spring把這個決定權交給了我們程式設計師(是不是瞬間開心了一點點!)。如果我們選擇懶載入的方式,那麼直到我們伸手向Spring要依賴物件例項之前,其都是以BeanDefinationRegistry中的一個個的BeanDefination的形式存在,也就是Spring只有在我們需要依賴物件的時候才開啟相應物件的例項化階段。而如果我們不是選擇懶載入的方式,容器啟動階段完成之後,將立即啟動Bean例項化階段,透過隱式的呼叫所有依賴物件的getBean方法來例項化所有配置的Bean並儲存起來。
接下來我們就聊一聊Bean例項化過程的那些事兒~
1、物件建立策略
到了這個時候,Spring就開始真刀真槍地幹了,物件的建立採用了策略模式,藉助我們前面BeanDefinationRegistry中的BeanDefination,我們可以使用反射的方式建立物件,也可以使用CGlib位元組碼生成建立物件。同時我們可以靈活的配置來告訴Spring採用什麼樣的策略建立指定的依賴物件。Spring中Bean的建立是策略設計模式的經典應用。這個時候,記憶體中應該已經有一個我們想要的具體的依賴物件的例項了,但是故事的發展還沒有我們想象中的那麼簡單。
2、BeanWrapper——物件的外衣
Spring中的Bean並不是以一個個的本來模樣存在的,由於Spring IOC容器中要管理多種型別的物件,因此為了統一對不同型別物件的訪問,Spring給所有建立的Bean例項穿上了一層外套,這個外套就是BeanWrapper(關於BeanWrapper的具體內容感興趣的請查閱相關原始碼)。BeanWrapper實際上是對反射相關API的簡單封裝,使得上層使用反射完成相關的業務邏輯大大的簡化,我們要獲取某個物件的屬性,呼叫某個物件的方法,現在不需要在寫繁雜的反射API了以及處理一堆麻煩的異常,直接透過BeanWrapper就可以完成相關操作,簡直不要太爽了。
3、設定物件屬性
上一步包裹在BeanWrapper中的物件還是一個少不經事的孩子,需要為其設定屬性以及依賴物件。
對於基本型別的屬性,如果配置元資訊中有配置,那麼將直接使用配置元資訊中的設定值賦值即可,即使基本型別的屬性沒有設定值,那麼得益於JVM物件例項化過程,屬性依然可以被賦予預設的初始化零值。對於引用型別的屬性,Spring會將所有已經建立好的物件放入一個Map結構中,此時Spring會檢查所依賴的物件是否已經被納入容器的管理範圍之內,也就是Map中是否已經有對應物件的例項了。如果有,那麼直接注入,如果沒有,那麼Spring會暫時放下該物件的例項化過程,轉而先去例項化依賴物件,再回過頭來完成該物件的例項化過程。這裡有一個Spring中的經典問題,那就是Spring是如何解決迴圈依賴的?
這裡簡單提一下,Spring是透過三級快取解決迴圈依賴,並且只能解決Setter注入的迴圈依賴,請大家思考一下如何解決?為何只能是Setter注入?詳細內容可以查閱相關部落格,文件,書籍。
4、檢查Aware相關介面
我們知道,我們如果想要依賴Spring中的相關物件,使用Spring的相關API,那麼可以實現相應的Aware介面,Spring IOC容器就會為我們自動注入相關依賴物件例項。Spring IOC容器大體可以分為兩種,BeanFactory提供IOC思想所設想所有的功能,同時也融入AOP等相關功能模組,可以說BeanFactory是Spring提供的一個基本的IOC容器。ApplicationContext構建於BeanFactory之上,同時提供了諸如容器內的時間釋出、統一的資源載入策略、國際化的支援等功能,是Spring提供的更為高階的IOC容器。
講了這麼多,其實就是想表達對於BeanFactory來說,這一步的實現是先檢查相關的Aware介面,然後去Spring的物件池(也就是容器,也就是那個Map結構)中去查詢相關的例項(例如對於ApplicationContextAware介面,就去找ApplicationContext例項),也就是說我們必須要在配置檔案中或者使用註解的方式,將相關例項註冊容器中,BeanFactory才可以為我們自動注入。
而對於ApplicationContext,由於其本身繼承了一系列的相關介面,所以當檢測到Aware相關介面,需要相關依賴物件的時候,ApplicationContext完全可以將自身注入到其中,ApplicationContext實現這一步是透過下面要講到的東東——BeanPostProcessor。
例如ApplicationContext繼承自ResourceLoader和MessageSource,那麼當我們實現ResourceLoaderAware和MessageSourceAware相關介面時,就將其自身注入到業務物件中即可。
5、BeanPostProcessor前置處理
唉?剛才那個是什麼Processor來?相信剛看這兩個東西的人肯定有點暈乎了,我當初也是,不過其實也好區分,只要記住BeanFactoryPostProcessor存在於容器啟動階段而BeanPostProcessor存在於物件例項化階段,BeanFactoryPostProcessor關注物件被建立之前 那些配置的修修改改,縫縫補補,而BeanPostProcessor階段關注物件已經被建立之後 的功能增強,替換等操作,這樣就很容易區分了。
BeanPostProcessor與BeanFactoryPostProcessor都是Spring在Bean生產過程中強有力的擴充套件點。如果你還對它感到很陌生,那麼你肯定知道Spring中著名的AOP(面向切面程式設計),其實就是依賴BeanPostProcessor對Bean物件功能增強的。
BeanPostProcessor前置處理就是在要生產的Bean例項放到容器之前,允許我們程式設計師對Bean例項進行一定程度的修改,替換等操作。
前面講到的ApplicationContext對於Aware介面的檢查與自動注入就是透過BeanPostProcessor實現的,在這一步Spring將檢查Bean中是否實現了相關的Aware介面,如果是的話,那麼就將其自身注入Bean中即可。Spring中AOP就是在這一步實現的偷樑換柱,產生對於原生物件的代理物件,然後將對源物件上的方法呼叫,轉而使用代理物件的相同方法呼叫實現的。
6、自定義初始化邏輯
在所有的準備工作完成之後,如果我們的Bean還有一定的初始化邏輯,那麼Spring將允許我們透過兩種方式配置我們的初始化邏輯:(1)InitializingBean (2)配置init-method引數
一般透過配置init-method方法比較靈活。
7、BeanPostProcess後置處理
與前置處理類似,這裡是在Bean自定義邏輯也執行完成之後,Spring又留給我們的最後一個擴充套件點。我們可以在這裡在做一些我們想要的擴充套件。
8、自定義銷燬邏輯
這一步對應自定義初始化邏輯,同樣有兩種方式:(1)實現DisposableBean介面 (2)配置destory-method引數。
這裡一個比較典型的應用就是配置dataSource的時候destory-method為資料庫連線的close()方法。
9、使用
經過了以上道道工序,我們終於可以享受Spring為我們帶來的便捷了,這個時候我們像對待平常的物件一樣對待Spring為我們產生的Bean例項,如果你覺得還不錯的話,動手試一下吧!
10、呼叫回撥銷燬介面
Spring的Bean在為我們服務完之後,馬上就要消亡了(通常是在容器關閉的時候),別忘了我們的自定義銷燬邏輯,這時候Spring將以回撥的方式呼叫我們自定義的銷燬邏輯,然後Bean就這樣走完了光榮的一生!
我們再透過一張圖來一起看一看Bean例項化階段的執行順序是如何的?
需要指出,容器啟動階段與Bean例項化階段之間的橋樑就是我們可以選擇自定義配置的延遲載入策略,如果我們配置了Bean的延遲載入策略,那麼只有我們在真實的使用依賴物件的時候,Spring才會開始Bean的例項化階段。而如果我們沒有開啟Bean的延遲載入,那麼在容器啟動階段之後,就會緊接著進入Bean例項化階段,透過隱式的呼叫getBean方法,來例項化相關Bean。