本文适合對依賴注入有相對了解的讀者,文章中對于部分名詞未作詳細解釋。對于沒有恰當的中文與之對應的英文内容,遂未翻譯
Guice簡介
Guice 簡介,本文中的内容也是參考該文檔完成,如有不一緻,以該文為準。
快速上手
作為示例,我們使用
BillingService
,它依賴于
CreditCardProcessor
和
TransactionLog
兩個接口。接下來我們看看如何使用Guice:
class BillingService {
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
@Inject
BillingService(CreditCardProcessor processor,
TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
...
}
}
我們将會把 PaypalCreditCardProcessor 和 DatabaseTransactionLog 注入BillingService。
Guice 使用
bindings
将類型和實作對應起來。
module
是特定的
bindings
的集合。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
/**
*這是告訴Guice,當遇到一個對象依賴于TransactionLog時,
*将DatabaseTransactionLog注入
*/
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
/**同上*/
bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
}
}
現在可以編寫main方法了:
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.
*/
BillingService billingService = injector.getInstance(BillingService.class);
...
}
大功告成!!!
Bindings
injector 的職責是确定系統中需要構造哪些對象,解析依賴,裝配對象(将被依賴的對象注入依賴的對象)。那麼如何指定依賴的解析方式,答案是使用 bindings 配置你的 injector
建立bindings
繼承
AbstractModule
重寫
configure
方法。在該方法中調用
bind()
便建立了一個binding。完成module之後,調用
Guice.createInjector()
,将module作為參數傳入,便可獲得一個injector對象。
Linked Bindings
Linked bindings 将一個實作綁定到一個類型。
下面例子将
DatabaseTransactionLog
綁定到接口
TransactionLog
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
}
}
綁定之後,當你調用
injector.getInstance(TransactionLog.class)
或當injector遇到一個對象依賴與
TransactionLog
,它便會使用
DatabaseTransactionLog
。連結可以建立于接口和其實作類,或者子類和父類之間。Linked bindings 可以鍊式使用。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);
}
}
在這種情況下,當一個對象依賴于
TransactionLog
,injector将會傳回一個
MySqlDatabaseTransactionLog
.
BindingAnnotations
Binding Annotations
有時候,你想要給特定的類型綁定多個實作。例如,你想給
CreditCardProcessor
同時綁定
PaypalCreditCardProcessor
和
CheckoutCreditCardProcessor
兩個實作. Guice 通過binding annotation滿足上述需求。注解和類型共同确定了一個唯一的binding。 在這兒,注解和類型構成了
Key
。
首先我們定義一個注解:
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 {}
然後使用我們定義的注解标示需要注入的類型。
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@PayPal CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
最後我們還需要建立相應的binding。
bind(CreditCardProcessor.class)
.annotatedWith(PayPal.class)
.to(PayPalCreditCardProcessor.class);
@Named
也并不是必須建立自己的注解,Guice提供了一個内建的注解
@Named
。用法如下:
public class RealBillingService implements BillingService {
@Inject
public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
TransactionLog transactionLog) {
...
}
bind(CreditCardProcessor.class)
.annotatedWith(Names.named("Checkout"))
.to(CheckoutCreditCardProcessor.class);
因為編譯器不能對String類型進行檢查,是以不建議使用
@Named
Instance Bindings
你可以将一個特定類型的執行個體綁定到該類型。
bind(String.class)
.annotatedWith(Names.named("JDBC URL"))
.toInstance("jdbc:mysql://localhost/pizza");
bind(Integer.class)
.annotatedWith(Names.named("login timeout seconds"))
.toInstance();
盡量避免給建立起來比較複雜的對象使用
.toInstance
方法,那樣會導緻應用啟動比較慢。可以使用
@Provides
代替該方法。
Provides Methods
當你需要編寫建立對象的代碼,使用
@Provides
方法。該方法隻能定義在module中。并且需要使用
@Provides
修飾。他的傳回值是一個對象。當Injector需要某個類型的執行個體時,便會調用相應的
@Provides
方法。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
...
}
@Provides
TransactionLog provideTransactionLog() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setJdbcUrl("jdbc:mysql://localhost/pizza");
transactionLog.setThreadPoolSize();
return transactionLog;
}
}
如果
@Provides
方法有binding annotation ,比如
@Paypal
或者
@Named("Checkout")
,Guice 也是可以的。所有的被依賴對象以參數形式傳入該方法即可。
@Provides @PayPal
CreditCardProcessor providePayPalCreditCardProcessor(
@Named("PayPal API key") String apiKey) {
PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
processor.setApiKey(apiKey);
return processor;
}
需要注意的是, Guice不允許
@Provides
方法抛出異常。
Provider Bindings
當
@Provides
方法比較複雜時,你也許會考慮将該方法轉移到一個單獨的類中。Provider類繼承Guice的
Provider
接口。
Provider
接口定義如下:
public interface Provider<T> {
T get();
}
我們的Provider實作類有自己的依賴,所有的依賴是通過被
@Inject
修飾的構造函數接收的。
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
private final Connection connection;
@Inject
public DatabaseTransactionLogProvider(Connection connection) {
this.connection = connection;
}
public TransactionLog get() {
DatabaseTransactionLog transactionLog = new DatabaseTransactionLog();
transactionLog.setConnection(connection);
return transactionLog;
}
}
綁定
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
bind(TransactionLog.class)
.toProvider(DatabaseTransactionLogProvider.class);
}
Untargeted Bindings
一些情況下,你需要建立bindings,但是卻不能指定具體的實作。這個對于被
@ImplementedBy
或者
@ProvidedBy
修飾的具體類或者類型特别有用。An untargetted binding informs the injector about a type, so it may prepare dependencies eagerly. Untargetted bindings have no to clause, like so:
bind(MyConcreteClass.class);
bind(AnotherConcreteClass.class).in(Singleton.class);
當指定binding annotation時,必須加上綁定的目标。
bind(MyConcreteClass.class)
.annotatedWith(Names.named("foo"))
.to(MyConcreteClass.class);
bind(AnotherConcreteClass.class)
.annotatedWith(Names.named("foo"))
.to(AnotherConcreteClass.class)
.in(Singleton.class);
Constructor Bindings
有時候, 我們需要綁定一個類型到任意的的構造函數。以下情況會有這種需求:
@Inject
注解無法被應用到目标構造函數。或者該類型是一個第三方類。或者該類型中有多個構造函數參與依賴注入。
針對這個,Guice 有
@toConstructor()
類型的綁定方式。
public class BillingModule extends AbstractModule {
@Override
protected void configure() {
try {
bind(TransactionLog.class).toConstructor(
DatabaseTransactionLog.class.getConstructor(DatabaseConnection.class));
} catch (NoSuchMethodException e) {
addError(e);
}
}
}
JustInTimeBindings
Just-in-time Bindings
當Injector需要一個類型的執行個體的時候,它需要一個binding。 如果這個binding在一個module中被建立,那麼這個binding是顯式binding,此時injector在每次需要該類型執行個體時,都使用該執行個體。但是如果Injector需要一個類型的執行個體,但是這個類型并沒有對應的顯式binding。此時injector會嘗試建立一個Just-in-time binding。也叫JIT binding或者隐式binding。
合格的構造函數
Guice會使用具體類型的可注入構造函數建立binding。可注入構造函數需要是非private,無參數的或者該構造函數被
@Inject
修飾。
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private final String apiKey;
@Inject
public PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
@ImplementedBy
告訴injector,該類型的預設實作類。
@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {
ChargeResult charge(String amount, CreditCard creditCard)
throws UnreachableException;
}
上述代碼和一下代碼是等價的:
bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);
如果某個類型同時使用
bind()
和
@ImplementedBy
,
bind()
會生效。
@ProvidedBy
告訴Injector,産生該類型的Provider類
@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {
void logConnectException(UnreachableException e);
void logChargeResult(ChargeResult result);
}
上述代碼等價于:
bind(TransactionLog.class)
.toProvider(DatabaseTransactionLogProvider.class);
如果同時使用
@ProvidedBy
和
bind()
,
bind()
會生效。
Scopes
預設情況下,Guice 每次都會傳回一個新建立的對象。不過這也是可以配置的,以便于我們重用執行個體。
配置Scopes
Guice 使用注解标示Scope。例如:
@Singleton
public class InMemoryTransactionLog implements TransactionLog {
/* everything here should be threadsafe! */
}
@Provides @Singleton
TransactionLog provideTransactionLog() {
...
}
上例中,
@Singleton
标示該類型隻能有一個執行個體。并且是線程安全的。
Scope也可以通過代碼來配置:
bind(TransactionLog.class).to(InMemoryTransactionLog.class).in(Singleton.class);
如果某個類型已經被注解标注了scope,同時還使用
bind()
方法配置了scope,那麼後者生效。如果一個類型已經被注解配置了scope而你不想那樣,你可以使用
bind()
覆寫。
預加載的單例
Injections
構造函數注入
這種情況下,需要用
@Inject
标注構造函數。構造函數同時需要将所依賴的類型作為其參數。通常情況下,都是将傳入的參數複制給類的final成員。
public class RealBillingService implements BillingService {
private final CreditCardProcessor processorProvider;
private final TransactionLog transactionLogProvider;
@Inject
public RealBillingService(CreditCardProcessor processorProvider,
TransactionLog transactionLogProvider) {
this.processorProvider = processorProvider;
this.transactionLogProvider = transactionLogProvider;
}
如果沒有
@Inject
标注的構造函數,Guice 會使用共有的午餐構造函數(如果存在)。
方法注入
這種情況下,需要使用
@Inject
标注方法,該方法的參數為需要注入的類型。
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private static final String DEFAULT_API_KEY = "development-use-only";
private String apiKey = DEFAULT_API_KEY;
@Inject
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
字段注入
這種情況下,需要使用
@Inject
标注字段。
public class DatabaseTransactionLogProvider implements Provider<TransactionLog> {
@Inject Connection connection;
public TransactionLog get() {
return new DatabaseTransactionLog(connection);
}
}
可選的注入
有時候,我們的依賴項不是必須的,如果系統中存在依賴項則注入,如果不存在,也不強制要求注入。這種情況在方法注入和字段注入中都是适用的。 啟用可選注入,隻需要使用
@Inejct(optional=true)
标注字段或方法即可。
public class PayPalCreditCardProcessor implements CreditCardProcessor {
private static final String SANDBOX_API_KEY = "development-use-only";
private String apiKey = SANDBOX_API_KEY;
@Inject(optional=true)
public void setApiKey(@Named("PayPal API key") String apiKey) {
this.apiKey = apiKey;
}
不過,如果混用可選注入和Just-in-time bindings,可能會産生奇怪的接口。例如:
@Inject(optional=true) Date launchDate;
上面代碼中的date總是會被成功注入即使沒有為他建立對應的顯示binding,因為它有無參構造函數,Guice會為他建立Just-in-time bindings。
On-demand注入
方法和字段注入可以用來初始化一個現存的執行個體。我們可以使用
Injector.injectMember()
API:
public static void main(String[] args) {
Injector injector = Guice.createInjector(...);
CreditCardProcessor creditCardProcessor = new PayPalCreditCardProcessor();
injector.injectMembers(creditCardProcessor);
AOP
Guice 支援方法攔截器。這裡直接看一個執行個體:
假如我們需要禁止在周末調用特定方法
為了标注我們在周末禁止調用的方法,我們定義一個注解類型:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface NotOnWeekends {}
然後使用該注解标注我們方法
public class RealBillingService implements BillingService {
@NotOnWeekends
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
...
}
}
現在我們定義我們的攔截器,我們的攔截器需要實作
org.aopalliance.intercept.MethodInterceptor
接口。
public class WeekendBlocker implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Calendar today = new GregorianCalendar();
if (today.getDisplayName(DAY_OF_WEEK, LONG, ENGLISH).startsWith("S")) {
throw new IllegalStateException(
invocation.getMethod().getName() + " not allowed on weekends!");
}
return invocation.proceed();
}
}
然後配置我們的攔截器
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
/**在這裡,我們比對所有的類,但是隻比對類中有NotOnWeekends的方法*/
bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class),
new WeekendBlocker());
}
}
所有工作就完成了。
注入攔截器
如果需要注入攔截器,使用 `requestInjection` API
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
WeekendBlocker weekendBlocker = new WeekendBlocker();
requestInjection(weekendBlocker);
bindInterceptor(Matchers.any(), Matchers.annotatedWith(NotOnWeekends.class),
weekendBlocker);
}
}
另外一種方式是使用 `Binder.getProvider`,将依賴的内容傳入攔截器的構造函數。
public class NotOnWeekendsModule extends AbstractModule {
protected void configure() {
bindInterceptor(any(),
annotatedWith(NotOnWeekends.class),
new WeekendBlocker(getProvider(Calendar.class)));
}
}
END
本人有意進一步閱讀Guice源碼,但是個人能力有限,如有感興趣的讀者,希望可以一起研究。