程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 通過Guice進行依賴項注入

通過Guice進行依賴項注入

編輯:關於JAVA

Guice 是一個依賴項注入(DI)框架。幾年來我一直建議開發人員使用 DI,因為它提高 了可維護性、可測試性和靈活性。通過觀察工程師對 Guice 的反饋,我發現說服程序員去采 用一種新技術的最好方法是使這種技術簡單易用。Guice 讓 DI 變得很簡單,因此 Google 采用了這種方法。我希望本文能幫助您輕松學習 Guice。

Guice 2.0 beta

在寫這篇文章時,Guice 開發團隊正在奮力編寫 Guice 2.0,希望能在 2008 年底之前發 布。早期的 beta 發布在 Google 代碼下載站點(請參閱 參考資料)。這是一個好消息,因 為 Guice 團隊添加了一些新功能,使 Guice 代碼的使用和理解變得更簡單。beta 版沒有最 終版中的一些功能,但是 beta 很穩定,質量也很好。事實上,Google 在產品軟件中使用的 是 beta 版。我建議您使用 beta 版。這篇文章是專門為 Guice 2.0 編寫的,介紹了 Guice 的一些新功能,但沒有討論 1.0 中已經廢棄的一些功能。Guice 團隊向我保證:這裡討論的 功能在最終發行版和當前 beta 版中都是一樣的。

如果您已經了解了 DI,而且知道為什麼要借助一個框架來使用 DI,那麼您可以跳到 通 過 Guice 進行基本注入 小節。否則,請繼續閱讀,了解 DI 的好處。

DI 案例

我將以一個例子開始。假設我正在編寫一個超級英雄(superhero)應用程序,同時實現 一個名為 Frog Man 的 hero(英雄)。清單 1 是相關代碼和第一個測試(您一定明白編寫 單元測試的重要性,這裡就不多說了)。

清單 1. 一個基本 hero 及其測試

public class FrogMan {
  private FrogMobile vehicle = new FrogMobile();
  public FrogMan() {}
  // crime fighting logic goes here...
}
public class FrogManTest extends TestCase {
public void testFrogManFightsCrime() {
   FrogMan hero = new FrogMan();
   hero.fightCrime();
   //make some assertions...
  }
}

似乎一切正常,但在運行測試時出現了如清單 2 所示的異常:

清單 2. 依賴項出現問題

java.lang.RuntimeException: Refinery startup failure.
  at HeavyWaterRefinery.<init>(HeavyWaterRefinery.java:6)
  at FrogMobile.<init>(FrogMobile.java:5)
  at FrogMan.<init>(FrogMan.java:8)
  at FrogManTest.testFrogManFightsCrime(FrogManTest.java:10)

似乎 FrogMobile 構建了一個 HeavyWaterRefinery,假設我不能在測試中構建其中一個 依賴項。當然,我可以在生產環境中實現這一點,但是不能保證能在測試中構建第二個提煉 廠(refinery)。在現實生活中,您不可能提煉出氧化氘,但您可以依賴遠程服務器和強大 的數據庫。原理是一樣的:這些依賴項很難啟動,交互起來也很慢,這使得測試比平時更容 易失敗。

輸入 DI

為了避免這個問題,您可以創建一個接口(例如 Vehicle),使 FrogMan 類接受 Vehicle 作為一個構造函數參數,如清單 3 所示:

清單 3. 依賴接口並注入它們

public class FrogMan {
  private Vehicle vehicle;
  public FrogMan(Vehicle vehicle) {
   this.vehicle = vehicle;
  }
  // crime fighting logic goes here...
}

這種用法就是 DI 的本質 — 使類通過引用接口而不是構建接口(或使用靜態引用)來接 受它們的依賴項。清單 4 顯示了 DI 如何使測試變得更簡單:

清單 4. 測試可以使用 mock 而不是麻煩的依賴項

static class MockVehicle implements Vehicle {
  boolean didZoom;
  public String zoom() {
   this.didZoom = true;
   return "Mock Vehicle Zoomed.";
  }
}
public void testFrogManFightsCrime() {
  MockVehicle mockVehicle = new MockVehicle();
  FrogMan hero = new FrogMan(mockVehicle);
  hero.fightCrime();
  assertTrue(mockVehicle.didZoom);
  // other assertions
}

這個測試使用了一個手動編寫的 mock 對象來替換 FrogMobile。DI 不僅在測試中省去了 麻煩的 refinery 啟動過程,而且使測試不用了解 FrogMobile 具體細節。需要的僅是一個 Vehicle 接口。除了使測試變得更簡單之外,DI 還有助於提高代碼的總體模塊性和可維護性 。現在,如果想將 FrogMobile 切換為 FrogBarge,可以不修改 FrogMan。所有 FrogMan 都 依賴於 Vehicle 接口。

不過這裡有一個陷阱。如果您是第一次閱讀 DI,可能會想:“這下好了,現在所有 FrogMan 的調用方 都必須知道 FrogMobile(refinery、refinery 的依賴項,依此類推…… )”。但如果是這樣,DI 就永遠不會這麼流行。您可以不增加調用方的負擔,而是編寫一些 工廠 來管理對象及其依賴項的創建。

工廠是存放框架的地方。工廠需要大量冗長重復的代碼。工廠往往會讓程序員(和讀者) 很痛苦,他們甚至會嫌它麻煩而放棄編寫。Guice 和其他 DI 框架可作為 “超級工廠”,您 可以通過配置它們來構建對象。配置 DI 框架比自己編寫工廠容易得多。因此,程序員編寫 的代碼大部分是 DI 樣式的。測試越多代碼就越好,程序員以後也就越省事。

通過 Guice 進行基本注入

我希望在我的介紹之後,您會相信 DI 能為您的設計增加價值,而且使用框架會使工作更 輕松。現在讓我們從 @Inject 注釋和模塊開始深入討論 Guice。

告訴 Guice 給類添加 @Inject

FrogMan 與 Guice 上的 FrogMan 之間的唯一區別是 @Inject。清單 5 顯示了 FrogMan 帶有注釋的構造函數:

清單 5. FrogMan 已經加上 @Inject

@Inject
public FrogMan(Vehicle vehicle) {
  this.vehicle = vehicle;
}

一些工程師不喜歡給類添加 @Inject。他們更喜歡一個完全忽略 DI 框架的類。這種說法 有一定道理,但是我不大贊同。依賴項的注入會使注釋的作用更加明顯。@Inject 標記只在 您要求 Guice 構建類時才有意義。如果不要求 Guice 構建 FrogMan,這個注釋對代碼行為 就沒有任何影響。這個注釋恰當地指出了 Guice 將參與構建類。但是,使用它需要源代碼級 別的訪問。如果這個注釋帶來不便,或者正在使用 Guice 創建無法控制其源代碼的對象,那 麼 Guice 就會用一個替代機制(請參閱本文後面的 provider 方法的其他用法 側邊欄)。

告訴 Guice 您需要哪個依賴項

Guice 知道您的 hero 需要一個 Vehicle 後,它需要知道提供什麼 Vehicle。清單 6 包 含一個 Module:一個特殊的類,用於告訴 Guice 各個接口對應的實現。

清單 6. HeroModule 將 Vehicle 綁定到 FrogMobile

public class HeroModule implements Module {
  public void configure(Binder binder) {
   binder.bind(Vehicle.class).to(FrogMobile.class);
  }
}

模塊就是一個具有某種單實例對象方法的接口。Guice 傳遞給模塊的 Binder 用於告訴 Guice 您想如何構造對象。綁定程序 API 形成一種 區域特定語言(請參閱 參考資料)。這 種小語言允許您編寫表達式代碼,比如 bind(X).to(Y).in(Z)。後面將提供更多有關綁定程 序作用的例子。每次調用 bind 都會創建一個綁定,Guice 將使用綁定集解析注入請求。

使用 Injector 啟動

然後,使用 Injector 類啟動 Guice。通常需要盡早在程序中創建注入器。這樣 Guice 能夠幫助您創建大部分對象。清單 7 包含一個以 Injector 開始的示例 main 程序:

清單 7 使用 Injector 啟動應用程序

public class Adventure {
  public static void main(String[] args){
   Injector injector = Guice.createInjector(new HeroModule());
   FrogMan hero = injector.getInstance(FrogMan.class);
   hero.fightCrime();
  }
}

為了獲取注入器,需要在 Guice 類上調用 createInjector。向 createInjector 傳遞一 個模塊列表,用於配置它本身(本例只有一個,但您可以添加一個配置 evildoer 的 VillainModule)。擁有注入器後,使用 getInstance 向它請求對象,傳遞您想返回的 .class(細心的讀者會注意到您不需要告訴 Guice 有關 FrogMan 的信息。如果您請求一個 具體類,而它有一個 @Inject 構造函數或公共非參數構造函數的話,Guice 就會創建這個類 ,而無需調用 bind)。

這是 Guice 構造對象的第一種方式:顯式詢問。但是,您不會希望在啟動例程之外使用 這個操作。更好、更簡單的方式是讓 Guice 注入依賴項、依賴項的依賴項,依此類推(正如 諺語所說:“背起地球的海龜站在另一個海龜的背上”;請參閱 參考資料)。最初看來,這 似乎比較麻煩,但您很快就會習慣這種用法。例如,清單 8 顯示了一個注入了 FuelSource 的 FrogMobile:

清單 8. FrogMobile 接受一個 FuelSource

@Inject
public FrogMobile(FuelSource fuelSource){
  this.fuelSource = fuelSource;
}

這意味著,當您檢索 FrogMan 時,Guice 會構建一個 FuelSource、一個 FrogMobile, 最後是一個 FrogMan。即使應用程序與注入器只交互一次,也是如此。

當然,您並不總是有機會控制應用程序的 main 例程。例如,許多 Web 框架自動構建 “ 操作”、“模板” 或其他一些初始服務。總是可以找到一個地方插入 Guice,不管是使用該 框架的一個插件,還是使用一些自己手動編寫的代碼(例如,Guice 項目發布了一個 Struts 2 插件,它允許 Guice 配置您的 Strut 操作;請參閱 參考資料)。

其他注入形式

到目前為止,我展示了 @Inject 應用於構造函數的用法。當 Guice 找到注釋時,它會挑 選構造函數參數,並試圖為每個參數找到一個配置綁定。這稱為 構造函數注入。根據 Guice 的最佳實踐指南,構造函數注入是詢問依賴項的首選方式。但這不是唯一的方式。清單 9 顯 示了配置 FrogMan 類的另一種方式:

清單 9. 方法注入

public class FrogMan{
  private Vehicle vehicle;
  @Inject
  public void setVehicle(Vehicle vehicle) {
   this.vehicle = vehicle;
  }
//etc. ...

注意,我沒有使用注入的構造函數,而是改用一個帶有 @Inject 標記的方法。Guice 會 在構造好 hero 之後立即調用此方法。Spring 框架的忠實用戶可以將此方法視為 “setter 注入”。不過,Guice 只關心 @Inject;您可以任意命名這個方法,它可以帶有多個參數。 此方法可以是包保護的,也可以是私有方法。

如果您認為 Guice 訪問私有方法不是很好,可以參見清單 10,其中 FrogMan 使用了字 段注入:

清單 10. 字段注入

public class FrogMan {
  @Inject private Vehicle vehicle;
  public FrogMan(){}
//etc. ...

同樣,所有 Guice 都只關心 @Inject 注釋。字段注入查找注釋的所有字段並試圖注入相 應的依賴項。

哪種方法最好

三個 FrogMan 版本都展示了相同的行為:Guice 在構建時注入相應的 Vehicle。不過, 像 Guice 的作者一樣,我更喜歡構造函數注入。下面簡單分析這三種方式:

構造函數注入 很簡單。因為 Java 技術能保證構造函數調用,您不用擔心出現未初始化 的對象 — 不管是不是由 Guice 創建的。您還可以將字段標記為 final。

字段注入 會影響可測試性,特別是將字段標記為 private 時。這破壞了使用 DI 的主要 目的。應該盡量少使用字段注入。

方法注入 在您不控制類的實例化時很有用。如果您有一個需要某些依賴項的超類,也可 以使用方法注入(構造函數注入會使這種情況變得很復雜)。

選擇實現

現在,假設應用程序中有多個 Vehicle。一樣英勇的 Weasel Girl 無法駕馭 FrogMobile !同時,您不想在 WeaselCopter 上硬編碼依賴項。清單 11 顯示了 Weasel Girl 請求一種 更快的傳輸模式:

清單 11. 使用注釋請求某種特定的實現

@Inject
public WeaselGirl(@Fast Vehicle vehicle) {
  this.vehicle = vehicle;
}

在清單 12 中,HeroModule 使用綁定函數告訴 Guice WeaselCopter 是 “很快” 的:

清單 12. 告訴 Guice Module 中的相關注釋

public class HeroModule implements Module {
public void configure(Binder binder) {
   binder.bind(Vehicle.class).to(FrogMobile.class);
   binder.bind(Vehicle.class).annotatedWith(Fast.class).to (WeaselCopter.class);
  }
}

注意,我選擇了一個注釋,描述我想以抽象形式描述的工具種類(@Fast),而不是與實 現太接近的注釋(@WeaselCopter)。如果您使用的注釋將想要的實現描述得太精確,就讓讀 者覺得創建一個隱式依賴項。如果使用 @WeaselCopter,而且 Weasel Girl 借用了 Wombat Rocket,就會對程序員閱讀和調試代碼造成混淆。

要創建 @Fast 注釋,需要復制清單 13 中的模板:

清單 13. 復制粘貼這段代碼以創建一個綁定注釋

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface Fast {}

如果您編寫了大量 BindingAnnotations,就會得到許多這樣的小文件,每個文件只是注 釋名稱不同。如果您覺得這很繁瑣,或者需要執行快速的原型設計,可以考慮 Guice 的內置 @Named 注釋,它接受一個字符串屬性。清單 14 展示了這種替代方法:

清單 14. 使用 @Named 代替自定義注釋

// in WeaselGirl
@Inject
public WeaselGirl(@Named("Fast") Vehicle vehicle) {
  //...
}
// in HeroModule
binder.bind(Vehicle.class)
  .annotatedWith(Names.named("Fast")).to(WeaselCopter.class);

這種方法是可行的,但由於名稱只在字符串內有效,所以這不能利用編譯時檢查和自動補 齊。總的來說,我更願意自己編寫注釋。

如果您根本不想使用注釋,怎麼辦?即使添加 @Fast 或 @Named("Fast") 都會使類在某 種程度上影響配置本身。如果想知道如何解決這個問題,請接著閱讀。

provider 方法

如果每次探險都派遣 Frog Man,您可能會厭煩。您喜歡在每個場景中出現的 hero 是隨 機的。但是,Guice 的默認綁定程序 API 不允許出現 “每次調用時將 Hero 類綁定到一個 不同的實現” 這樣的調用。不過,您可以 告訴 Guice 使用一種特殊的方法來創建每個新的 Hero。清單 15 顯示了將一個新方法添加到 HeroModule 中,並用特殊的 @Provides 注釋進 行注釋:

清單 15. 使用 provider 編寫自定義創建邏輯

@Provides
private Hero provideHero(FrogMan frogMan, WeaselGirl weaselGirl) {
  if (Math.random() > .5) {
   return frogMan;
  }
  return weaselGirl;
}

Guice 會自動發現具有 @Provides 注釋的 Module 中的所有方法。根據 Hero 的返回類 型,在您請求某個 hero 時,Guice 會進行計算,它應該調用 provider 方法來提供 hero。 您可以為 provider 方法添加邏輯以構建對象並在緩存中查詢它,或者通過其他方式獲得它 。provider 方法是將其他庫集成到 Guice 模塊中的很好方式。它們也是從 Guice 2.0 開始 提供的新方法(Guice 1.0 中只編寫自定義 provider 類,這比較乏味,而且更加繁瑣。如 果您已經決定使用 Guice 1.0,用戶指南中有這種舊方法的文檔,而且在本文隨附的 示例代 碼 中,您可以找到一個自定義 provider)。

在清單 15 中,Guice 自動使用正確的參數注入 provider 方法。這意味著 Guice 將從 它的綁定列表中找到 WeaselGirl 和 FrogMan,您無需在 provider 方法中手動構建它們。 這演示了 “海龜背地球” 原則(海龜背地球,哪海龜下面是什麼呢?是由另一只海龜背著 ,如此反復)。您依靠 Guice 來提供依賴項,即使是配置 Guice 模塊本身。

請求一個 Provider 而不是一個依賴項

假設一個故事(Saga)中要有多個 hero。如果要求 Guice 注入一個 Hero,只會得到一 個 hero。但如果您請求一個 “hero provider”,就可以根據需要創建任意多的 hero,如 清單 17 所示:

清單 17. 注入 provider 來控制實例化

public class Saga {
  private final Provider<Hero> heroProvider;
  @Inject
  public Saga(Provider<Hero> heroProvider) {
   this.heroProvider = heroProvider;
  }
  public void start() throws IOException {
   for (int i = 0; i < 3; i++) {
    Hero hero = heroProvider.get();
    hero.fightCrime();
   }
  }
}

提供者也可以推遲英雄的出場時間,直到傳奇真正開始。如果英雄依賴於時間敏感或上下 文敏感的數據,這就會很方便。

Provider 接口有一個方法:get<T>。要訪問提供的對象,調用這個方法即可。每 次有沒有獲取新對象以及對象如何配置取決於 Guice 是如何配置的(參閱下面的 作用域 部 分,了解單實例對象和其他長生命周期對象的詳細信息)。在本例中,Guice 使用 @Provides 方法,因為它是構建新 Hero 的注冊方式。這意味著該傳奇應該由任意三位英雄 混合而成。

不要把提供者與 provider 方法弄混淆了(在 Guice 1.0,這兩者更難區分開來)。盡管 該 Saga 是從自定義 @Provides 方法中獲得它的英雄,但您可以請求任意 Guice 實例化依 賴項的一個 Provider。如果需要,可以根據清單 18 重新編寫 FrogMan 的構造函數:

清單 18. 請求 Provider 而不是依賴項

@Inject
public FrogMan(Provider<Vehicle> vehicleProvider) {
  this.vehicle = vehicleProvider.get();
}

(注意您完全不用更改這個模塊代碼)。重新編寫沒有任何作用;只是說明您總是可以請 求 Provider,而不用直接請求依賴項。

作用域

默認情況下,Guice 為每個請求的依賴項創建一個新實例。如果對象是輕量級的,這個策 略可以很好地工作。但是,如果有一個創建開銷很大的依賴項,就可能需要在幾台客戶機之 間共享實例。在清單 19 中,HeroModule 將 HeavyWaterRefinery 作為一個單實例對象綁定 :

清單 19. 將 HeavyWaterRefinery 綁定為一個單實例對象

public class HeroModule implements Module {
  public void configure(Binder binder) {
   //...
   binder.bind(FuelSource.class)
    .to(HeavyWaterRefinery.class).in(Scopes.SINGLETON);
  }
}

這意味著 Guice 會一直保持 “提煉廠” 可用,只要另一個實例需要燃料源,Guice 就 會注入相同 的 “提煉廠”。這避免了在應用程序中啟動多個 “提煉廠”。

在選擇作用域時,Guice 提供了一個選項。可以使用綁定程序配置它們,或者直接注釋依 賴項,如清單 20 所示:

清單 20. 改用注釋選擇作用域

@Singleton
public class HeavyWaterRefinery implements FuelSource {...}

Guice 提供了超出范圍的 Singleton 作用域,但它允許您定義自己的作用域(如果您願 意)。例如,Guice servlet 包提供了兩個其他作用域:Request 和 Session,它們為 servlet 請求和 servlet 會話提供類的一個獨特實例。

常量綁定和模塊配置

HeavyWaterRefinery 需要一個許可密鑰才能啟動。Guice 可以綁定常量值和新實例。請 查看清單 21:

清單 21. 在模塊中綁定常量值

public class HeavyWaterRefinery implements FuelSource {
  @Inject
  public HeavyWaterRefinery(@Named("LicenseKey") String key) {...}
}
// in HeroModule:
binder.bind(String.class)
  .annotatedWith(Names.named("LicenseKey")).toInstance("QWERTY");

這裡有必要使用綁定注釋,否則 Guice 將不能區分不同的 String。

注意,盡管前面不推薦使用,我還是選擇使用 @Named 注釋。因為我想顯示清單 22 所示 的代碼:

清單 22. 使用屬性文件配置模塊

//In HeroModule:
private void loadProperties(Binder binder) {
  InputStream stream =
   HeroModule.class.getResourceAsStream("/app.properties");
  Properties appProperties = new Properties();
  try {
   appProperties.load(stream);
   Names.bindProperties(binder, appProperties);
  } catch (IOException e) {
   // This is the preferred way to tell Guice something went wrong
   binder.addError(e);
  }
}
//In the file app.properties:
LicenseKey=QWERTY1234

這段代碼使用 Guice Names.bindProperties 實用函數,通過恰當的 @Named 注釋將 app.properties 文件中的每個屬性與一個常量綁定。這本身就很好,而且還顯示了您可以使 模塊代碼更復雜。如果喜歡,可以從數據庫或 XML 文件加載綁定信息。模塊是純 Java 代碼 ,這提供了很大的 靈活性。

結束語

Guice 主要概念小結:

使用 @Inject 請求依賴項。

將依賴項與 Module 中的實現綁定。

使用 Injector 引導應用程序。

使用 @Provides 方法增加靈活性。

需要了解的 Guice 知識還很多,但您應該先掌握這篇文章中討論的內容。我建議下載它 ,以及本文的 示例代碼。當然,您也可以創建自己的示例應用程序,這就更好了。通過示例 深入了解概念但又不用考慮生產代碼是很有意思的。如果要了解更多 Guice 高級功能(比如 面向方面編程支持),建議您訪問 參考資料 中的一些鏈接。

說到生產代碼,DI 的一個缺點是它可能感染病毒。注入一個類後,它會導致注入下一個 類,依此類推。不過這很好,因為 DI 使代碼更好。另一方面,這需要大量重構現有代碼。 為了使工作易於管理,可以將 Guice Injector 存儲在某處並直接調用它。應該將這當作一 根臨時需要的拐杖,但最後一定可以擺脫它。

Guice 2.0 即將推出。有一些功能我還沒有討論,它可以使模塊的配置更簡單,而且能支 持更大、更復雜的配置方案。可以訪問 參考資料 中的鏈接了解即將面世的功能。

我希望您會考慮將 Guice 添加到工具包中。根據我的經驗,DI 對於實現靈活的可測試代 碼庫特別有用。Guice 使 DI 變得簡單而有趣。還有什麼比容易編寫的、靈活的、可測試的 代碼更好呢?

來源:http://www.ibm.com/developerworks/cn/java/j-guice.html

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved