天天看點

Guice基本用法

本文适合對依賴注入有相對了解的讀者,文章中對于部分名詞未作詳細解釋。對于沒有恰當的中文與之對應的英文内容,遂未翻譯

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源碼,但是個人能力有限,如有感興趣的讀者,希望可以一起研究。