簡介
Java企業應用開發社區在連接對象方面花了很大功夫。你的Web應用如何訪問中間層服 務?你的服務如何連接到登錄用戶和事務管理器?關於這個問題你會發現很多通用的和特 定的解決方案。有一些方案依賴於模式,另一些則使用框架。所有這些方案都會不同程度 地引入一些難於測試或者程式化代碼重復的問題。你馬上就會看到,Guice 在這方面是全 世界做得最好的:非常容易進行單元測試,最大程度的靈活性和可維護性,以及最少的代 碼重復。
我們使用一個假想的、簡單的例子來展示 Guice 優於其他一些你可能已經熟悉的經典 方法的地方。下面的例子過於簡單,盡管它展示了許多顯而易見的優點,但其實它還遠沒 有發揮出 Guice 的全部潛能。我們希望,隨著你的應用開發的深入,Guice 的優越性也 會更多地展現出來。
在這個例子中,一個客戶對象依賴於一個服務接口。該服務接口可以提供任何服務, 我們把它稱為Service。
public interface Service {
void go();
}
對於這個服務接口,我們有一個缺省的實現,但客戶對象不應該直接依賴於這個缺省 實現。如果我們將來打算使用一個不同的服務實現,我們不希望回過頭來修改所有的客戶 代碼。
public class ServiceImpl implements Service {
public void go() {
...
}
}
我們還有一個可用於單元測試的偽服務對象。
public class MockService implements Service {
private boolean gone = false;
public void go() {
gone = true;
}
public boolean isGone() {
return gone;
}
}
簡單工廠模式
在發現依賴注入之前,最常用的是工廠模式。除了服務接口之外,你還有一個既可以 向客戶提供服務對象,也可以向測試程序傳遞偽服務對象的工廠類。在這裡我們會將服務 實現為一個單件對象,以便讓示例盡量簡化。
public class ServiceFactory {
private ServiceFactory() {}
private static Service instance = new ServiceImpl();
public static Service getInstance() {
return instance;
}
public static void setInstance(Service service) {
instance = service;
}
}
客戶程序每次需要服務對象時就直接從工廠獲取。
public class Client {
public void go() {
Service service = ServiceFactory.getInstance();
service.go();
}
}
客戶程序足夠簡單。但客戶程序的單元測試代碼必須將一個偽服務對象傳入工廠,同 時要記得在測試後清理。在我們這個簡單的例子裡,這不算什麼難事兒。但當你增加了越 來越多的客戶和服務代碼後,所有這些偽代碼和清理代碼會讓單元測試的開發一團糟。此 外,如果你忘記在測試後清理,其他測試可能會得到與預期不符的結果。更糟的是,測試 的成功與失敗可能取決於他們被執行的順序。
public void testClient() {
Service previous = ServiceFactory.getInstance();
try {
final MockService mock = new MockService();
ServiceFactory.setInstance(mock);
Client client = new Client();
client.go();
assertTrue(mock.isGone());
}
finally {
ServiceFactory.setInstance(previous);
}
}
最後,注意服務工廠的API把我們限制在了單件這一種應用模式上。即便 getInstance() 可以返回多個實例, setInstance() 也會束縛我們的手腳。轉換到非單 件模式也意味著轉換到了一套更復雜的API。
手工依賴注入
依賴注入模式的目標之一是使單元測試更簡單。我們不需要特殊的框架就可以實踐依 賴注入模式。依靠手工編寫代碼,你可以得到該模式大約80%的好處。
當上例中的客戶代碼向工廠對象請求一個服務時,根據依賴注入模式,客戶代碼希望 它所依賴的對象實例可以被傳入自己。也就是說:不要調用我,我會調用你。
public class Client {
private final Service service;
public Client(Service service) {
this.service = service;
}
public void go() {
service.go();
}
}
這讓我們的單元測試簡化了不少。我們可以只傳入一個偽服務對象,在結束後也不需 要多做什麼。
public void testClient() {
MockService mock = new MockService();
Client client = new Client(mock);
client.go();
assertTrue(mock.isGone());
}
我們也可以精確地區分出客戶代碼依賴的API。
現在,我們如何連接客戶和服務對象呢?手工實現依賴注入的時候,我們可以將所有 依賴邏輯都移動到工廠類中。也就是說,我們還需要有一個工廠類來創建客戶對象。
public static class ClientFactory {
private ClientFactory() {}
public static Client getInstance() {
Service service = ServiceFactory.getInstance();
return new Client(service);
}
}
手工實現依賴注入需要的代碼行數和簡單工廠模式差不多。
用 Guice 實現依賴注入
手工為每一個服務與客戶實現工廠類和依賴注入邏輯是一件很麻煩的事情。其他一些 依賴注入框架甚至需要你顯式將服務映射到每一個需要注入的地方。
Guice 希望在不犧牲可維護性的情況下去除所有這些程式化的代碼。
使用 Guice,你只需要實現模塊類。Guice 將一個綁定器傳入你的模塊,你的模塊使 用綁定器來連接接口和實現。以下模塊代碼告訴 Guice 將 Service映射到單件模式的 ServiceImpl:
public class MyModule implements Module {
public void configure(Binder binder) {
binder.bind(Service.class)
.to(ServiceImpl.class)
.in(Scopes.SINGLETON);
}
}
模塊類告訴 Guice 我們想注入什麼東西。那麼,我們該如何告訴 Guice 我們想把它 注入到哪裡呢?使用 Guice,你可以使用 @Inject 標注你的構造器,方法或字段:
public class Client {
private final Service service;
@Inject
public Client(Service service) {
this.service = service;
}
public void go() {
service.go();
}
}
@Inject 標注可以清楚地告訴其他程序員你的類中哪些成員是被注入的。
為了讓 Guice向 Client 中注入,我們必須直接讓 Guice 幫我們創建Client 的實例 ,或者,其他類必須包含被注入的Client 實例。
Guice vs. 手工依賴注入
如你所見,Guice 省去了寫工廠類的麻煩。你不需要編寫代碼將客戶連接到它們所依 賴的對象。如果你忘了提供一個依賴關系,Guice 在啟動時就會失敗。Guice 也會自動處 理循環依賴關系。
Guice 允許你通過聲明指定對象的作用域。例如,你需要編寫相同的代碼將對象反復 存入 HttpSession。
實際情況通常是,只有到了運行時,你才能知道具體要使用哪一個實現類。因此你需 要元工廠類或服務定位器來增強你的工廠模式。Guice 用最少的代價解決了這些問題。
手工實現依賴注入時,你很容易退回到使用直接依賴的舊習慣,特別是當你對依賴注 入的概念還不那麼熟悉的時候。使用 Guice 可以避免這種問題,可以讓你更容易地把事 情做對。Guice 使你保持正確的方向。
更多的標注
只要有可能,Guice 就允許你使用標注來替代顯式地綁定對象,以減少更多的程式化 代碼。回到我們的例子,如果你需要一個接口來簡化單元測試,而你又不介意編譯時的依 賴,你可以直接從你的接口指向一個缺省的實現。
@ImplementedBy(ServiceImpl.class)
public interface Service {
void go();
}
這時,如果客戶需要一個 Service 對象,且 Guice 無法找到顯式綁定,Guice 就會 注入一個 ServiceImpl 的實例。
缺省情況下,Guice 每次都注入一個新的實例。如果你想指定不同的作用域規則,你 也可以對實現類進行標注。
@Singleton
public class ServiceImpl implements Service {
public void go() {
...
}
}
架構概覽
我們可以將 Guice 的架構分成兩個不同的階段:啟動和運行。你在啟動時創建一個注 入器 Injector,在運行時用它來注入對象。
啟動
你通過實現 Module 來配置 Guice。你傳給 Guice 一個模塊對象,Guice 則將一個綁 定器 Binder 對象傳入你的模塊,然後,你的模塊使用綁定器來配置綁定。一個綁定通常 包含一個從接口到具體實現的映射。例如:
public class MyModule implements Module {
public void configure(Binder binder) {
// Bind Foo to FooImpl. Guice will create a new
// instance of FooImpl for every injection.
binder.bind(Foo.class).to(FooImpl.class);
// Bind Bar to an instance of Bar.
Bar bar = new Bar();
binder.bind(Bar.class).toInstance(bar);
}
}
在這個階段,Guice 會察看你告訴它的所有類,以及任何與這些類有關系的類,然後 通知你是否有依賴關系的缺失。例如,在一個Struts 2 應用中,Guice 知道你所有的動 作類。Guice 會檢查你的動作類以及它們依賴的所有類,如果有問題會及早報錯。
創建一個 Injector 涉及以下步驟:
首先創建你的模塊類實例,並將其傳入 Guice.createInjector().
Guice 創建一個綁定器 Binder 並將其傳入你的模塊。
你的模塊使用綁定器來定義綁定。
基於你所定義的綁定,Guice 創建一個注入器 Injector 並將其返回給你。
你使用注入器來注入對象。
運行
現在你可以使用第一階段創建的注入器來注入對象並內省(introspect)我們的綁定 了。Guice 的運行時模型由一個可管理一定數量綁定的注入器組成。
鍵 Key 唯一地確定每一個綁定。Key 包含了客戶代碼所依賴的類型以及一個可選的標 注。你可以使用標注來區分指向同一類型的多個綁定。Key 的類型和標注對應於注入時的 類型和標注。
每個綁定有一個提供者 provider,它提供所需類型的實例。你可以提供一個類, Guice 會幫你創建它的實例。你也可以給 Guice 一個你要綁定的類的實例。你還可以實 現你自己的 provider,Guice 可以向其中注入依賴關系。
每個綁定還有一個可選的作用域。缺省情況下綁定沒有作用域,Guice 為每一次注入 創建一個新的對象。一個定制的作用域可以使你控制 Guice 是否創建新對象。例如,你 可以為每一個 HttpSession 創建一個實例。
自舉(Bootstrapping)你的應用
自舉(bootstrapping)對於依賴注入非常重要。總是顯式地向 Injector 索要依賴, 這就將 Guice 用作了服務定位器,而不是一個依賴注入框架。
你的代碼應該盡量少地和 Injector 直接打交道。相反,你應該通過注入一個根對象 來自舉你的應用。容器可以更進一步地將依賴注入根對象所依賴的對象,並如此迭代下去 。最終,在理想情況下,你的應用中應該只有一個類知道Injector,每個其他類都應該使 用注入的依賴關系。
例如,一個諸如 Struts 2 的Web 應用框架通過注入你的所有動作類來自舉你的應用 。你可以通過注入你的服務實現類來自舉一個 Web 服務框架。
依賴注入是傳染性的。如果你重構一個有大量靜態方法的已有代碼,你可能會覺得你 正在試圖拉扯一根沒有盡頭的線。這是好事情。它表明依賴注入正在幫助你改進代碼的靈 活性和可測試性。
如果重構工作太復雜,你不想一次性地整理完所有代碼,你可以暫時將一個 Injector 的引用存入某個類的一個靜態的字段,或是使用靜態注入。這時,請清楚地命名包含該字 段的類:比如 InjectorHack和 GodKillsAKittenEveryTimeYouUseMe。記住你將來可能不 得不為這些類提供偽測試類,你的單元測試則不得不手工安裝一個注入器。記住,你將來 需要清理這些代碼。
綁定依賴關系
Guice 是如何知道要注入什麼東西的呢?對啟動器來說,一個包含了類型和可選的標 注的 Key 唯一地指明了一個依賴關系。Guice 將 key和實現之間的映射標記為一個 Binding。一個實現可以包含一個單獨的對象,一個需要由 Guice 注入的類,或一個定制 的 provider。
當注入依賴關系時,Guice 首先尋找顯式綁定,即你通過綁定器 Binder指明的綁定。 Binder API 使用生成器(Builder)模式來創建一種領域相關的描述語言。根據約束適用 方法的上下文的不同,不同方法返回不同的對象。
例如,為了將接口 Service 綁定到一個具體的實現 ServiceImpl,調用:
binder.bind(Service.class).to(ServiceImpl.class);
該綁定與下面的方法匹配:
@Inject
void injectService(Service service) {
...
}
注: 與某些其他的框架相反,Guice 並沒有給 "setter" 方法任何特殊待遇。不管方 法有幾個參數,只要該方法含有 @Inject標注,Guice就會實施注入,甚至對基類中實現 的方法也不例外。
不要重復自己
對每個綁定不斷地重復調用"binder" 似乎有些乏味。Guice 提供了一個支持 Module 的類,名為 AbstractModule,它隱含地賦予你訪問 Binder 的方法的權力。例如,我們 可以用擴展 AbstractModule 類的方式改寫上述綁定:
bind(Service.class).to(ServiceImpl.class);
在本手冊的余下部分中我們會一直使用這樣的語法。
標注綁定
如果你需要指向同一類型的多個綁定,你可以用標注來區分這些綁定。例如,將接口 Service 和標注 @Blue 綁定到具體的實現類 BlueService 的代碼如下:
bind(Service.class)
.annotatedWith(Blue.class)
.to(BlueService.class);
這個綁定會匹配以下方法:
@Inject
void injectService(@Blue Service service) {
...
}
注意,標注 @Inject 出現在方法前,而綁定標注(如 @Blue)則出現在參數前。對構 造函數也是如此。使用字段注入時,兩種標注都直接應用於字段,如以下代碼:
@Inject @Blue Service service;
創建綁定標注
剛才提到的標注 @Blue 是從哪裡來的?你可以很容易地創建這種標注,但不幸的是, 你必須使用略顯復雜的標准語法:
/**
* Indicates we want the blue version of a binding.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface Blue {}
幸運的是,我們不需要理解這些代碼,只要會用就可以了。對於好奇心強的朋友,下 面是這些程式化代碼的含義:
@Retention(RUNTIME) 使得你的標注在運行時可見。
@Target({FIELD, PARAMETER}) 是對用戶使用的說明;它不允許 @Blue 被用於方法、 類型、局部變量和其他標注。
@BindingAnnotation 是 Guice 特定的信號,表示你希望該標注被用於綁定標注。當 用戶將多於一個的綁定標注應用於同一個可注入元素時,Guice 會報錯。
有屬性的標注
如果你已經會寫有屬性的標注了,請跳到下一節。
你也可以綁定到標注實例,即,你可以有多個綁定指向同樣的類型和標注類型,但每 個綁定擁有不同的標注屬性值。如果 Guice 找不到擁有特定屬性值的標注實例,它會去 找一個綁定到該標注類型的綁定。
例如,我們有一個綁定標注 @Named,它有一個字符串屬性值。
@Retention(RUNTIME)
@Target({ FIELD, PARAMETER })
@BindingAnnotation
public @interface Named {
String value();
}
如果我們希望綁定到 @Named("Bob"),我們首先需要一個 Named 的實現。我們的實現 必須遵守關於 Annotation 的約定,特別是 hashCode() 和 equals() 的實現。
class NamedAnnotation implements Named {
final String value;
public NamedAnnotation(String value) {
this.value = value;
}
public String value() {
return this.value;
}
public int hashCode() {
// This is specified in java.lang.Annotation.
return 127 * "value".hashCode() ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Named))
return false;
Named other = (Named) o;
return value.equals(other.value());
}
public String toString() {
return "@" + Named.class.getName() + "(value=" + value + ")";
}
public Class<? extends Annotation> annotationType() {
return Named.class;
}
}
現在我們可以使用這個標注實現來創建一個指向 @Named 的綁定。
bind(Person.class)
.annotatedWith(new NamedAnnotation("Bob"))
.to(Bob.class);
與其它框架使用基於字符串的標識符相比,這顯得有些繁瑣,但記住,使用基於字符 串的標識符,你根本無法這樣做。而且,你會發現你可以大量復用已有的綁定標注。
因為通過名字標記一個綁定非常普遍,以至於 Guice 在 com.google.inject.name 中 提供了一個十分有用的 @Named 的實現。
隱式綁定
正如我們在簡介中看到的那樣,你並不總是需要顯式地聲明綁定。如果缺少顯式綁定 ,Guice 會試圖注入並創建一個你所依賴的類的新實例。如果你依賴於一個接口,Guice 會尋找一個指向具體實現的 @ImplementedBy 標注。例如,下例中的代碼顯式綁定到一個 具體的、可注入的名為 Concrete 的類。它的含義是,將 Concrete 綁定到 Concrete。 這是顯式的聲明方式,但也有些冗余。
bind(Concrete.class);
刪除上述綁定語句不會影響下面這個類的行為:
class Mixer {
@Inject
Mixer(Concrete concrete) {
...
}
}
好吧,你自己來選擇:顯式的或簡略的。無論何種方式,Guice 在遇到錯誤時都會生 成有用的信息。
注入提供者
有時對於每次注入,客戶代碼需要某個依賴的多個實例。其它時候,客戶可能不想在 一開始就真地獲取對象,而是等到注入後的某個時候再獲取。對於任意綁定類型 T,你可 以不直接注入 T 的實例,而是注入一個 Provider<T>,然後在需要的時候調用 Provider<T>.get(),例如:
@Inject
void injectAtm(Provider<Money> atm) {
Money one = atm.get();
Money two = atm.get();
...
}
正如你所看到的那樣, Provider 接口簡單得不能再簡單了,它不會為簡單的單元測 試添加任何麻煩。
注入常數值
對於常數值,Guice 對以下幾種類型做了特殊處理:
基本類型(int, char, ...)
基本封裝類型(Integer, Character, ...)
Strings
Enums
Classes
首先,當綁定到這些類型的常數值的時候,你不需要指定你要綁定到的類型。Guice 可以根據值判斷類型。例如,一個綁定標注名為 TheAnswer:
bindConstant().annotatedWith(TheAnswer.class).to(42);
它的效果等價於:
bind(int.class).annotatedWith(TheAnswer.class).toInstance(42);
當需要注入這些類型的數值時,如果 Guice 找不到指向基本數據類型的顯式綁定,它 會找一個指向相應的封裝類型的綁定,反之亦然。
轉換字符串
如果 Guice 仍然無法找到一個上述類型的顯式綁定,它會去找一個擁有相同綁定標注 的常量 String 綁定,並試圖將字符串轉換到相應的值。例如:
bindConstant().annotatedWith(TheAnswer.class).to("42"); // String!
會匹配:
@Inject @TheAnswer int answer;
轉換時,Guice 會用名字去查找枚舉和類。Guice 在啟動時轉換一次,這意味著它提 前做了類型檢查。這個特性特別有用,例如,當綁定值來自一個屬性文件的時候。
定制的提供者
有時你需要手工創建你自己的對象,而不是讓 Guice 創建它們。例如,你可能不能為 來自第三方的實現類添加 @Inject 標注。在這種情況下,你可以實現一個定制的 Provider。Guice 甚至可以注入你的提供者類。例如:
class WidgetProvider implements Provider<Widget> {
final Service service;
@Inject
WidgetProvider(Service service) {
this.service = service;
}
public Widget get() {
return new Widget(service);
}
}
你可以像這樣把 Widget 綁定到 WidgetProvider:
bind(Widget.class).toProvider(WidgetProvider.class);
注入定制的提供者可以使 Guice 提前檢查類型和依賴關系。定制的提供者可以在任意 作用域中使用,而不依賴於他們所創建的類的作用域。缺省情況下,Guice 為每一次注入 創建一個新的提供者實例。在上例中,如果每個 Widget 需要它自己的 Service 實例, 我們的代碼也沒有問題。通過在工廠類上使用作用域標注,或為工廠類創建單獨的綁定, 你可以為定制的工廠指定不同的作用域。
示例:與 JNDI 集成
例如我們需要綁定從 JNDI 得到的對象。我們可以仿照下面的代碼實現一個可復用的 定制的提供者。注意我們注入了 JNDI Context:
package mypackage;
import com.google.inject.*;
import javax.naming.*;
class JndiProvider<T> implements Provider<T> {
@Inject Context context;
final String name;
final Class<T> type;
JndiProvider(Class<T> type, String name) {
this.name = name;
this.type = type;
}
public T get() {
try {
return type.cast(context.lookup(name));
}
catch (NamingException e) {
throw new RuntimeException(e);
}
}
/**
* Creates a JNDI provider for the given
* type and name.
*/
static <T> Provider<T> fromJndi(
Class<T> type, String name) {
return new JndiProvider<T>(type, name);
}
}
感謝泛型擦除(generic type erasure)技術。我們必須在運行時將依賴傳入類中。 你可以省略這一步,但在今後跟蹤類型轉換錯誤會比較棘手(當 JNDI 返回錯誤類型的對 象的時候)。
我們可以使用定制的 JndiProvider 來將 DataSource 綁定到來自 JNDI 的一個對象 :
import com.google.inject.*;
import static mypackage.JndiProvider.fromJndi;
import javax.naming.*;
import javax.sql.DataSource;
...
// Bind Context to the default InitialContext.
bind(Context.class).to(InitialContext.class);
// Bind to DataSource from JNDI.
bind(DataSource.class)
.toProvider(fromJndi(DataSource.class, "..."));
限制綁定的作用域
缺省情況下,Guice 為每次注入創建一個新的對象。我們把它稱為“無作用域”。你 可以在配制綁定時指明作用域。例如,每次注入相同的實例:
bind(MySingleton.class).in(Scopes.SINGLETON);
另一種做法是,你可以在實現類中使用標注來指明作用域。Guice 缺省支持 @Singleton:
@Singleton
class MySingleton {
...
}
使用標注的方法對於隱式綁定也同樣有效,但需要 Guice 來創建你的對象。另一方面 ,調用 in() 適用於幾乎所有綁定類型(顯然,綁定到一個單獨的實例是個例外)並且會 忽略已有的作用域標注。如果你不希望引入對於作用域實現的編譯時依賴,in() 還可以 接受標注。
可以使用 Binder.bindScope() 為定制的作用域指定標注。例如,對於標注 @SessionScoped 和一個 Scope 的實現 ServletScopes.SESSION:
binder.bindScope(SessionScoped.class, ServletScopes.SESSION);
創建作用域標注
用於指定作用域的標注必須:
有一個 @Retention(RUNTIME) 標注,從而使我們可以在運行時看到該標注。
有一個 @Target({TYPE}) 標注。作用域標注只用於實現類。
有一個 @ScopeAnnotation 元標注。一個類只能使用一個此類標注。
例如:
/**
* Scopes bindings to the current transaction.
*/
@Retention(RUNTIME)
@Target({TYPE})
@ScopeAnnotation
public @interface TransactionScoped {}
盡早加載綁定
Guice 可以等到你實際使用對象時再加載單件對象。這有助於開發,因為你的應用程 序可以快速啟動,只初始化你需要的對象。但是,有時你總是希望在啟動時加載一個對象 。你可以告訴 Guice,讓它總是盡早加載一個單件對象,例如:
bind(StartupTask.class).asEagerSingleton();
我們經常在我們的應用程序中使用這個方法實現初始化邏輯。你可以通過在 Guice 必 須首先初始化的單件對象上創建依賴關系來控制初始化順序。
在不同作用域間注入
你可以安全地將來自大作用域的對象注入到來自小作用域或相同作用域的對象中。例 如,你可以將一個作用域為 HTTP 會話的對象注入到作用域為 HTTP 請求的對象中。但是 ,向較大作用域的對象中注入就是另一件事了。例如,如果你把一個作用域為 HTTP 請求 的對象注入到一個單件對象中,最好情況下,你會得到無法在 HTTP 請求中運行的錯誤信 息,最壞情況下,你的單件對象會總是引用來自第一個 HTTP 請求的對象。在這些時候, 你應該注入一個 Provider<T>,然後在需要的時候使用它從較小的作用域中獲取對 象。這時,你必須確保,在 T 的作用域之外,永遠不要調用這個提供者(例如,當目前 沒有 HTTP 請求且 T 的作用域為 HTTP 請求的時候)。
開發階段
Guice 明白你的應用開發需要經歷不同的階段。你可以在創建容器時告訴它應用程序 運行在哪一個階段。Guice 目前支持“開發”和“產品”兩個階段。我們發現測試通常屬 於其中某一個階段。
在開發階段,Guice 會根據需要加載單件對象。這樣,你的應用程序可以快速啟動, 只加載你正在測試的部分。
在產品階段,Guice 會在啟動時加載全部單件對象。這幫助你盡早捕獲錯誤,提前優 化性能。
你的模塊也可以使用方法攔截和其他基於當前階段的綁定。例如,一個攔截器可能會 在開發階段檢查你是否在作用域之外使用對象。
攔截方法
Guice 使用 AOP Alliance API 支持簡單的方法攔截。你可以在模塊中使用 Binder 綁定攔截器。例如,對標注有 @Transactional 的方法應用事務攔截器:
import static com.google.inject.matcher.Matchers.*;
...
binder.bindInterceptor(
any(), // Match classes.
annotatedWith(Transactional.class), // Match methods.
new TransactionInterceptor() // The interceptor.
);
盡量讓匹配代碼多做些過濾工作,而不是在攔截器中過濾。因為匹配代碼只在啟動時 運行一次。
靜態注入
靜態字段和方法會增加測試和復用的難度,但有的時候你唯一的選擇就是保留一個 Injector 的靜態引用。
在這些情況下,Guice 支持注入可訪問性較少的靜態方法。例如,HTTP 會話對象經常 需要被串行化,以支持復制機制。但是,如果你的會話對象依賴於一個作用域為容器生命 周期的對象,該怎麼辦呢?我們可以保留一個該對象的臨時引用,但在反串行化的時候, 我們該如何再次找到該對象呢?
我們發現更實用的解決方案是使用靜態注入:
@SessionScoped
class User {
@Inject
static AuthorizationService authorizationService;
...
}
Guice 從不自動實施靜態注入。你必須使用 Binder 顯式請求 Injector 在啟動後注 入你的靜態成員:
binder.requestStaticInjection(User.class);
靜態注入是一個很難避免的禍害,它會使測試難度加大。如果有辦法避開它,你多半 會很高興的。
可選注入
有時你的代碼應該在無論綁定是否存在的時候都能工作。在這些情況下,你可以使用 @Inject(optional=true),Guice 會在有綁定可用時,用一個綁定實現覆蓋你的缺省實現 。例如:
@Inject(optional=true) Formatter formatter = new DefaultFormatter();
如果誰為 Formatter 創建了一個綁定,Guice 會基於該綁定注入實例。否則,如果 Formatter 不能被注入(參見隱式綁定),Guice 會忽略可選成員。
可選注入只能應用於字段和方法,而不能用於構造函數。對於方法,如果一個參數的 綁定找不到,Guice 就不會注入該方法,即便其他參數的綁定是可用的。
綁定到字符串
只要有可能,我們就盡量避免使用字符串,因為它們容易被錯誤拼寫,對工具不友好 ,等等。但使用字符串而不是創建定制的標注對於“快而髒”的代碼來說仍是有用的。在 這些情況下,Guice 提供了@Named 和 Names。例如,一個到字符串名字的綁定:
import static com.google.inject.name.Names.*;
...
bind(named("bob")).to(10);
會匹配下面的注入點:
@Inject @Named("bob") int score;
Struts 2支持
要在 Struts 2.0.6 或更高版本中安裝 Guice Struts 2 插件,只要將 guice- struts2-plugin-1.0.jar 包含在你的 Web 應用的 classpath 中,並在 struts.xml 文 件中選擇 Guice 作為你的 ObjectFactory 實現即可:
<constant name="struts.objectFactory" value="guice" />
Guice 會注入所有你的 Struts 2 對象,包括動作和攔截器。你甚至可以設置動作類 的作用域。你也可以在你的 struts.xml 文件中指定 Guice 的 Module:
<constant name="guice.module" value="mypackage.MyModule"/>
如果你的所有綁定都是隱式的,你就根本不用定義模塊了。
一個計數器的例子
例如,我們試圖統計一個會話中的請求數目。定義一個在會話中存活的 Counter 對象 :
@SessionScoped
public class Counter {
int count = 0;
/** Increments the count and returns the new value. */
public synchronized int increment() {
return count++;
}
}
接下來,我們可以將我們的計數器注入到動作中:
public class Count {
final Counter counter;
@Inject
public Count(Counter counter) {
this.counter = counter;
}
public String execute() {
return SUCCESS;
}
public int getCount() {
return counter.increment();
}
}
然後在 struts.xml 文件中為動作類創建映射:
<action name="Count"
class="mypackage.Count">
<result>/WEB-INF/Counter.jsp</result>
</action>
以及一個用於顯示結果的 JSP 頁面:
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<body>
<h1>Counter Example</h1>
<h3><b>Hits in this session:</b>
<s:property value="count"/></h3>
</body>
</html>
我們實際上把這個例子做得比需求更復雜,以便展示更多的概念。在現實中,我們不 需要使用單獨的 Counter 對象,只要把 @SessionScoped 直接應用於我們的動作類即可 。
JMX 集成
參見 com.google.inject.tools.jmx.
附錄:注入器如何解決注入請求
注入器解決注入請求的過程依賴於已有的綁定和相關類型中的標注。這裡是關於如何 解決注入請求的一個概要描述:
觀察被注入元素的 Java 類型和可選的“綁定標注”。如果類型是 com.google.inject.Provider<T>,就使用類型 T 解決注入請求。對於(類型,標 注)對,尋找一個綁定。如果找不到,則跳到步驟4。
沿著綁定鏈檢查。如果該綁定連接到另一個綁定,則沿著這條邊繼續檢查,直到到達 一個沒有連接到任何後續綁定的綁定為止。現在我們就為該注入請求找到了最明確的顯式 綁定。
如果綁定指明一個實例或一個 Provider 實例,所有事情都做完了;使用這個實例來 滿足請求即可。
此時,如果注入請求使用了標注類型或值,我們就報告錯誤。
否則,檢查綁定的 Java 類型;如果找到了 @ImplementedBy 標注,就實例化該類型 。如果找到了 @ProvidedBy 標注,就實例化提供者類並用它來獲取想要的對象。否則試 圖實例化類型本身。