Spring揭祕-Spring的IoC容器之BeanFactory
1.Spring容器的介紹
Spring的IoC容器是一個IoC Service Provider,但是,這只是它被冠以IoC之名的部分原因,我們不能忽略的是”容器”。Spring的IoC容器是一個提供IoC支援的輕量級容器,除了提供基本的IoC支援,它作為輕量級容器還提供了IoC之外的支援。如在Spring的IoC容器之上,Spring還提供了相應的AOP框架支援,企業級服務整合等服務。Spring的IoC容器和IoC Service Provider所提供的服務之間存在一定的交集,二者的關係如圖
1.1Spring提供了兩種容器型別:
BeanFactory:基礎型別IoC容器,提供完整的IoC服務支援。如果沒有特殊指定,預設採用延遲初始化策略(lazy-load)
ApplicationContext: ApplicationContext在BeanFactory的基礎上構建,是相對比較高階的容器實現,除了擁有BeanFactory的所有支援,ApplicationContext還提供了其他高階特性,比如事件釋出,國際化資訊支援等,這些會在後面詳述。ApplicationContext所管理的物件,在該型別容器啟動之後,預設全部初始化並繫結完成
BeanFactory,就是bean的工廠,既然spring框架提倡使用POJO,那麼把每個業務物件看作一個JavaBean物件,那麼把每個業務物件看作一個JavaBean物件,或許理解為什麼Spring的IoC基本容器會起這麼一個名字。
擁有BeanFactory後,物件之間的依賴關係的解決方式改變了,以前我們是需要自己去”拉”(Pull)所依賴的業務物件,現在需要依賴什麼,讓BeanFactory為我們推過來(Push)就行了
2.BeanFactory的物件註冊與依賴繫結方式
2.1直接編碼方式:
其實,把編碼方式單獨提出來稱作一種方式並不合適,不管使用什麼方式,最終都需要編碼才能”落實”所有資訊並付諸使用。不過通過這些程式碼,起碼可以讓我們更加清楚BeanFactory在底層是如何運作的
通過編碼方式使用BeanFactory實現業務物件的註冊及繫結
public static void main(String[] args){
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory contaioner = (BeanFactory)bindViaCode(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider) contaioner.getBean(“djNewsProvider”);
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaCode(BeanDefinitionRegistry registry){
//bean定義
AbstractBeanDefinition newsProvider =
new RootBeanDefinition(FXNewsProvider.class,true);
AbstractBeanDefinition newsListener =
new RootBeanDefinition(DowJonesNewsListener.class,true);
AbstractBeanDefinition newsPersister =
new RootBeanDefinition(DowJonesNewsPersister.class,true);
//將bean定義註冊到容器中
Registry.registerBeanDefinition(“djNewsProvider”,newsProvider);
Registry.registerBeanDefinition(“djListener”,newsProvider);
Registry.registerBeanDefinition(“djPersister”, newsPersister);
//1.可以通過構造方法注入方式
ConstructorArgumentValues argValues = new ConstructorArgumentValues();
argValues.addIndexedArgumentValue(0,newsListener);
argValues.addIndexedArgumentValue(1,newsPersister);
newsProvider.setConstructorArgumentValues(argValues);
//2.或者通過setter方法注入方式
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.addPropertyValue(new propertyValue(“newsListener”, newsListener));
propertyValues.addPropertyValue(new propertyValue(“newsPersister”, newsPersister));
//繫結完成
return (BeanFactory)registry;
}
先看下圖:
BeanFactory是個介面不能直接使用,DefaultListableBeanFactory同時是BeanFactory和BeanDefinitionRegistry的實現類
BeanDefinitionRegistry介面在BeanFactory的實現中擔當Bean註冊管理的角色,定義了抽象Bean的註冊邏輯 BeanFactory介面只定義如何訪問容器內管理Bean的方法
每一個受管的物件,在容器中都會有一個BeanDefinition的例項(instance)與之相對應,該BeanDefinition的例項負責儲存物件的所有必要資訊,包括其對應的物件的class型別,是否是抽象類,構造方法引數以及其他屬性等。當客戶端向BeanFactory請求相應物件的時候,BeanFactory會通過這些資訊為客戶端返回一個完備可用的物件例項。
2.2外部配置檔案方式
採用外部配置檔案時、 Spring的loC容器有一個統一的處理方式。 通常情況下,需要根據不同的外部配置檔案格式,給出相應的BeandefinitionReader現類,由 BeanDefinitionReader的相應實現類負責將相應的配置檔案內容讀取並對映到BeanDefinition,然後將對映後的BeanDefinition註冊到一個BeanDefinitionRegistry,之後,BeanDefinitionRegistry即完成Bean的註冊和載入。實當然,大部分工作,包括解析檔案格式、裝配 BeanDefinition之類的工作,都是由 BeandefinitionReader的相應實現類來做的, BeanDefinitionRegistry只不過負責保管而已。
2.2.1 Properties配置格式的載入(略)
2.2.2 XML配置格式的載入
載入XML配置檔案的BeanFactory的使用演示
public static void main(String[] args){
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaXMLFile(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider)container.getBean(“djNewsProvider”);
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaXMLFile(BeanDefinitionRegistry registry){
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
Reader.loadBeanDefinitions(“classpath:../news-config.xml”);
Return (BeanFactory)registry;
//或者直接
//return new XmlBeanFactory(new ClassPathResource(“../news-config.xml”));
}
與為Properties配置檔案格式提供PropertiesBeanDefinitionReader相對應,Spring同樣為XML格式的配置檔案提供了現成的BeanDefinitionReader實現,即XmlBeanDefinitionReader。XmlBeanDefinitionReader負責讀取Spring指定格式的XML配置檔案並解析,之後將解析後的檔案內容對映到相應的BeanDefinition,並載入相應的BeanDefinitionRegistry(在這裡是Default-ListableBeanFactory),這時,整個BeanFactory就可以放給客戶端使用了。 除了提供XmlBeanDefinitionReader用於XML格式配置檔案的載入,Spring還在DefaultListableBeanFactory的基礎上構建了簡化XML格式配置載入的XmlBeanFactory實現。從以上程式碼最後註釋掉的一行,你可以看到使用了XmlBeanFactory之後,完成XML的載入和BeanFactory的初始化是多麼簡單
2.2.3 註解方式
@Component
public class FXNewsProvider{
@Autowired
private IFXNewsListener newsListener;
@Autowired
private IFXNewsListener newsListener;
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister){
this. newsListner = newsListner;
this. newsPersister = newsPersister;
}
….
}
@Component
Public class DowJonesNewsListener implements IFXNewsListener{
…
}
@Component
Public class DowJonesNewsPersister implements IFXNewsPersister{
…
}
然後在配置檔案中使用
<context:component-scan base-package=”cn.spring21.project.base.package”/>
然後在以上所有這些工作都完成之後,我們就可以像通常那樣載入並執行當前應用程式了,如下所示:
public static void main(String[] args){
ApplicationContext ctx = new ClassPathXmlApplicationContext(“配置檔案路徑”);
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean(“FXNewsProvider”);
newsProvider.getAndPersistNews();
}
3.BeanFactory的XML之旅
3.1bena的scope
-
singleton 配置中的bean定義可以看作是一種模板,容器會根據這個模板來構造物件。但是要根據這個模板構造多少物件例項,又該讓這些構造完的物件例項存活多久,則由容器根據bean定義的scope語義來決定。標記為singleton scope(容器預設的scope)的物件定義,①(物件例項數量)在Spring的IoC容器中只存在一個例項,所有對該物件的引用將共享這個例項。②(物件存活時間)該例項從容器啟動,並因為第一次被請求而初始化之後,將一直存活到容器退出,也就是說,它與IoC容器”幾乎”擁有相同的”壽命”.
-
prototype 針對宣告為擁有 prototype scope的beam定義,容器在接到該型別物件的請求的時候,會每次都重新生成一個新的物件例項給請求方。雖然這種型別的物件的例項化以及屬性設定等工作都是由容器負責的,但是隻要準備完畢,並且物件例項返回給請求方之後,容器就不再擁有當前返回物件的引用,請求方需要自己負責當前返回物件的後繼生命週期的管理工作,包括該物件的銷燬。也就是說,容器每次返回給請求方一個新的物件突例之後,就任由這個物件例項“自生自滅”了
-
request session和global session 這三個scope型別是Spring2.0之後新增加的,它們不像之前的singleton和prototype那麼”通用”,因為它們只適用於Web應用程式,通常是與XmlWebApplicationContext共同 使用
4.容器背後的祕密
先看圖
4.1Spring的IoC容器功能實現的兩個階段:
功能:Spring的IoC容器所起的作用,就像上圖那樣,它會以某種方式載入Configuration Metadata(通常也就是XML格式的配置資訊),然後根據這些資訊繫結整個系統的物件
Spring的IoC容器實現以上功能的過程,基本上可以按照類似的流程劃分為兩個階段,即容器啟動階段和Bean例項化階段,如下圖所示
4.1.1 容器啟動階段
容器啟動伊始,首先會通過某種途徑載入 Configuration Metadata。除了程式碼方式比較直接,在大部分情況下,容器需要依賴某些工具類( BeanDefinitionReader)對載入的 Configuration Metadata進行解析和分析。並將分析後的資訊編組為相應的BeanDefinition,最後把這些儲存了bean定義必要資訊BeanDefinition,註冊到相應的 BeanDefinitionRegistry,這樣容器啟動工作就完成了。 下圖演示了這個階段的主要工作。 總地來說,該階段所做的工作可以認為是準備性的,重點更加側重於物件管理資訊的收集。當然,一些驗證性或者輔助性的工作也可以在這個階段完成
4.1.2 Bean例項化階段
經過第一階段,現在所有的bean定義資訊都通過 Beandefinition的方式註冊到了 BeanDefinitionRegistry中。當某個請求通過容器的getBean方法明確地請求某個物件,或者因依賴關係容器需要隱式地呼叫getBean方法時,就會觸發第二階段的活動。 該階段,容器會首先檢查所請求的物件之前是否已經初始化。如果沒有初始化。則會根據註冊的BeanDefinition所提供的資訊例項化被請求物件,併為其注入依賴。如果該物件實現了某些回撥介面,也會根據回撥介面的要求來裝配它。當該物件裝配完畢之後,容器會立即將其返回請求方使用。如果說第一階段只是根據圖紙裝配生產線的話,那麼第二階段就是使用裝配好的生產線來生產具體的產品了
4.2 插手”容器的啟動”
Spring提供了一種叫做BeanFactoryPostProcessor的容器擴充套件機制,該機制允許我們在容器例項化相應物件之前,對註冊到容器的BeanDefinition所儲存的資訊做相應的修改,這就相當於在容器實現的第一階段最後加入一道工序,讓我們對最終的BeanDefinition做一些額外的操作,比如修改其中bean定義的某些屬性,為bean定義增加其他資訊等。
可以自己實現BeanFactoryPostProcessor ,如果有多個BeanFactoryPostProcessor,需要為其新增Order實現。Spring提供了幾個現成的BeanFactoryPostProcessor實現類,分別是PropertyPlaceholderConfigurer和OverrideConfigurer,最後還有CustomEditorConfigurer,對於BeanFactory來說,我們需要用手動方式應用所有的BeanFactoryPostProcessor,這裡不作介紹,對於ApplicationContext,來說,簡單很多,因為ApplicationContext會自動識別配置檔案中的BeanFactoryPostProcessor,僅需要在XML配置檔案中簡單配置一下即可,如配置CustomEditorConfigurer
<bean class =”org.springframework.beans.factory.config.CustomEditorConfigurer”>
<property name=”customEditors”>
<map>
<entry key=”java.util.Date”>
<ref bean=”datePropertyEditor”/>
</entry>
</map>
</property>
</bean>
<bean id=”datePropertyEditor” class =”…DatePropertyEditor”>
<property name=”datePattern”>
<value>yyyy/MM/dd</value>
</property>
</bean>
以下對這三個BeanFactoryPostProcessor實現類進行介紹,重點介紹CustomEditorConfigurer
4.2.1 PropertyPlaceholderConfigurer
配置檔案中的佔位符
4.2.2 OverrideConfigurer
可以對容器中配置的任何你想處理的bean定義的property資訊進行覆蓋替換
4.2.3 CustomEditorConfigurer
為了處理配置檔案中的資料型別與真正的業務物件所定義的資料型別轉換其它兩個BeanFactoryPostProcessor都是通過BeanDefinition中的資料進行變更以達到某種目的,與它們有所不同,CustomEditorConfigurer是另一種型別的BeanFactoryPostProcessor實現,它只是輔助性地將後期會用到的資訊註冊到容器,對BeanDefinition沒有任何變動,我們知道,不管物件是什麼型別,也不管這些物件所宣告的依賴物件是什麼型別,通常都是通過XML(或者 properties甚至其他媒介)檔案格式來配置這些物件型別。但XML所記載的,都是 string型別,即容器從XML格式的檔案中讀取的都是字串形式,最終應用程式卻是由各種型別的物件所構成。要想完成這種由字串到具體物件的轉換(不管這個轉換工作最終由誰來做),都需要這種轉換規則相關的資訊,而CustomEditorConfigurer就是幫助我們傳達類似資訊的。
Spring內部通過 Javabean的PropertyEditor來幫助進行和String型別到其他型別的轉換工作。只要為每種物件型別提供一個 PropertyEditor,就可以根據該物件型別取得與其相對應PropertyEditor來做具體的型別轉換。Spring容器內部在做具體的型別轉換的時候,會採用JavaBean框架內預設的PropertyEditor的搜尋邏輯,從而繼承了對原生型別以及java.lang.String.java.awt.Color和java.awt.Font等型別的轉換支援。同時,Spring框架還提供了自身實現的一些PropertyEditor,這些PropertyEditor大部分都位於org.springframework.beans.propertyeditors包下。
其中有StringArrayPropertyEditor ClassEditor FileEditor LocaleEditor PatternEditor以上這些PropertyEditor,容器通常會預設載入使用,所以,即使我們不告訴容器應該如何對這些型別進行轉換,容器同樣可以正確完成工作。但當我們需要指定的型別沒有包含在以上所提到的PropertyEditor之列的時候,就需要給出針對這種型別的PropertyEditor實現,並通過CustomEditorConfigurer告知容器,以便容器在適當的時機使用到適當的PropertyEditor,詳細使用方式,這裡不作介紹,請自行查詢資料
5.瞭解bean的一生
容器啟動之後,並不會馬上就例項化相應的bean定義,我們知道,容器現在僅僅擁有所有物件的BeanDefinition來儲存例項化階段將要用的必要資訊。只有當請求通過BeanFactory的getBean()方法來請求某個物件例項的時候,才有可能觸發Bean例項化階段的活動。BeanFactory的getBean方法可以被客戶端物件顯示呼叫,也可以在容器內部隱式地被呼叫。隱式呼叫有如下兩種情況 1.對於 Beanfactory來說,物件例項化預設採用延遲初始化。通常情況下,當物件A被請求而需要第一次例項化的時候,如果它所依賴的物件B之前同樣沒有被例項化,那麼容器會先例項化物件A所依賴的物件。這時容器內部就會首先例項化物件B,以及物件A依賴的其他還沒有被例項化的物件。這種情況是容器內部呼叫getBean(),對於本次請求的請求方是隱式的。
2.Applicationcontext啟動之後會例項化所有的bean定義。但 ApplicationContext在實現的過程中依然循Sping容器實現流程的兩個階段,只不過它會在啟動階段的活動完成之後,緊接著呼叫註冊到該容器的所有bean定義的例項化方法getBean()。這就是為什麼當你得到 ApplicationContext型別的容器引用時,容器內所有物件已經被全部例項化完成。不信你查一下類org.springframework.context.support.AbstractApplicationContext的 refresh()方法
之所以說 getBean()方法是有可能觸發Bean例項化階段的活動,是因為只有當對應某個bean定義的 getBean()方法第一次波呼叫時,不管是顯式的還是隱式的,Bean例項化階段的活動才會被觸發,第二次被呼叫則會直接返回容器快取的第一次例項化完的物件例項( prototype型別bean除外)。當getBean()方法內部發現該bean定義之前還沒有被例項化之後,會通過 createBean()方法來進行具體的物件例項化,例項化過程如下圖所示
Spring容器將對其所管理的物件全部給予統一的生命週期管理,這些被管理的物件完全擺脫了原來那種”new完後被使用,脫離作用域後即被回收”的命運。
5.1 Bean的例項化與BeanWrapper
容器在內部實現的時候,採用”策略模式”來決定採用何種方式初始化bean例項,通常,可以通過反射或者CGLIB動態位元組碼生成來初始化相應的bean例項或者動態生成其子類
org.springframework.beans.factory.support.InstantiationStrategy定義是例項化策略的抽象介面,其直接子類SimpleInstantiationStrategy實現了簡單的物件例項化功能,可以通過反射來例項化物件例項,但不支援方法注入方式的物件例項化。CglibSubclassingInstantiationStrategy繼承了SimpleInstantiationStrategy的以反射方式例項化物件的功能,並且通過CGLIB的動態位元組碼生成功能,該策略實現類可以動態生成某個類的子類,進而滿足了方法注入所需的物件例項化需求。預設情況下,容器內部採用的是CglibSubclassingInstantiationStrategy 容器只要根據相應bean定義的BeanDefinition取得例項化資訊結合CglibSubclassingInstantiationStratery以及不同的bean定義型別,就可以返回例項化完成的物件例項。但是,返回方式上有些”點綴”.不是直接返回構造完成的物件例項,而是以BeanWrapper對構造完成的物件例項進行包裹,返回相應的BeanWrapper例項。
至此,第一步結束。
BeanWrapper介面通常在Spring框架內部使用,它有一個實現類org.springframework.beans.BeanWrapperImpl. 其作用是對某個bean進行”包裹”,然後對這個”包裹”的bean進行操作,比如設定或者獲取bean的相應屬性值。而在第一步結束後返回BeanWrapper例項而不是原先的物件例項,就是為了第二步”設定物件屬性”。
BeanWrapper定義繼承了org.springframework.beans.PropertyAccessor介面,可以以統一的方式對物件屬性進行訪問;BeanWrapper定義同時又直接或者間接繼承了PropertyEditorRegistry和TypeConverter介面。不知你是否還記得CustomEditorConfigurer?當把各種PropertyEditor註冊給容器時,BeanWrapper用到這些PropertyEditor,在第一步構造完成物件之後,Spring會根據物件例項構造一個BeanWrapperImpl例項,然後將之前CustomEditorConfigurer註冊的PropertyEditor複製一份給BeanWrapperImpl例項(這就是BeanWrapper同時又是PropertyEditorRegistry的原因),這樣,當BeanWrapper轉換型別,設定物件屬性時,就不會無從下手了。
5.2 各色的Aware介面
當物件例項化完成並且相關屬性以及依賴設定完成之後,Spring容器會檢查當前物件例項是否實現了一系列的以Aware命名結尾的介面定義,如果是,則將這些Aware介面定義中規定的依賴注入給當前物件例項
5.3 BeanPostProcessor
BeanpostProcessor的概念容易與 BeanfactoryPostProcessor的概念混淆。但只要記住BeanPostProcessor是存在於物件例項化階段,而BeanFactoryPostProcessor則是存在於容器啟動階段,這兩個概念就比較容易區分了。 與BeanFactoryPostProcessor通常會處理容器內所有符合條件的 BeanDefinition類似,BeanPostProcessor會處理容器內所有符合條件的例項化後的物件例項。該介面聲明瞭兩個方法,分別在兩個不同的時機執行,見如下程式碼定義:
Public interface BeanPostProcessor{
Object postProcessBeforeInitialization(Object bean,String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean,String beanName) throws BeansException;
}
BeanPostProcessor的兩個方法中都傳入了原來的物件例項的引用,這為我們擴充套件容器的物件例項化過程中的行為提供了極大的便利,我們幾乎可以對傳入的物件例項執行任何操作。 通常比較常見的使用BeanPostProcessor的場景,是處理標記介面實現類,或者為當前物件提供代理實現,還可以通過BeanPostProcessor對當前物件例項做更多的處理,比如替換當前物件例項或者位元組碼增強當前物件例項等。Spring的AOP則更多地使用BeanPostProcessor來為物件生成相應的代理物件,如org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator。
BeanPostProcessor是容器提供的物件例項化階段的強有力擴充套件點。
我們可以自定義BeanPostProcessor,對於BeanFactory容器來說,需要採用手工編碼的方式將BeanPostProcessor實現類註冊到容器。對於ApplicationContext容器來說,事情則方便很多,直接將相應的BeanPostProcessor實現類通過通常的XML配置檔案配置一下即可。ApplicationContext容器會自動識別並載入註冊到容器的BeanPostProcessor
實際上,並非所有註冊到Spring容器內的bean定義都是按照bean的例項化過程圖例項化的
5.4 InitializingBean和init-method
org. springframework.beans. factory.InitializingBean是容器內部廣泛使用的一個物件生命週期標識介面其定義如下:
public interface InitializingBean{
void afterPropertiesSet() throws Exception;
}
該介面定義很簡單,其作用在於,在物件例項化過程呼叫過“ BeanPostProcessor的前置處理”之後,會接著檢測當前物件是否實現了InitializingBean介面,如果是,則會呼叫其afterPropertiesSet()方法進一步調整物件例項的狀態。比如,在有些情況下,某個業務物件例項化完成後,還不能處於可以使用狀態。這個時候就可以讓該業務物件實現該介面,並在方法 afterPropertiesSet()中完成對該業務物件的後續處理。 雖然該介面在Spring容器內部廣泛使用,但如果真的讓我們的業務物件實現這個介面,則顯得Spring容器比較具有侵入性。所以,Spring還提供了另一種方式來指定自定義的物件初始化操作,那就是在XML配置的時候,使用的init-method屬性。
通過init-method,系統中業務物件的自定義初始化操作可以以任何方式命名,而不再受制於InitializingBean的 afterPropertiesSet()。如果系統開發過程中規定:所有業務物件的自定義初始化操作都必須以init()命名,為了省去挨個的設定init-method這樣的煩瑣,我們還可以通過最頂層的的 default-init-method統一指定這-init()方法名。
5.5 DisposableBean與destroy-method
當所有的一切,該設定的設定,該注入的注入,該呼叫的呼叫完成之後,容器將檢查 singleton型別的bean例項,看其是否實現了org.spring framework.beans.factory. Disposablebean介面。或者其對應的bean定義是否通過的 destroy-method屬性指定了自定義的物件銷燬方法。如果是,就會為該例項註冊一個用於物件銷燬的回撥( Callback),以便在這些 singleton型別的物件例項銷燬之前,執行銷燬邏輯 與 InitializingBean和init-method用於物件的自定義初始化相對應, DisposableBean和 destroy-method為物件提供了執行自定義銷燬邏輯的機會。
最常見到的該功能的使用場景就是在 Spring容器中註冊資料庫連線池,在系統退出後,連線池應該關閉,以釋放相應資源。
不過,這些自定義的物件銷燬邏輯,在物件例項化完成並註冊了相關的回撥方法之後,並不會馬上執行。回撥方法註冊後,返回的物件例項即處於使用狀態,只有該物件例項不再被使用的時候,才會執行相關的自定義銷燬邏輯,此時通常也就是Spring容器關閉的時候。但Spring容器在關閉之前,不會聰明到自動呼叫這些回撥方法。所以,需要我們告知容器,在哪個時間點來執行物件的自定義銷燬方法。具體使用方法自行查閱資料,並不難。這裡不再作過多篇幅解釋。
好了,關於BeanFactory就介紹到這了。歡迎留言區評論