天天看点

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源码,但是个人能力有限,如有感兴趣的读者,希望可以一起研究。