DI框架 Google-Guice入門介紹
概述
Guice是一個輕量級的DI框架。本文對Guice的基本用法作以介紹。
本文的所有例子基于Guice 3.0
本文的很多代碼來源于Guice主頁:http://code.google.com/p/google-guice/wiki/GettingStarted舉例說明Guice的用法
Guice本身只是一個輕量級的DI框架,首先我們通過一個例子來看看怎么使用Guice。首先有一個需要被實現的接口:
public interface BillingService {/**
- Attempts to charge the order to the credit card. Both successful and
- failed transactions will be recorded. *
- @return a receipt of the transaction. If the charge was successful, the
- receipt will be successful. Otherwise, the receipt will contain a
decline note describing why the charge failed. */ Receipt chargeOrder(PizzaOrder order, CreditCard creditCard); }</pre>
然后,有一個實現該接口的實現類:class RealBillingService implements BillingService { private final CreditCardProcessor processor; private final TransactionLog transactionLog;
@Inject RealBillingService(CreditCardProcessor processor, TransactionLog transactionLog) { this.processor = processor; this.transactionLog = transactionLog; }
@Override public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) { ... } }</pre>
現在接口有了,實現類也有了,接下來就是如何將接口和實現類關聯的問題了,在Guice中需要定義Module來進行關聯public class BillingModule extends AbstractModule { @Override protected void configure() {
/*
- This tells Guice that whenever it sees a dependency on a TransactionLog,
- it should satisfy the dependency using a DatabaseTransactionLog. */ bind(TransactionLog.class).to(DatabaseTransactionLog.class);
/*
- Similarly, this binding tells Guice that when CreditCardProcessor is used in
- a dependency, that should be satisfied with a PaypalCreditCardProcessor.
/
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
}
}</pre>
好了,現在萬事俱備,就讓我們一起看看怎么使用Guice進行依賴注入吧:
public static void main(String[] args) { /
- Guice.createInjector() takes your Modules, and returns a new Injector
- instance. Most applications will call this method exactly once, in their
- main() method. */ Injector injector = Guice.createInjector(new BillingModule());
/*
- Now that we've got the injector, we can build objects.
*/
RealBillingService billingService = injector.getInstance(RealBillingService.class);
...
}</pre>
以上就是使用Guice的一個完整的例子,很簡單吧,不需要繁瑣的配置,只需要定義一個Module來表述接口和實現類,以及父類和子類之間的關聯關系的綁定。本文不對比guice和spring,只是單純介紹Guice的用法。
綁定方式的介紹
從上面我們可以看出,其實對于Guice而言,程序員所要做的,只是創建一個代表關聯關系的Module,然后使用這個Module即可得到對應關聯的對象。因此,主要的問題其實就是在如何關聯實現類和接口(子類和父類)。在自定義的Module類中進行綁定
在configure方法中綁定
鏈式綁定
鏈式綁定是最簡單,最直接,也是使用最多的綁定方式。protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); }
就是直接把一種類型的class對象綁定到另外一種類型的class對象,這樣,當外界獲取TransactionLog時,其實返回的就是一個DatabaseTransactionLog對象。當然,鏈式綁定也可以串起來,如:
protected void configure() { bind(TransactionLog.class).to(DatabaseTransactionLog.class); bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class); }
這樣,當外界請求TransactionLog時,其實返回的就會是一個MySqlDatabaseTransactionLog對象。注解(Annotations)綁定
鏈式綁定針對于同樣的類型都綁定到同一種目標類型時,非常好用,但是對于一個接口有多種實現的時候,鏈式綁定就不好區分該用哪種實現了。可以把Annotations綁定方式看作是鏈式綁定的一種擴展,專門用來解決這種同一個接口有多種實現的問題。Annotations綁定又可以分為兩種,一種是需要自己寫Annotations,另外一種則簡化了一些。自己寫Annotations的方式
首先,寫一個注解import com.google.inject.BindingAnnotation; import java.lang.annotation.Target; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD;
@BindingAnnotation @Target({ FIELD, PARAMETER, METHOD }) @Retention(RUNTIME) public @interface PayPal {}</pre>然后,使用這個注解去修飾目標字段或參數,如:
public class RealBillingService implements BillingService {@Inject public RealBillingService(@PayPal CreditCardProcessor processor, TransactionLog transactionLog) { ... } }</pre>或
public class RealBillingService implements BillingService { @Inject @Www private CreditCardProcessor processor; ... }最后,在我們進行鏈式綁定時,就可以區分一個接口的不同實現了,如:
bind(CreditCardProcessor.class) .annotatedWith(PayPal.class) .to(PayPalCreditCardProcessor.class);這樣,被Annotations PayPal?修飾的CreditCardProcessor就會被綁定到目標實現類PayPalCreditCardProcessor。如果有其他的實現類,則可把用不同Annotations修飾的CreditCardProcessor綁定到不同的實現類使用@Named的方式
使用@Named的方式和上面自己寫Annotation的方式很類似,只不過做了相應的簡化,不再需要自己去寫Annotation了。public class RealBillingService implements BillingService {@Inject public RealBillingService(@Named("Checkout") CreditCardProcessor processor, TransactionLog transactionLog) { ... }</pre>
直接使用@Named修飾要注入的目標,并起個名字,下面就可以把用這個名字的注解修飾的接口綁定到目標實現類了bind(CreditCardProcessor.class) .annotatedWith(Names.named("Checkout")) .to(CheckoutCreditCardProcessor.class);
實例綁定
上面介紹的鏈式綁定是把接口的class對象綁定到實現類的class對象,而實例綁定則可以看作是鏈式綁定的一種特例,它直接把一個實例對象綁定到它的class對象上。
bind(String.class) .annotatedWith(Names.named("JDBC URL")) .toInstance("jdbc:mysql://localhost/pizza"); bind(Integer.class) .annotatedWith(Names.named("login timeout seconds")) .toInstance(10);
需要注意的是,實例綁定要求對象不能包含對自己的引用。并且,盡量不要對那種創建實例比較復雜的類使用實例綁定,否則會讓應用啟動變慢
Provider綁定
在下面會介紹基于@Provides方法的綁定。其實Provider綁定是基于@Provides方法綁定的后續發展,所以應該在介紹完基于@Provides方法綁定之后再來介紹,不過因為Provider綁定也是在configure方法中完成的,而本文又是按照綁定的位置來組織的,因為就把Provider綁定放在這了,希望大家先跳到后面看過基于@Provides方法的綁定再回來看這段。在使用基于@Provides方法綁定的過程中,如果方法中創建對象的過程很復雜,我們就會考慮,是不是可以把它獨立出來,形成一個專門作用的類。Guice提供了一個接口:
public interface Provider { T get(); }
實現這個接口,我們就會得到專門為了創建相應類型對象所需的類:public class DatabaseTransactionLogProvider implements Provider { private final Connection connection;@Inject public DatabaseTransactionLogProvider(Connection connection) { this.connection = connection; }
public TransactionLog get() { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setConnection(connection); return transactionLog; } }</pre>
這樣以來,我們就可以在configure方法中,使用toProvider方法來把一種類型綁定到具體的Provider類。當需要相應類型的對象時,Provider類就會調用其get方法獲取所需的對象。其實,個人感覺在configure方法中使用Provider綁定和直接寫@Provides方法所實現的功能是沒有差別的,不過使用Provider綁定會使代碼更清晰。而且當提供對象的方法中也需要有其他類型的依賴注入時,使用Provider綁定會是更好的選擇。
無目標綁定
無目標綁定是鏈接綁定的一種特例,在綁定的過程中不指明目標,如:bind(MyConcreteClass.class); bind(AnotherConcreteClass.class).in(Singleton.class);
如果使用注解綁定的話,就不能用無目標綁定,必須指定目標,即使目標是它自己。如:
bind(MyConcreteClass.class).annotatedWith(Names.named("foo")).to(MyConcreteClass.class); bind(AnotherConcreteClass.class).annotatedWith(Names.named("foo")).to(AnotherConcreteClass.class).in(Singleton.class);
無目標綁定,主要是用于與被@ImplementedBy 或者 @ProvidedBy修飾的類型一起用。如果無目標綁定的類型不是被@ImplementedBy 或者 @ProvidedBy修飾的話,該類型一定不能只提供有參數的構造函數,要么不提供構造函數,要么提供的構造函數中必須有無參構造函數。因為guice會默認去調用該類型的無參構造函數。
指定構造函數綁定
在configure方法中,將一種類型綁定到另外一種類型的過程中,指定目標類型用那種構造函數生成對象。public class BillingModule extends AbstractModule { @Override protected void configure() { try { bind(TransactionLog.class).toConstructor( DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class)); } catch (NoSuchMethodException e) { addError(e); } } }
這種綁定方式主要用于不方便用注解@Inject修飾目標類型的構造函數的時候。比如說目標類型是第三方提供的類型,或者說目標類型中有多個構造函數,并且可能會在不同情況采用不同的構造函數。
Built-in綁定
Built-in綁定指的是不用程序員去指定,Guice會自動去做的綁定。目前,Guice所支持的Built-in綁定只有對java.util.logging.Logger的綁定。個人感覺,所謂的Built-in綁定,只是在比較普遍的東西上為大家帶來方便的一種做法。@Singleton public class ConsoleTransactionLog implements TransactionLog {private final Logger logger;
@Inject public ConsoleTransactionLog(Logger logger) { this.logger = logger; }
public void logConnectException(UnreachableException e) { / the message is logged to the "ConsoleTransacitonLog" logger / logger.warning("Connect exception failed, " + e.getMessage()); }</pre>
在@Provides方法中進行綁定
當你只是需要在需要的時候,產生相應類型的對象的話,@Provides Methods是個不錯的選擇。方法返回的類型就是要綁定的類型。這樣當需要創建一個該類型的對象時,該provide方法會被調用,從而得到一個該類型的對象。public class BillingModule extends AbstractModule { @Override protected void configure() { ... }@Provides TransactionLog provideTransactionLog() { DatabaseTransactionLog transactionLog = new DatabaseTransactionLog(); transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza"); transactionLog.setThreadPoolSize(30); return transactionLog; } }</pre>
需要注意的是Provide方法必須被@Provides所修飾。同時,@Provides方法綁定方式是可以和上面提到的注解綁定混合使用的,如:@Provides @PayPal CreditCardProcessor providePayPalCreditCardProcessor( @Named("PayPal API key") String apiKey) { PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor(); processor.setApiKey(apiKey); return processor; }
這樣一來,只有被@PayPal修飾的CreditCardProcessor對象才會使用provide方法來創建對象,同時在父類型中進行綁定
@ImplementedBy
在定義父類型的時候,直接指定子類型的方式。
這種方式其實和鏈式綁定的效果是完全一樣的,只是聲明綁定的位置不同。
和鏈式綁定不同的是它們的優先級,@ImplementedBy實現的是一種default綁定,當同時存在@ImplementedBy和鏈式綁定時,鏈式綁定起作用。
@ImplementedBy(PayPalCreditCardProcessor.class) public interface CreditCardProcessor { ChargeResult charge(String amount, CreditCard creditCard) throws UnreachableException; }
等價于:
bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);
@ProvidedBy
在定義類型的時候直接指定子類型的Provider類。
@ProvidedBy(DatabaseTransactionLogProvider.class) public interface TransactionLog { void logConnectException(UnreachableException e); void logChargeResult(ChargeResult result); }
等價于:bind(TransactionLog.class) .toProvider(DatabaseTransactionLogProvider.class);
并且,和@ImplementedBy類似,@ProvidedBy的優先級也比較低,是一種默認實現,當@ProvidedBy和toProvider函數兩種綁定方式并存時,后者有效。在子類型中進行注入
構造函數注入
在構造函數綁定中,Guice要求目標類型要么有無參構造函數,要么有被@Inject注解修飾的構造函數。這樣,當需要創建該類型的對象時,Guice可以幫助進行相應的綁定,從而生成對象。在Guice創建對象的過程中,其實就是調用該類型被@Inject注解修飾的構造函數,如果沒要@Inject注解修飾的構造函數,則調用無參構造函數。在使用構造函數綁定時,無需再在Module中定義任何綁定關系。這里需要注意的是,Guice在創建對象的過程中,無法初始化該類型的內部類(除非內部類有static修飾符),因為內部類會有隱含的對外部類的引用,Guice無法處理。
public class PayPalCreditCardProcessor implements CreditCardProcessor { private final String apiKey;@Inject public PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) { this.apiKey = apiKey; }</pre>
屬性注入
屬性綁定的目的是告訴Guice,當創建該類型的對象時,哪些屬性也需要進行依賴注入。用一個例子來看:
首先,有一個接口:package guice.test;import com.google.inject.ImplementedBy;;
@ImplementedBy (SayHello.class) public interface Talk { public void sayHello(); }</pre>
該接口指明了它的實現類SayHellopackage guice.test;public class SayHello implements Talk{ @Override public void sayHello() { System.out.println("Say Hello!");
}
}</pre>
接下來就是屬性注入的例子:package guice.test;import com.google.inject.Inject;
public class FieldDI { @Inject private Talk bs;
public Talk getBs() { return bs; }
}</pre>
這里面,指明熟悉Talk類型的bs將會被注入。使用的例子是:package guice.test;import com.google.inject.Guice; import com.google.inject.Injector;
public class Test { public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule()); FieldDI fdi = injector.getInstance(FieldDI.class); fdi.getBs().sayHello(); }
}</pre>
如果我們沒有用@Inject修飾Talk bs的話,就會得到如下錯誤:Exception in thread "main" java.lang.NullPointerException如果我們用@Inject修飾Talk bs了,但是Talk本身沒有被@ImplementedBy修飾的話,會得到如下錯誤:
Exception in thread "main" com.google.inject.ConfigurationException: Guice configuration errors:1) No implementation for guice.test.Talk was bound. while locating guice.test.Talk for field at guice.test.FieldDI.bs(FieldDI.java:5) while locating guice.test.FieldDI
1 error at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1004) at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:961) at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1013) at guice.test.Test.main(Test.java:24)</pre>
另外,屬性注入的一種特例是注入provider。如:
public class RealBillingService implements BillingService { private final Provider processorProvider; private final Provider transactionLogProvider;@Inject public RealBillingService(Provider processorProvider, Provider transactionLogProvider) { this.processorProvider = processorProvider; this.transactionLogProvider = transactionLogProvider; }
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) { CreditCardProcessor processor = processorProvider.get(); TransactionLog transactionLog = transactionLogProvider.get();
/* use the processor and transaction log here */
} }</pre>
其中,Provider的定義如下:public interface Provider { T get(); }
Setter方法注入
有了上面的基礎我們再來看Setter注入就非常簡單了,只不過在setter方法上增加一個@Inject注解而已。 還是上面的例子,只是有一點修改:package guice.test;import com.google.inject.Inject;
public class FieldDI {
@Inject public void setBs(Talk bs) { this.bs = bs; } private Talk bs; public Talk getBs() { return bs; }
}</pre>
對象產生的Scopes
在默認情況下,每次通過Guice去請求對象時,都會得到一個新的對象,這種行為是通過Scopes去配置的。
Scope的配置使得我們重用對象的需求變得可能。目前,Guice支持的Scope有@Singleton, @SessionScoped, @RequestScoped。
同樣,程序員也可以自定義自己的Scope,本文不涉及自定義scope,如果有興趣,請參考:http://code.google.com/p/google-guice/wiki/CustomScopes
下面我們就以@Singleton舉例說明怎么來告訴Guice我們要以@Singleton的方式產生對象:1. 在定義子類型時聲明
@Singleton public class InMemoryTransactionLog implements TransactionLog { /* everything here should be threadsafe! */ }
2.在module的configure方法中做綁定時聲明bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);3.在module的Provides方法里聲明@Provides @Singleton TransactionLog provideTransactionLog() { ... }
這里有一點需要注意的是,如果發生下面的情況:
bind(Bar.class).to(Applebees.class).in(Singleton.class); bind(Grill.class).to(Applebees.class).in(Singleton.class);
這樣一共會生成2個Applebees對象,一個給Bar用,一個給Grill用。如果在上面的配置的情況下,還有下面的配置,bind(Applebees.class).in(Singleton.class);
這樣一來,Bar和Grill就會共享同樣的對象了。