對象的生命週期管理在基於面向對象的編程語言中是一個永恆的話題。從語法上講,面向對象的高級編程語言都是以“對象”爲中心的。而對象之間的繼承關係、嵌套引用關係所形成的對象樹結構爲我們進行對象級別的邏輯操作提供了足夠的語法支持。但這樣一來,對象之間所形成的複雜關係也就爲對象生命週期的管理帶來了問題:
- 在程序的運行期,我們如何創建我們所需要的對象?
- 當我們創建一個新的對象時,如何保證與這個對象所關聯的依賴關係(其關聯對象)也能夠被正確地創建出來呢?
downpour 寫道
結論 爲了更好地管理好對象的生命週期,我們有必要在程序邏輯中引入一個額外的編程元素,這個元素就是容器(Container)。
在本章中,我們就來探討這一額外的編程元素 —— 容器(Container)的方方面面,並深入分析XWork框架的容器(Container)實現機制。
5.2 XWork容器概覽
在上一節中,我們已經探討了引入容器的重要意義以及容器在對象生命週期管理中的作用。XWork作爲一個優秀的開發框架,在其內部也實現了一個小型的容器。接下來,我們將對XWork中實現的容器做一個簡單的介紹,其中包括容器的定義、容器的管轄範圍和容器的基本操作。
5.2.1 XWork容器的定義
XWork框架中的容器,被定義成爲一個Java接口,其相關源碼,如代碼清單5-1所示:
- public interface Container extends Serializable {
- /**
- * 定義默認的對象獲取標識
- */
- String DEFAULT_NAME = "default";
- /**
- * 進行對象依賴關係注入的基本操作接口,作爲參數的object將被XWork容器進行處理。
- * object內部聲明有@Inject的字段和方法,都將被注入受到容器託管的對象,
- * 從而建立起依賴關係。
- */
- void inject(Object object);
- /**
- * 創建一個類的實例並進行對象依賴注入
- */
- <T> T inject(Class<T> implementation);
- /**
- * 根據type和name作爲唯一標識,獲取容器中的Java類的實例
- */
- <T> T getInstance(Class<T> type, String name);
- /**
- * 根據type和默認的name(default)作爲唯一標識,獲取容器中的Java類的實例
- */
- <T> T getInstance(Class<T> type);
- /**
- * 根據type獲取與這個type所對應的容器中所有註冊過的name
- * @param type
- * @return
- */
- Set<String> getInstanceNames(Class<?> type);
- /**
- * 設置當前線程的作用範圍的策略
- */
- void setScopeStrategy(Scope.Strategy scopeStrategy);
- /**
- * 刪除當前線程的作用範圍的策略
- */
- void removeScopeStrategy();
- }
從容器(Container)的接口定義方法來看,它完全能夠符合我們之前所討論的容器設計的基本原則之一:簡單而全面。從接口的內容和表現形式來看,他也能符合我們的對容器的基本要求:容器首先被設計成了一個接口而不是具體的實現類;而整個接口定義中既包含了獲取對象實例的方法,也包含了管理對象依賴關係的方法。
在這裏,我們可以看到容器設計的基本原則在一定程度上指導着容器的接口設計,因爲我們更加關心容器能夠對外提供什麼樣的服務,而並不是容器自身的數據結構。
從源碼中,我們可以依照方法的不同作用對這些操作接口進行分類:
- 獲取對象實例 —— getInstance、getInstanceName
- 處理對象依賴關係 —— inject
- 處理對象的作用範圍策略 —— setScopeStrategy、removeScopeStrategy
downpour 寫道
結論 容器(Container)是一個輔助的編程元素,它在整個系統中應該被實例化爲一個全局的、單例的對象。
這是容器實現中最爲基本的一個特性,也是由容器(Container)自身的設計初衷所決定的。如果我們在整個系統中能夠獲取到多個不同的容器的對象實例,或者容器的對象實例在整個系統中的作用域又存在局域性,那麼我們依託容器進行對象生命週期管理就會變得混亂不堪。
downpour 寫道
結論 容器(Container)在系統初始化時進行自身的初始化。系統應該提供一個可靠的、在任何編程層次都能夠對這個全局的容器或者容器中管理對象進行訪問的機制。
這一條結論,是我們對容器(Container)實現的基本要求。從這條結論中,我們可以看到兩個不同的方面:
- 容器的初始化需求 —— 我們應該掌握好容器初始化的時機,並考慮如何對容器實例進行系統級別的緩存
- 系統與容器的通訊機制 —— 我們應該提供一種有效的機制與這個全局的容器實例進行溝通
5.2.2 XWork容器的管轄範圍
既然引入容器(Container)的主要目的在於管理對象的生命週期,那麼在明確了XWork的容器定義之後,我們就非常有必要去了解一下XWork容器的管轄範圍。換句話說,如果我們擁有了這個全局的容器(Container)實例,當我們調用容器的操作接口時,到底操作的是哪些對象呢?
從容器(Container)操作接口的角度,容器的兩類操作接口:獲取對象實例(getInstance)和實施依賴注入(inject),它們所操作的對象也有所不同。接下來我們就對這兩類不同的操作接口分別進行分析。
5.2.2.1 獲取對象實例
當我們調用容器的getInstance方法來獲取對象實例時,我們只能夠獲取到那些“被容器接管”的對象的實例。那麼,哪些對象屬於“被容器接管”的對象呢?
在第三章中,我們已經介紹過Struts2 / XWork的配置元素以及這些配置元素的分類。當時,我們把XML配置文件中基本節點的分爲兩類:其中一類是bean節點和constant節點,我們把這兩個節點統稱爲容器配置元素;另外一類則是package節點,這個節點下的所有配置定義都被稱之爲事件映射關係。而我們進行配置元素分類的基本思路是按照XML節點所表達的邏輯含義和該節點在程序中所起的作用進行的分類。
現在,當我們回過頭來再來看配置元素的分類時,我們就能理解“容器配置元素”的真正含義了。在XML配置元素中,bean節點被廣泛用於定義框架級別的內置對象和自定義對象;而constant節點和Properties文件中的配置選項,則被用於定義系統級別的運行參數。我們之所以把這兩類節點統稱爲“容器配置元素”,就是因爲他們所定義的對象的生命週期,都是由容器(Container)所管理的,這些對象也就是所謂的“被容器接管”的對象。
downpour 寫道
結論XWork容器所管理的對象,包括了所有框架配置定義中的“容器配置元素”。
根據之前的分析,這些對象主要可以被分爲三類:
- 在bean節點中聲明的框架內部對象
- 在bean節點中聲明的自定義對象
- 在constant節點和Properties文件中聲明的系統運行參數
在這裏,我們對這三類容器託管對象的歸納,實際上蘊含了我們對自定義對象納入XWork容器管理的過程:只要在Struts2 / XWork的配置文件中進行聲明即可。
5.2.2.2 對象的依賴注入
當我們調用容器的inject方法來實施依賴注入操作時,所操作的對象卻不僅僅限於“容器配置元素”中所定義的對象。因爲我們對於inject方法的定義是說:只要傳入一個對象的實例,容器將負責建立起傳入對象實例與容器託管對象之間的依賴關係。
由此可見,雖然傳入inject的操作對象是任意的,然而實施依賴注入操作時的那些依賴對象卻是被容器(Container)接管的對象。這就爲我們爲任意對象與XWork容器中所管理的對象之間建立起一條通道提供了有效的途徑。
downpour 寫道
結論 調用XWork容器的inject方法,能夠幫助我們將容器所管理的對象(包括框架的內置對象以及系統的運行參數)注入到任意的對象實例中去,從而建立起任意對象與框架元素溝通的橋樑。
這一條結論對我們非常關鍵,因爲它不僅反映了容器的基本職責,也是我們日後進行應用級別對象操作的理論基礎。有關容器的兩大類操作的具體實現機制,我們將在之後的章節中陸續給出分析。
從方法的命名上,inject非常直觀,表達了“注入”的含義,與我們之前所提到的“依賴注入”的概念是吻合的。如果我們繼續深入思考一下inject方法的邏輯,我們就會發現這個方法的內部實現實際上蘊含了系統與容器對象之間的通訊機制。根據之前我們在XWork的容器(Container)對象的定義,我們可以看到inject方法的調用流程:當某個對象實例作爲參數傳入方法之後,該方法會掃描傳入對象內部聲明有@Inject這個Annotation的字段、方法、構造函數、方法參數並將他們注入容器託管對象,從而建立起傳入對象與容器託管對象之間的依賴關係。
由此可見,整個流程的調用過程被一個神祕的Annotation有效地驅動。我們接下來就首先來看看@Inject這個Annotation的定義,如代碼清單5-2所示:
- @Target({METHOD, CONSTRUCTOR, FIELD, PARAMETER})
- @Retention(RUNTIME)
- public @interface Inject {
- /**
- * 進行依賴注入的名稱。如果不聲明,這個名稱會被設置爲‘default’
- */
- String value() default DEFAULT_NAME;
- /**
- * 是否必須進行依賴注入,僅僅對於方法和參數有效。
- */
- boolean required() default true;
- }
從@Inject的定義中,我們看到這個Annotation可以被設置在任何對象的方法、構造函數、內部實例變量或者參數變量之中。在這裏,我們可以看到對於@Inject的使用並不受限於對象本身。它既可以被加入到Struts2 / XWork的內置對象之上,也可以被加到任意我們自行編寫的對象之上。一旦它被加入到我們自定義的對象之中,那麼我們就建立起了自定義對象與容器託管對象之間的聯繫。因爲被加入了@Inject這個Annotation的方法、構造函數、內部實例變量或者方法參數變量,實際上是在告訴容器:“請爲我注入由容器託管的對象實例”。
細細考慮這個過程,它不正是我們引入容器來解決對象生命週期管理的目標嗎?當我們需要尋求容器幫忙時,只要在恰當的地方加入一個標識符Annotation,容器在進行依賴注入操作時,就能夠知曉並接管整個過程了。在這裏,我們看到兩個過程共同構成了XWork容器進行對象依賴注入操作的步驟:
- 爲某個對象的方法、構造函數、內部實例變量、方法參數變量加入@Inject的Annotation
- 調用容器(Container)的inject方法,完成被加入Annotation的那些對象的依賴注入
5.2.3 XWork容器操作詳解
5.2.3.1 通過容器(Container)接口進行對象操作
在瞭解了XWork中的容器的操作定義以及XWork容器的管轄範圍之後,我們可以看看如何通過直接操作容器(Container)的實例來進行對象操作。
我們首先來看看如何通過容器(Container)對象來獲取對象實例。我們在這裏摘取了XWork框架中的一個處理類DefaultUnknownHandlerManager進行說明,其相關源碼如代碼清單5-3所示:
- public class DefaultUnknownHandlerManager implements UnknownHandlerManager {
- protected ArrayList<UnknownHandler> unknownHandlers;
- private Configuration configuration;
- private Container container;
- @Inject
- public void setConfiguration(Configuration configuration) {
- this.configuration = configuration;
- build();
- }
- @Inject
- public void setContainer(Container container) {
- this.container = container;
- build();
- }
- protected void build() {
- // 如果configuration對象不爲空,則依次從configuration對象
- // 以及Container中讀取UnknowHandler的實例
- if (configuration != null && container != null) {
- List<UnknownHandlerConfig> unkownHandlerStack = configuration.getUnknownHandlerStack();
- unknownHandlers = new ArrayList<UnknownHandler>();
- if (unkownHandlerStack != null && !unkownHandlerStack.isEmpty()) {
- // 根據一定順序獲取UnknownHandlers實例
- for (UnknownHandlerConfig unknownHandlerConfig : unkownHandlerStack) {
- // 調用container對象的getInstance方法獲取UnknownHandler
- UnknownHandler uh = container.getInstance(UnknownHandler.class, unknownHandlerConfig.getName());
- unknownHandlers.add(uh);
- }
- } else {
- // 調用container對象的getInstanceNames方法獲取
- // 所有受到容器管理的UnknownHanlder實例名稱
- Set<String> unknowHandlerNames = container.getInstanceNames(UnknownHandler.class);
- if (unknowHandlerNames != null) {
- // 根據名稱調用container對象的getInstance方法獲取實例
- for (String unknowHandlerName : unknowHandlerNames) {
- UnknownHandler uh = container.getInstance(UnknownHandler.class, unknowHandlerName);
- unknownHandlers.add(uh);
- }
- }
- }
- }
- }
- // 這裏省略了許多其他的代碼
- }
在這裏,我們看到了通過容器(Container)對象獲取對象實例的兩種方法:getInstance和getInstanceNames。其中,前者用於獲取接受容器託管的具體對象實例。後者則被用於對於一個接口的多個不同實現類之間的實例獲取的管理。我們在這裏需要注意的是,在代碼示例中的build方法調用的前提是setContainer方法對於容器(Container)對象的正確初始化。
有關容器(Container)的另外一種操作:依賴注入,我們則通過XWork框架中的核心類ActionSupport的源代碼來進行解釋說明,如代碼清單5-4所示:
- public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable {
- // 這裏省略了許多其他的代碼
- private TextProvider getTextProvider() {
- if (textProvider == null) {
- TextProviderFactory tpf = new TextProviderFactory();
- if (container != null) {
- container.inject(tpf);
- }
- textProvider = tpf.createInstance(getClass(), this);
- }
- return textProvider;
- }
- @Inject
- public void setContainer(Container container) {
- this.container = container;
- }
- // 這裏省略了許多其他的代碼
- }
在上面的代碼中,我們看到兩個主要的方法:getTextProvider和setContainer。從邏輯上講,很明顯getTextProvider將以setContainer的存在爲基礎。setContainer實際上就是框架幫助我們獲取全局的容器實例的具體方法。值得我們注意的是@Inject這個Annotation的使用,使得setContainer方法將在ActionSupport初始化時被注入了全局的Container對象。而getTextProvider則在運行期被調用,此時全局的容器(Container)對象中的接口函數就可以被隨意調用,並完成依賴注入操作。具體來說,就是代碼中的container.inject(tpf)操作。
綜合上述的操作容器(Container)進行的兩類對象操作:獲取受到容器(Container)託管的對象和對象的依賴注入操作,我們可以從中得出使用容器(Container)進行對象操作的幾個要點:
- 通過操作容器進行對象操作的基本前提是當前的操作主體能夠獲得全局的容器實例。因而,全局的容器實例的獲取,在操作主體的初始化過程中完成。
- 通過操作容器進行的對象操作都是運行期(Runtime)操作。
- 通過操作容器所獲取的對象實例,都是那些受到容器託管的對象實例。
- 通過操作容器進行的依賴注入操作,可以針對任意對象進行,該操作可以建立起任意對象和容器託管對象之間的聯繫。
5.2.3.2 通過Annotation獲取容器對象實例
在展開本節的話題之前,我們首先來回顧一下上一節中我們所得出的一個重要結論:
downpour 寫道
結論通過操作容器(Container)進行對象操作的基本前提是當前的操作主體能夠獲得全局的容器實例。因而,全局的容器實例的獲取,在操作主體的初始化過程中完成。
這個重要結論在之前我們對容器進行操作的示例代碼中也能夠得到證實,那就是以下這樣一段公共代碼,如代碼清單5-5所示:
- @Inject
- public void setContainer(Container container) {
- this.container = container;
- }
我們對這段公共代碼實際含義的解讀是:在當前的對象操作主體進行初始化時,這個方法會被調用,而全局的容器(Container)對象則會被初始化到當前的對象操作主體之中。然而,這個方法並不是對象構造函數的一部分,那麼這個方法又是如何被包含在對象的初始化過程中去的呢?在這裏,引發這一系列神祕操作的,就是加在方法之上的這個Annotation:@Inject。
在上一節的分析中,我們得知@Inject是建立起任意對象實例與容器託管對象之間橋樑的唯一途徑。因此,當我們需要在一個自定義對象(非容器託管)中獲得容器託管對象的實例時,我們就可以藉助@Inject這個Annotation來實現。下面的例子就展示了這樣一個過程,如代碼清單5-6所示:
- public class ObjectProviderTest {
- private ObjectFactory objectFactory;
- @Inject
- public void setObjectFactory(ObjectFactory objectFactory) {
- this.objectFactory = objectFactory;
- }
- }
在這個例子中,我們使用的對象是一個自定義的對象ObjectProviderTest,然而我們卻需要在這個對象中獲得容器託管的對象(在這裏,ObjectFactory是受到XWork容器託管的框架內置對象)的實例。整個過程則通過@Inject的注入來完成。
在本節中,讀者應始終沿着XWork容器進行對象依賴注入的操作步驟進行過程的解讀。讀到這裏,或許讀者已經迫不及待地想要弄清楚容器(Container)內部操作的實現細節了。在接下來的章節中,我們就將揭開這個神祕過程的種種細節。