前言
Google Guice 是一個輕量級的依賴注入框架,它支持Java 5或者更高版本的JDK,得利於Java 5中提供的泛型 (Generics) 和注釋 (Annotations) ,它可以使得代碼類型安全 (type-safe) 。那么何時使用在代碼中使用 Guice 進行注入呢?一般來說,如果在你的應用代碼中業務對象 (Business Objects) 之間的關系或者依賴需要手動維護的話,你就可以使用Guice 進行注入。
該文章中,首先我將通過一些例子來初步的認識一下 Guice 框架,然后我將介紹下 依賴注入框架的理論知識 以及在應用程序中使用依賴注入的好處,同樣我也會和大家探討一下 Guice 提供的用於簡化代碼的 API (包括Annotations) 。最后通過大量使用 Guice API的例子來使大家更好地理解這些API。
依賴注入(Dependency Injection)
由於Gucie 是一個依賴注入框架 (Dependency Injection Framework) ,因此我們首先要很清楚依賴注入 (Dependency Injection) 是什么概念。這些年來,依賴注入變得越來越流行,變得越來越重要,在很多典型的應用中它甚至變成了一個必需的機制,比如 J2EE 5.0, Spring, JBoss Seam就是使用依賴注入的很好的例子。現在我們來使用一個簡單的例子來說明使用依賴注入框架的必要性。
請看以下代碼:
interface Storage{ public void store(String uniqueId, Data data); public Data retrieve(String uniqueId); }
上面的接口 Storage 提供了存儲 (store) 和獲取 (retrieve) 數據的機制,由於數據可以存儲在數據庫中也可以存儲在一個文件中,因此上面接口 Storage 的實現可以如下。
class FileStorage implements Storage{ public void store(String uniqueId, Data data){ // Store the object in a file using Java Serialization mechanism. } public Data retrieve(String uniqueId){ // Code to retrieve the object. } }
實現類 FileStorage 可以將數據存儲到硬盤文件中,同樣也可以從硬盤文件中獲取存儲數據。接下來是 Storage 接口的另一種實現,它用於將數據存儲到數據庫中。
class DatabaseStorage implements Storage{ public void store(String uniqueId, Data data){ // Open a connection and store the data. } public Data retrieve(String uniqueId){ // Get the data from the Database. } }
現在,我們來看一個 Storage 應用客戶端的例子。下面的 StorageClient 代碼片段中,首先初始化一個 FileSorage,然后在轉向 DatabaseStorage 實現。
public class StorageClient { public static void main(String[] args) { // Making use of file storage. Storage storage = new FileStorage(); storage.store("123", new Data()); // Making use of the database. storage = new DatabaseStorage(); storage.store("456", new Data()); } }
仔細看下 StorageClient 模塊中的代碼,盡管接口 (Storage) 和實現類 (FileStorage/DatabaseStorage) 松耦合,但是 客戶端 (StorageClient) 模塊需要手動地去創建實現類的實例對象 (instance) ,同樣接口和實現類之間的關系 (Relationship) 是直接在客戶端代碼中寫死的,然而在大多數情況下,在代碼編譯的時候,客戶端應用程序已經知道需要綁定哪一種接口實現類,如果只綁定某一個具體的實現類,肯定比上面的代碼中同時實現兩個類 (某一個是沒必要的) 更有用。Google Guice 就是干這個工作的,它在應用程序的客戶端代碼中創建不同形式服務 (Services) 實例, 而且客戶端和服務之間的依賴是通過一些簡單的配置機制 (Configuration Mechanism) 自動注入的。
接下來我將提供一個簡單使用 Guice Framework 的例子。
一個簡單的 Guice 例子
在這個簡單的例子中,讓我們看一下 Guice 在維護不同對象之間的關系/依賴時如何簡化開發的。讓我們看一下下面的代碼片段,我們創建了一個 Add 接口,並且在里面定義了一個 add() 方法。
public interface Add { public int add(int a, int b); }
下面是接口 Add 的一個實現類
public class SimpleAdd implements Add{ public int add(int a, int b) { return a + b; } }
接着我們定義一個 Module 類,這個類用於使用 Guice API 在一個應用程序中創建 Bindings。Module 和 Bindings 理論方面的詳細介紹在后面章節。現在,你只需要明白通過 Binder 類,你可以將一些 Bindings 配置到某個 Module中。在 Guice 條目中,Binding 提供了一種方式將接口 (interface) 和實現類相關聯。
import com.google.inject.Binder; import com.google.inject.Module; public class AddModule implements Module{ public void configure(Binder binder) { binder.bind(Add.class).to(SimpleAdd.class); } }
在上面的代碼中,我們告訴 Guice 將 SimpleAdd 實現類綁定到 Add 接口上,也就是說在客戶端調用Add.add() 方法時,實際會去執行 SimpleAdd.add() 方法。下面給出了一個客戶端例子用戶使用 Add 接口。
import com.google.inject.Guice; import com.google.inject.Injector; public class AddClient { public static void main(String[] args) { Injector injector = Guice.createInjector(new AddModule()); Add add = injector.getInstance(Add.class); System.out.println(add.add(10, 54)); } }
更多關於 Injector, Guice 的理論知識將會在后面的章節介紹。injector.getInstance(Add.class) 將會創建並返回一個 SimpleAdd 類型的實例。實際上是通過 AddModule.configure() 方法來獲取具體的綁定信息的。
Guice API 探討
讓我們探討一下實現 Guice 依賴注入不同的 API。特別會涉及以下的 接口/實現類。
- Binder
- Injector
- Module
- Guice
1. Binder
Binder 接口主要是由與 Bindings 相關的信息組成的。一個 Binding 其實就是一個接口和其相應的實現類的映射關系。例如,回想一下上面的例子,我們創建了一個由接口 Add 指向 實現類 SimpleAdd 的映射關系。
從程序角度來說,可以通過以下代碼方式實現。注意的是無論是接口 (interface) 還是實現類 (implementation classes),都是通過 bind() 和 to()方法實現映射的。
binder.bind(Add.class).to(SimpleAdd.class)
同樣也可以將一個接口直接映射到一個具體的實例對象,代碼如下。
binder.bind(Add.class).to(new SimpleAdd())
第三種方式是將一個接口綁定到一個相應的 Provider 類。默認情況下,Guice 框架會創建並返回應用程序需要的實例對象。但是,如果需要定制化一個對象創建流程(Object Creation Process),該怎么辦? Providers 可以很簡單的實現這種定制化。 你只需要遵循傳統的工廠模式(Factory Pattern)創建對象的方式使用 Providers,例如下面的代碼。
binder.bind(Add.class).to(new AddProvider<Add>())
后面我將會通過一些例子講解如何創建 Provider 對象。不過現在,你只需要知道在 AddProvider 類中提供了一種工廠方法,它會返回具體的 Add 實現類的實例對象。后面我同樣會講解到如何將一個接口綁定到多個具體實現上。
2. Injector
Injectors 通常會在客戶端 (Clients) 使用,它只關心如何創建 (Creating)和維護 (Maintaining) 對象(生命周期)。Injectors 會去維護一組默認的 Bindings (Default Bindings),這里我們可以獲取創建和維護不同對象間關系的配置信息 (Configuration information)。以下代碼將會返回 Add 的實現類對象。
Add addObject = injector.getInstance(Add.class)
你可以簡單地通過 Injector.getBindings() 方法獲取與 Injector 相關的 Bindings信息,getBindings() 方法會返回一個 Map。
Map<Key, Binding> allBindings = injector.getBindings()
這里需要注意的是每一個 Binding 通常有一個對應的 Key 對象,該對象是由 Guice 自動創建並維護的。如果你想要獲取於Injector相關的 Providers 的話,你可以通過以下方法獲取。
Provider<SomeType> provider = injector.getProvider(SomeType.class)
3. Module
Module 對象會去維護一組 Bindings。在一個應用中可以有多個 Module 。反過來 Injectors 會通過 Module 來獲取可能的 Bindings。Module 是通過一個包含需要被重寫 override 的 Module.configure() 方法的接口去管理 Bindings。 簡單地說,就是你要繼承一個叫做 AbstractModule的類,這個類實現了 Module 接口,並且重寫 configure() 方法, 代碼如下。
class MyModule extends AbstractModule{ public void configure(Binder binder){ // Code that binds information using the various // flavours of bind method. } }
4. Guice
客戶端 (Clients) 是通過 Guice 類直接和其他 Objects 進行交互的。Injector 和不同的 Modules 之間的聯系是通過 Guice 建立的。例如下面的代碼。
MyModule module = new MyModule(); Injector injector = Guice.createInjector(module);
這里需要注意的是 Guice.createInjector() ,該方法接受一個 Module 對象作為參數。 Module 類必需要重寫 configure() 方法, 該方法是用於傳遞一個 默認 Binder 對象, 該 Binder 對象為應用程序用於填充特定的 Bindings (to Classes, Objects and Providers)。 當客戶端調用 Injector 類的 getInstance() 方法創建一個實例的時候,Injector 會從 Binder 對象維護的各種 Bindings 中獲取原來的對象。
Guice 注釋 (Annotations)
Guice 提供了一些十分有用的 Annotations ,這些 Annotations 可以用來在應用程序中添加 元數據 (meta-data)。 這一章節我將要講以下幾個注釋。
- Implemented By
- Inject
- Provided By
- Singleton
1. Implemented By
該 Annotation 用於指向接口的實現類。例如,如果 Add 接口有多個實現類,但是我們希望 SimpleAdd 是 Add 的默認實現類,於是我們可以像下面一樣處理。
@ImplementedBy(SimpleAdd.class) interface Add{ public int add(int a, int b); }
2. Inject
我們可以使用 Inject Annotation 來直接將實例注入到客戶端的代碼中。該注釋可以用於某個類的構造方法上,代碼如下。
class Client{ @Inject public Client(MyService service){ } }
上面的代碼,我們是基於構造方法層次 (Constrcctor-level)的 注入,並且假設 MyService 接口的具體實現已經在應用程序的 Module 中定義映射好了。同樣你也可以在方法層次 (Method-level) 和 字段層次 (Field-level) 使用注釋。
3. Provided By
假設我們想要為一些接口定制化對象創建的流程 (Object creation process),那么我們需要依賴 Guice Provider 機制, 對於接口 Add 來說,我們需要使用 AddProvider 來創建並返回 SimpleAdd 對象。在這個案例中,我們可以直接在接口聲明處使用 ProvidedBy 注釋來指定該接口的 Provider 類型, 代碼如下。
@ProvidedBy(AddProvider.class) public interface Add{ }
4. Singleton
默認情況下,客戶端可以多次使用 Injector.getInstance() 來調用對象,每一個都會返回一個新創建的對象。如果我們想要使用單例模式(Singleton Pattern)來獲取對象,即 One Instance in the application,你可以在實現類上使用 Singleton 注釋去標記。
@Singleton public class MyConnection{ public void connect(){ } public void disconnect(){ } }
例子 (Samples)
這一章節將會提過更多的例子幫助你理解和使用 Guice API ,我將會更加詳細的解析。
1. 簡單的例子
在這個簡單的例子中我們沒有使用接口編程,即將接口和實現分離。我們只有一個實現類 Player 和一個依賴它的客戶端 PlayerTest, 這里 Guice 沒有做什么,只是提供了一個映射。
首先來看一下 Player 類。
public class Player { public String name; public Player(){ } public String toString(){ return name; } }
下面是客戶端代碼的例子,用於使用 Player 類。這里需要注意的是我們沒有在 Guice.createInjector() 方法里面傳遞 Module ,因為我們不需要在程序代碼中綁定對象。
import com.google.inject.Guice; import com.google.inject.Injector; public class PlayerTest { public static void main(String[] args) { Injector injector = Guice.createInjector(); Player player = injector.getInstance(Player.class); player.name = "David Boon"; System.out.println(player); } }
2. 處理多個依賴 (Multiple Dependencies)
這一小節里面,我們將探討如何是用 @Inject 注釋來處理多個依賴。 比方說有一個對象直接依賴其它兩個或者多個對象。這里我們創建一個簡單的 Case ,一個人有一台筆記和一個手機。
首先我們給出 Mobile 類和 Laptop 類。
public class Laptop { private String model; private String price; public Laptop(){ this.model = "HP 323233232"; this.price = "$545034"; } public String toString(){ return "[Laptop: " + model + "," + price + "]"; } }
public class Mobile { private String number; public Mobile(){ this.number = "988438434"; } public String toString(){ return "[Mobile: " + number + "]"; } }
接下來我們將會在 Person 類中使用 @Inject 注釋來直接引用 Laptop 和 Mobile 對象。注意我們這兒使用的是構造方法層次上的注入。
import com.google.inject.Inject; public class Person { private Mobile mobile; private Laptop laptop; @Inject public Person(Mobile mobile, Laptop laptop){ this.mobile = mobile; this.laptop = laptop; } public void diplayInfo(){ System.out.println("Mobile:" + mobile); System.out.println("Laptop:" + laptop); } }
最后是客戶端的代碼,這段代碼用於使用這個例子。由於我們沒有使用到 Bindings, 我們沒有在 Guice.createInject() 方法中傳遞 Module 對象。
import com.google.inject.Guice; import com.google.inject.Injector; public class MultipleDependencyTest { public static void main(String[] args) { Injector injector = Guice.createInjector(); Person person = injector.getInstance(Person.class); person.diplayInfo(); } }
上面程序的運行結果如下:
Mobile:[Mobile: 988438434] Laptop:[Laptop: HP 323233232,$545034]
3. 使用 Binding 注釋
在 Guice 中,一個類型不能綁定多個實現,如下,代碼會拋 Runtime Error.
binderObject.bind(SomeType.class).to(ImplemenationOne.class); binderObject.bind(SomeType.class).to(ImplemenationTwo.class);
由於 Guice 並不知道客戶端究竟要綁定哪一個實現類,因此拋出了異常。但是在類似 Java 的語言中,一個類可以實現多個接口,基於這個思想,Guice 提供了一種依賴 Binding 注釋的方式來實現一個類型綁定多個實現。例如,接口 Player 定義如下,
public interface Player { public void bat(); public void bowl(); }
接着我們提供了 Player 的兩種實現類, GoodPlayer 和 BadPlayer。
public class GoodPlayer implements Player{ public void bat() { System.out.println("I can hit any ball"); } public void bowl() { System.out.println("I can also bowl"); } }
public class BadPlayer implements Player{ public void bat() { System.out.println("I think i can face the ball"); } public void bowl() { System.out.println("I dont know bowling"); } }
現在我們開始介紹 Guice ,對於接口 Player 而言,有兩個實現類 GoodPlayer 和 BadPlayer。無論如何,最終客戶端只會使用其中一個具體的實現類,無論它使用GoodPlayer 實現類還是 BadPlayer 實現類,通過一些注釋機制 (Annotaion mechanisms) 我們可以指示 Guice 使用不同的實現。代碼實現如下。
1 import com.google.inject.*; 2 3 public class PlayerModule implements Module{ 4 5 public void configure(Binder binder) { 6 7 binder.bind(Player.class).annotatedWith(Good.class).to( 8 GoodPlayer.class); 9 binder.bind(Player.class).annotatedWith(Bad.class).to( 10 BadPlayer.class); 11 } 12 }
注意第7行和第9行代碼,我們分別使用了.annotatedWith(Good.class) 和 .annotatedWith(Bad.class), 這兩處代碼指明了如果使用Good注釋,那么就綁定GoodPlayer實現類,如果使用了Bad注釋,那么就綁定BadPlayer實現類。
上面的代碼中我們使用了兩個自定義的 Annotation,Good 和 Bad。下面我們給出 Good annotation 和 Bad annotation 的代碼。
import java.lang.annotation.*; import com.google.inject.BindingAnnotation; @Retention(RetentionPolicy.RUNTIME) @BindingAnnotation @Target(ElementType. LOCAL_VARIABLE) public @interface Good {}
import java.lang.annotation.*; import com.google.inject.BindingAnnotation; @Retention(RetentionPolicy.RUNTIME) @BindingAnnotation @Target(ElementType. LOCAL_VARIABLE) public @interface Bad {}
接下來是上面程序的客戶端代碼。這里需要注意的是當在客戶端代碼中請求某一個接口的具體實現的時候,可以直接通過指定不同的 Annotation 來指定返回不同的實現類。
1 import com.google.inject.Guice; 2 import com.google.inject.Injector; 3 import com.google.inject.Module; 4 5 public class PlayerClient { 6 7 public static void main(String[] args) { 8 9 PlayerModule module = new PlayerModule(); 10 Injector injector = Guice.createInjector(new Module[]{module}); 11 12 @Good Player player = (Player)injector.getInstance(Player.class); 13 player.bat(); 14 player.bowl(); 15 } 16 }
此處注意第10行和第12行。 第12行代碼 @Good 告訴 Guice 去 Playe Moduler 中獲取一個 GoodPlayer實例對象。
4. Named 注釋
像上面例子中,如果只是為了標記實現類以便於客戶端使用,而為每一個實現類創建新的 Annotation ,那么是完全沒有必要的。我們可以使用 @Named 注釋來命名這些 entities。這兒有一個工具方法 - Names.named() ,當你給它一個命名,它會返回好一個命名好的 Annotation。例如上面的例子中,在 Player Module 中可以使用 Names.named() 來完成一些相同的事情。
import com.google.inject.Binder; import com.google.inject.Module; public class PlayerModule implements Module{ public void configure(Binder binder) { binder.bind(Player.class).annotatedWith(Names.named("Good")).to( GoodPlayer.class); binder.bind(Player.class).annotatedWith(Names.named("Bad")).to( BadPlayer.class); } }
現在在客戶端代碼中,我們將使用 @Named() annotation來獲取注釋。
@Named("Good") Player goodPlayer = (Player)injector.getInstance(Player.class); @Named("Bad") Player badPlayer = (Player)injector.getInstance(Player.class);
5. 一個簡單的 Provider
在 Guice 中 Providers 就像 Factories 一樣創建和返回對象。在大部分情況下,客戶端可以直接依賴 Guice 框架來為服務(Services)創建依賴的對象。但是少數情況下,應用程序代碼需要為一個特定的類型定制對象創建流程(Object creation process),這樣可以控制對象創建的數量,提供緩存(Cache)機制等,這樣的話我們就要依賴 Guice 的 Provider 類。
例如,我們需要為 MockConnection 創建一個對象創建和銷毀的流程,代碼如下。
public class MockConnection { public void connect(){ System.out.println("Connecting to the mock database"); } public void disConnect(){ System.out.println("Dis-connecting from the mock database"); } }
現在我們來寫一個簡單的 Provider 類來實現 Guice 的 Provider 接口,使用它創建並返 MockConnection對象,代碼如下。
public class ConnectionProvider implements Provider<MockConnection>{ @Override public MockConnection get() { // Do some customization mechanism here. MockConnection connection = new MockConnection(); // Do some customization mechanism here too. return connection; } }
需要注意的是,所有的自定義 Provider 類必需實現 Provider 接口,並且重寫里面的 get() 方法。現在 Module 需要留意這個自定義的 Provider 類,它需要請求 ConnectionProvider 來創建對象,而不是直接創建對象,實現的代碼如下。
1 import com.google.inject.*; 2 3 public class ConnectionTest { 4 5 public static void main(String args[]){ 6 Injector injector = Guice.createInjector( 7 new Module(){ 8 @Override 9 public void configure(Binder binder) { 10 binder.bind(MockConnection.class).toProvider( 11 ConnectionProvider.class); 12 } 13 } 14 ); 15 16 MockConnection connection = 17 injector.getInstance(MockConnection.class); 18 connection.connect(); 19 connection.disConnect(); 20 } 21 }
注意第10行,我們使用 toProvider() 方法將 MockConnection.class 綁定到一個 Provider 上。
小結
這篇文章簡要的講解了一些 Guice 相關的內容,有時間我將講講Guice的一些高級應用,還有 Robo Guice的使用。
本文參考鏈接
http://code.google.com/p/google-guice/
http://www.javabeat.net/2007/08/introduction-to-google-guice/
/**********************************************************
* Author: Canice Hu
* QQ : 540678976
* 版權歸本人所有,轉載請聲明出處
**********************************************************/