讀古今文學網 > Java程序員修煉之道 > 3.3 Java中的DI參考實現:Guice 3 >

3.3 Java中的DI參考實現:Guice 3

Guice(讀作「Juice」)是由Bob Lee在大約2006年發起的開源項目,項目站點地址為http://code.google.com/p/google-guice/。你可以在網站上看到該項目的設計初衷、相關文檔並下載運行本節示例代碼所需的二進制JAR文件。

在Guice這樣的DI框架裡,你可以配置依賴項、綁定依賴項,並在使用@Inject註解(和它在JSR-330中的朋友們)注入依賴項時聲明它們的作用域。

Guice 3是JSR-330規範的完整參考實現,本節所有內容都是基於Guice 3的。雖然Guice不僅僅是一個簡單的DI框架,但我們關注的主要還是它的DI能力和示例代碼,其中你可以使用JSR-330標準註解和Guice一起編寫DI代碼。

3.3.1 Guice新手指南

現在你已經瞭解JSR-330的各種註解了。可以借助Guice構建一個Java注入對像集合(包括它們的依賴項)。用Guice的話說,為了讓注入器創建對像關係圖,需要創建聲明各種綁定關係的模塊,其中綁定是用來明確要注入的具體實現類的。暈了沒?不用擔心,看到代碼你就明白了,這些概念實際上非常簡單。

提示 對像關係圖、綁定、模塊和注入器都是Guice裡的常用語,如果你想用好Guice,最好盡快搞清楚它們是什麼意思。

本節我們還是用HollywoodService的例子。一開始先創建一個擁有各種綁定關係的配置類(模塊)。實際上這是Guice框架要管理的那些依賴項的外部配置。

在哪裡下載Guice?

最新版的Guice 3可以從http://code.google.com/p/google-guice/downloads/list上下載,對應的文檔可以在http://code.google.com/p/google-guice/wiki/Motivation?tm=6上找到。 要得到完整的IoC容器和DI支持,還需要下載Guice zip文件並將它解壓到你選定的位置。為了在Java代碼中使用Guice,要確保這些JAR文件包含在CLASSPATH中。

對於本書中後續代碼示例而言,構建Maven時也會自動下載Guice 3的JAR文件。

我們先來創建一個定義綁定關係的AgentFinderModule。這個AgentFinderModule類擴展了AbstractModule,綁定關係在重寫的configure方法中聲明。在本例中,當客戶類HollywoodService要求@Inject一個AgentFinder的時候,就會綁定WebServiceAgentFinder類作為注入對象。我們在這裡遵循構造方法注入的慣例,具體實現請見代碼:

代碼清單3-7 HollywoodService——用 Guice 注入AgentFinder

import com.google.inject.AbstractModule;
public class AgentFinderModule extends AbstractModule //擴展AbstractModule
{
  @Override //重寫configure方法
  protected void configure
    {
    bind(AgentFinder.class).
                to(WebServiceAgentFinder.class); //1綁定要注入的實現類
    }
}

public class HollywoodServiceGuice
{
  private AgentFinder finder = null;

  @Inject
  public HollywoodServiceGuice(AgentFinder finder)
    {
    this.finder = finder;
    }
  public List<Agent> getFriendlyAgents
    {
    List<Agent> agents = finder.findAllAgents;
    List<Agent> friendlyAgents = filterAgents(agents, \"Java Developers\");
    return friendlyAgents;
    }
    public List<Agent> filterAgents(List<Agent> agents, String agentType)
    {
       ...//同代碼清單3-2
     }
}
  

綁定關係的確立在調用Guice的bind方法時發生,把要綁定的類(AgentFinder)傳給它,然後調用to方法指明要注入到哪個實現類1。

現在已經在模塊中聲明了綁定關係,可以讓注入器構建對像關係圖了。接下來我們要看看在獨立Java程序和Web應用程序這兩種情況下分別要如何實現。

1. 構建Guice對像關係圖——獨立Java程序

在標準的Java程序中,可以通過public static void main(String args)方法構建對像關係圖。代碼清單3-8如下所示。

代碼清單3-8 HollywoodServiceClient——用Guice構建對像關係圖

import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.List;

public class HollywoodServiceClient
{
  public static void main(String args)
    {
    Injector injector =
        Guice.createInjector(new AgentFinderModule);
    HollywoodServiceGuice hollyWoodService =
        injector.getInstance(HollywoodServiceGuice.class);
    List<Agent> agents = hollywoodService.getFriendlyAgents;
       ...
    }
  }
  

對於Web應用,情況稍有不同。

2. 構建Guice對像關係圖——Web應用程序

在Web應用程序中,需要把guice-servlet.jar加到Web應用的類庫中,然後在web.xml中添加下面的配置項:

<filter>
  <filter-name>guiceFilter</filter-name>
  <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>guiceFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
  

然後是標準動作,擴展ServletContextListener以便使用Guice的ServletModule(與代碼清單3-7中的AbstractModule類似)。

public class MyGuiceServletConfig extends GuiceServletContextListener {
  @Override
  protected Injector getInjector {
    return Guice.createInjector(new ServletModule);
  }
}
  

最後一步,把下面這些配置加到web.xml文件中,以便servlet容器在部署應用時觸發該類。

<listener>
   <listener-class>com.java7developer.MyGuiceServletConfig</listener-class>
</listener>
  

經由注入器創建HollywoodServiceGuice,你得到了一個配置完備的類,馬上就可以調用其中的getFriendlyAgents方法。

非常簡單,對不對?沒錯,但這種把WebServiceAgentFinder綁定到AgentFinder上的情況很簡單,而你所需要的綁定方式可能要比這個複雜,所以我們還需要瞭解一下如何定義更複雜的綁定方式。

3.3.2 水手繩結:Guice的各種綁定

Guice提供了多種綁定方式,官方文檔列出的綁定類型如下所示:

  • 鏈接綁定
  • 綁定註解
  • 實例綁定
  • @Provides方法
  • Provider綁定
  • 無目標綁定
  • 內置綁定
  • 即時綁定

我們無意在此重複Guice的官方文檔,因此只挑最常用的講解一下,其中包括鏈接綁定、綁定註解和@Provides方法及Provider<T>綁定。

1. 鏈接綁定

鏈接綁定是最簡單的綁定方式,代碼清單3-6中配置AgentFinderModule時用的就是這種方式。這種綁定方式只是告訴注入器運行時應該注入實現類或擴展類(是的,可以直接注入子類)。

@Override
protected void configure
{
  bind(AgentFinder.class).to(WebServiceAgentFinder.class);
}
  

你已經見過這種綁定代碼了,讓我們看看另外一種最常用的綁定方式:綁定註解。

2. 綁定註解

綁定註解是指將注入類的類型和額外的標識符組合起來,以標識恰當的注入對象。你可以自定義綁定註解(參見Guice在線文檔),不過我們還是先介紹一下JSR-330標準註解@Named的用法。

在下面的例子中,你所熟悉的@Inject依然會出現,但它這次會與@Named註解聯袂登場,以注入特定名稱的AgentFinder。為了配置@Named綁定,需要在AgentModule中調用annotatedWith方法,代碼清單3-9如下所示:

代碼清單3-9 HollywoodService——使用@Named

public class HollywoodService
{
  private AgentFinder finder = null;
  @Inject
  public HollywoodService(@Named(「primary」) AgentFinder finder) //使用@Named註解
   {
    this.finder = finder;
   }
}
public class AgentFinderModule extends AbstractModule
{
  @Override
  protected void configure
   {
    bind(AgentFinder.class)
         .annotatedWith(Names.named(「primary」)) //與命名參數綁定在一起
         .to(WebServiceAgentFinder.class);
   }
}
  

現在你已經知道如何配置命名依賴項了,可以繼續學習另一種綁定方式了。下面就用@Provides註解和Provider<T>接口綁定完全由你自己定制的依賴項吧。

3. @Provides和Provider:提供完全定制的對象

你可以用@Provides註解,或者在configure方法中綁定,以返回一個完全由你自己定制的對象。比如說,你可能想注入一個非常特別的SpreadsheetAgentFinder(微軟的Excel電子錶格實現)。

注入器會查看所有標記了@Provides註解方法的返回類型,以決定要注入哪個對象。比如在下面的代碼中,HollywoodService會用由provideAgentFinder方法提供的並帶有@Provides註解的AgentFinder

代碼清單3-10 AgentFinderModule——使用@Provides

public class AgentFinderModule extends AbstractModule
{
  @Override
  protected void configure { }

  @Provides 
  AgentFinder provideAgentFinder //返回注入器需要的類型
   {
      /**創建SpreadsheetAgentFinder的實例並設定具體值*/
      SpreadsheetAgentFinder finder = new SpreadsheetAgentFinder; 
      finder.setType(「Excel 97」);
      finder.setPath(「C:tempagents.xls」) 
      return finder;
   }
}
 

@Provides方法會變得越來越多,為了不把模塊類撐爆,你可能要把它們拆分出去建立自己的類。因此,Guice支持JSR-330的Provider<T>接口;如果你還記得第3.2.6節的內容,應該不會忘記T get方法。當在AgentFinderModule類中通過toProvider方法綁定到AgentFinderProvider時,就會調用這個方法。代碼如下所示:

代碼清單3-11 AgentFinderModule——使用Provider 接口

public class AgentFinderProvider implements Provider<AgentFinder>
{
  @Override
  public AgentFinder get//使用T get 方法
  {
      SpreadsheetAgentFinder finder = new SpreadsheetAgentFinder;
      finder.setType(「Excel 97」);
      finder.setPath(「C:tempagents.xls」)
      return finder;
   }
}
public class AgentFinderModule extends AbstractModule
{
  @Override
  protected void configure
  {
    bind(AgentFinder.class)
      .toProvider(AgentFinderProvider.class);//綁定provider
   }
}
  

這是最後一個關於綁定的例子。現在你應該能用Guice綁定你需要的依賴項了。但我們還沒討論依賴項的生命週期範圍。瞭解生命週期非常重要,因為如果對像生命週期設置錯誤的話,它們可能會存在更長時間並佔用更多的內存空間。

3.3.3 在Guice中限定注入對象的生命週期

Guice為注入對像提供了不同級別的生命週期。其中最短的是@RequestScope,然後是@SessionScope,還有就是JSR-330規範中定義的@Singleton,也就是應用級別的生命週期。

在代碼中有以下幾種方式應用依賴項的生命週期:

  • 在要注入的類中;
  • 作為綁定聲明的一部分(比如bind.to.in);
  • @Provides一起使用註解聲明。

上面的列表有點兒抽像,我們還是用限定依賴項生命週期的一小段代碼來說明上面這些方式究竟是什麼意思吧。

1. 限定注入項的生命週期

假設你希望程序的整個生命週期中只用一個SpreadsheetAgentFinder實例。為此需在類聲明中設置@Singleton,如下所示:

@Singleton
public class SpreadsheetAgentFinder
{
  ...
}
  

用這個方法還有個好處,可以提醒開發人員注入項對線程安全的要求。因為從理論上來說,SpreadsheetAgentFinder類可以多次注入,@Singleton範圍意味著要保證這個類的線程安全性(第4章會討論線程安全)。

如果你更喜歡在綁定依賴項時聲明其生命週期,你就可以這樣做。

2. 用BIND方法設置生命週期

有些開發人員可能喜歡把所有和注入對像相關的規則都放在一起。還記得代碼清單3-9中如何綁定「primary」AgentFinder嗎?在綁定時設置生命週期與此類似,只要在bind方法串後面再加上.in(<Scope>.class)就行了。

下面的代碼改進了代碼清單3-9,在bind方法串後面加上了in(Session.class),使得在會話(session)範圍中能夠獲取「primary」 AgentFinder對象。

public class AgentFinderModule extends AbstractModule
{
  @Override
  protected void configure
  {
    bind(AgentFinder.class)
      .annotatedWith(Names.named(「primary」))
      .to(WebServiceAgentFinder.class)
      .in(Session.class);
   }
}
  

還有最後一種設置注入對像生命週期的方法:與@Provides註解聯合設置。

3. 設置@Provides對象的生命週期

你可以在@Provides註解旁邊加上一個生命週期,以定義由該方法所提供對象的生命週期。比如在代碼清單3-9中,可以加上@Request註解,將最終提供的SpreadsheetAgentFinder實例限定在請求(request)範圍中。

@Provides @Request
AgentFinder provideAgentFinder
{
  SpreadsheetAgentFinder finder = new SpreadsheetAgentFinder;
  finder.setType(「Excel 97」);
  finder.setPath(「C:tempagents.xls」);
  return finder;
}
  

Guice也提供了針對Web應用的注入項生命週期(比如Servlet request範圍),當然你也可以根據需要實現自己的注入項生命週期。

現在你已經基本掌握了在Guice中用JSR-330註解滿足你的依賴注入需求了。其實Guice還提供許多非JSR-330特性,比如它還支持面向方面的編程(AOP),讓你可以實現安全性和日誌處理的橫切關注點。要瞭解這些內容,請參考Guice的在線文檔和代碼示例。

謹慎選擇對象的生命週期!

一個成熟的Java開發人員總是會認真考慮對象的生命週期。創建無狀態對像相對來說成本低廉,無需考慮其生命週期。JVM會在需要時創建和銷毀它們,毫無障礙。(第6章詳細討論了JVM和性能。)

另一方面,狀態對像總是需要設定生命週期!你應該認真考慮是否要讓一個對象的生命週期貫穿整個應用程序的運行期,或者僅存在於當前會話,還是只在當前請求中。接下來就要考慮對象的線程安全性了(第4章會進一步討論這個問題)。