讀古今文學網 > Java程序員修煉之道 > 3.2 Java中標準化的DI >

3.2 Java中標準化的DI

從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容器會注入其參數HeaderContent對象。

@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實際操練一把了。