從2004年開始,有幾個用於依賴注入的IoC容器得到了廣泛的應用(僅以Spring、Guice和PicoContainer為例)。曾幾何時,它們在DI的配置方式上仍然各自為政,這使開發人員很難在不同框架之間遷移。
這一問題直到2009年5月才出現轉機,DI社區的兩大巨頭Bob Lee(Guice)和Rod Johnson(SpringSource)宣佈要齊心協力,共同打造一組標準的接口註解1。他們緊接著提出了JSR-330(javax.inject
)規範請求,倡導Java SE的DI標準化。DI社區的各路諸侯紛紛響應,全部表示支持。
1 Bob Lee,「Announcing @javax.inject.Inject」(2009-05-08),www.theserverside.com/news/thread.tss?thread_id=54499。
Java EE中的DI標準化情況如何?
Java企業應用從JEE 6開始構建了自己的依賴注入體系(即CDI),由JSR-299(Java EE平台中的上下文及依賴注入)規範確定,你可在http://jcp.org/中搜索JSR-299瞭解其詳細信息。簡言之,JSR-299構建在JSR-330基礎之上,旨在為企業應用提供標準化的配置。2
2 JSR-299(Java Contexts and Dependency Injection)目前由Redhat的Gavin King(Hibernate的創建者)主導,因為它比較新,所以設計理念上解決了以前DI框架中的一些問題,而且也不是非得在Java EE容器上才能使用,在Servlet容器上也可以使用。其參考實現為weld,詳情請參見官網:http://www.seamframework.org/Weld。——譯者注
自從javax.inject
出現在Java中(Java SE 5、6和7都支持)以來,代碼中就可以使用標準的依賴注入了,也可以在不同的DI框架中進行遷移。比如,你原來在輕量級的Guice框架中運行的代碼,為了利用其豐富的特性,也可以遷移到Spring框架中去。
警告 實際上,代碼遷移並不容易。一旦你的代碼用到了僅由特定DI框架支持的特性,就不太可能擺脫這一框架了。儘管
javax.inject
包提供了常用DI功能的子集,但是你可能需要使用更高級的DI特性。正如你想像的那樣,對於哪些特性應該作為通用的標準也是眾說紛紜,很難統一。雖然現狀不盡如人意,但Java畢竟朝DI框架的標準化方向邁出了一步。
為了理解最新的DI框架如何應用新標準,我們需要對javax.inject
包進行一番研究。記住,javax.inject
包只是提供了一個接口和幾個註解類型,這些都會被遵循JSR-330標準的各種DI框架實現。也就是說,除非你在創建與JSR-330兼容的IoC容器(如果如此,向你致敬),通常不用自己實現它們。
我為什麼要知道這東西怎麼工作?
優秀的Java開發人員不能滿足於只作為類庫和框架的使用者,還要明白其內部的基本工作原理。在DI領域,不理解其原理可能會面臨各種難纏的問題,比如依賴項配置錯誤、依賴項詭異地超出作用域、依賴項在不該共享時被共享以及分步調試離奇宕機等。
javax.inject
的文檔對這個包的目的做出了精彩的解釋,所以我們全盤照搬過來了:
javax.inject
包 3這個包指明了獲取對象的一種方式,與傳統的構造方法、工廠模式和服務定位器模式(比如JNDI)等相比,這種方式的可重用性、可測試性和可維護性都得到了極大提升。這種方式稱為依賴注入,對於大多數非小型應用程序都很有幫助。
3 http://atinject.googlecode.com/svn/trunk/javadoc/javax/inject/package-summary.html 。
javax.inject
包裡包括一個Provider<T>
接口和5個註解類型(@Inject
、@Qualifier
、@Named
、@Scope
和@Singleton
),後續章節中會逐一對它們進行介紹。先從@Inject
開始。
3.2.1 @Inject註解
@Inject
註解可以出現在三種類成員之前,表示該成員需要注入依賴項。按運行時的處理順序,這三種成員類型是:
1. 構造器
2. 方法
3. 屬性
在構造器上使用@Inject
時,其參數在運行時由配置好的IoC容器提供。比如在下面的代碼中,運行時調用MurmurMessage
類的構造器時,IoC容器會注入其參數Header
和Content
對象。
@Inject public MurmurMessage(Header header, Content content)
{
this.header = header;
this.content = content;
}
規範中規定向構造器注入的參數數量為0或多個,所以在不含參數的構造器上使用@Inject
也是合法的。
警告 因為JRE無法決定構造器注入的優先級,所以規範中規定類中只能有一個構造器帶
@Inject
註解。
也可以用@Inject
註解方法,與構造器一樣,運行時可注入參數的數量也可以是0或多個。但使用參數注入的方法不能聲明為抽像方法,也不能聲明其自身的類型參數1。下面這段代碼在set方法前使用@Inject
,這是注入可選屬性的常用技術。
1 即不能使用Oracle網站上的Java教程(http://download.oracle.com/javase/tutorial/extra/generics/methods.html)中所講的泛型方法技巧。
@Inject public void setContent(Content content)
{
this.content = content;
}
向方法中注入參數的技術對於服務類方法來說非常有用,其所需的資源可以作為參數注入,比如向查詢數據的服務方法中注入數據訪問對像(DAO)。
提示 向構造器中注入的通常是類中必需的依賴項,而對於非必需的依賴項,通常是在set方法上注入。比如已經給出了默認值的屬性就是非必需的依賴項。這一最佳實踐已經成了慣例。
也可以直接在屬性上注入(只要它們不是final
),雖然這樣做簡單直接,但你最好不要用。因為這樣可能會讓單元測試更加困難。直接注入的語法也非常簡單。
public class MurmurMessenger
{
@Inject private MurmurMessage murmurMessage;
...
}
你可以在Javadoc中瞭解更多關於@Inject
註解的詳細內容,可以在其中找到哪些類型的值可以注入以及如何處理依賴循環。
對於@Inject
,現在你應該不再感到陌生了。接下來就該瞭解如何限定(進一步標識)那些注入到你的代碼中的對象了。
3.2.2 @Qualifier註解
支持JSR-330規範的框架要用註解@Qualifier
限定(標識)要注入的對象。比如在IoC容器中有兩個類型相同的對象,當把它們注入到你的代碼中時,肯定要把它們區別開。圖3-1解釋了這一概念。
圖3-1 用@Qualifier註解區分同一類型MusicGenre的不同bean
如果你用過由框架實現的限定符,應該知道要創建一個@Qualifier
實現必須遵循如下規則。
必須標記為
@Qualifier
和@Retention(RUNTIME)
,以確保該限定註解在運行時一直有效。通常還應該加上
@Documented
註解,這樣該實現就能加到API的公共Javadoc中了。可以有屬性。
@Target
註解可以限定其使用範圍;比如將其使用範圍限制為屬性,而不是限定為屬性的默認值和方法中的參數。
為了讓你對上面的規則有直觀的感受,下面給出一個@Qualifier
實現。某音樂庫框架中的IoC容器提供了一個限定符@MusicGenre
,開發人員在創建MetalRecordAlbumns
類時可以使用該限定符,以確保注入了正確的Genre
。
@Documented
@Retention(RUNTIME)
@Qualifier
public @interface MusicGenre
{
Genre genre default Genre.TRANCE;
public enum GENRE { CLASSICAL, METAL, ROCK, TRANCE }
}
public class MetalRecordAlbumns
{
@Inject @MusicGenre(GENRE.METAL) Genre genre;
}
Java開發人員一般不需要自己創建@Qualifier
註解,但要對各種IoC容器如何實現限定有個基本的瞭解。
JSR-330規範中要求所有IoC容器都要提供一個默認的@Qualifier
註解:@Named
。
3.2.3 @Named註解
@Named
是一個特別的@Qualifier
註解,借助@Named
可以用名字標明要注入的對象。將@Named
和@Inject
一起使用,符合指定名稱並且類型正確的對象會被注入。
在下面這個例子中,注入了名稱為「murmur」和「broadcast」的兩個MurmurMessage
對象。
public class MurmurMessenger
{
@Inject @Named(「murmur」) private MurmurMessage murmurMessage;
@Inject @Named(「broadcast」) private MurmurMessage broadcastMessage;
...
}
儘管還有其他比較常用的限定註解,但最終只有@Named
被選作JSR-330的標準限定註解,所有DI框架都要實現。
發起規範的各方支持者還在另外一個問題上達成了一致——同意用標準化接口來確定注入對象的生命週期。
3.2.4 @Scope註解
@Scope
註解用於定義注入器(即IoC容器)對注入對象的重用方式。JSR-330規範中明確了如下幾種默認行為。
如果沒有聲明任何
@Scope
註解接口的實現,注入器應該創建注入對象並且僅使用該對像一次。如果聲明了
@Scope
註解接口的實現,那麼注入對象的生命週期由所聲明的@Scope
註解實現決定。如果注入對像在
@Scope
實現中要由多個線程使用,則需要保證注入對象的線程安全性。關於線程及線程安全的更多細節,請參閱第4章。如果某個類上聲明了多個
@Scope
註解,或聲明了不受支持的@Scope
註解,IoC容器應該拋出異常。
DI框架管理注入對象的生命週期時不會超出這些默認行為劃定的界限。有些IoC容器支持自己特有的@Scope
,尤其是在Web前端領域,至少在JSR-299被廣泛應用之前是這樣。因為大家公認的通用@Scope
實現只有@Singleton
一個,所以JSR-330規範中僅確定了它這麼一個標準的生命週期註解。
3.2.5 @Singleton註解
@Singleton
註解接口在DI框架中應用廣泛。在需要注入一個不會改變的對象時,就要用@Singleton
。
單例模式
單例設計模式是為了確保類僅被實例化一次,詳情參見由Erich Gamma, Richard Helm, Ralph Johnson, 和John Vlissides合著的《設計模式:可復用面向對像軟件的基礎》(Addison-Wesley Professional, 1994)第127頁。請謹慎使用單例模式,因為它有時候會變成反模式。
大多數DI框架都將@Singleton
作為注入對象的默認生命週期,無需顯式聲明。也就是說如果沒有明確指定注入對象的生命週期,框架就會認為你想注入一個單例對象。如果你想顯式聲明它為單例對象,可以用下面這種方式:
public MurmurMessage
{
@Inject @Singleton MessageHeader defaultHeader;
}
在這個例子中,我們假定defaultHeader
從來不會改變(切實有效的靜態數值),所以它可以作為單例對像注入。
最後,我們來討論一下當你覺得標準註解無法滿足你的需求時該怎麼辦。
3.2.6 接口Provider<T>
如果你想對由DI框架注入代碼中的對象擁有更多的控制權,可以要求DI框架將Provider<T>
接口實現注入對像1(T)
。控制對象的好處在於:
- 可以獲取該對象的多個實例。
- 可以延遲獲取該對像(延遲加載)。
- 可以打破循環依賴。
- 可以定義作用域,能在比整個被加載的應用小的作用域中查找對象。
1 原文如此,應為該類,後面還有幾處筆誤,請注意。——譯者注
該接口僅有一個T get
方法,這個方法會返回一個構造好的注入對像(T)
。例如,在MurmurMessage
需要依賴項Message
對像時,可以向其構造方法中注入對應的Provider<T>
接口實現的實例(Provider<Message>
)。根據限定條件的不同,得到的Message
對象也會不同。請看下面的代碼。
代碼清單3-6 使用接口Provider<T>
import com.google.inject.Inject;
import com.google.inject.Provider;
class MurmurMessage
{
@Inject MurmurMessage(Provider<Message> messageProvider)
{
Message msg1 = messageProvider.get;//得到一個Message
if (someGlobalCondition)
{
Message copyOfMsg1 = messageProvider.get;//1得到Message的復本
}
...
}
}
注意上面的代碼中是如何從Provider<Message>
中獲取第二個Message
對象的。如果直接注入Message
,就無法獲取另外一個Message
實例。在這個例子中,第二個注入對像僅在需要時才會加載1。
我們已經對新javax.inject
包背後的理論做了介紹,還給出了一些例子進行講解。現在就該挽起袖子,用成熟的DI框架Guice實際操練一把了。