Guice(二) Guice中的绑定

翻译自官方文档,能力有限,如有缺漏,还望指正。

1.绑定(Bindings)

注射器(injector)的主要作用是搜集对象依赖图。当我们需要创建一个类型的对象时,他就会找出绑定类,然后会解决依赖的关系,并将所有的关系联系到一起。为了帮助解决依赖之间的关系,我们需要给injector指定绑定关系。

1.1 创建绑定关系

创建bind,我们只需要实现AbstractModule接口,然后重写configure方法即可。在这个方法体中,我们通过调用bind方法就可以实现绑定了。这些方法会进行类型检查,所以当我们参数有问题的时候,编译就会报错。我们创建了自定义的module之后就可以将其作为参数传递给Guice.creatInjector()来创建一个injector了。

绑定方式也有很多种:

  • linked bindings
  • instance bindings
  • @Provides methods
  • provider bindings
  • constructor bindings
  • untargetted bindings

1.2 其他的绑定

除了普通的绑定之外,我们还可以指定内嵌(built-in)绑定的injector。当有一个创建请求到达,但是为找到依赖项时,它就会尝试创建一个即时绑定(just-in-time)。

2. Linked bindings

Linked Bindings 会将一种类型与它的实现绑定。下面的例子,就是将TransactionLog接口绑定到了它的实现类 DatabaseTransactionLog上了:

public class BillingModule extends AbstractModule {
  @Override 
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
  }
}

如此,当我们调用injector.getInstance(TransactionLog.class)时,或者有类依赖于TransactionLog时,它就会创建DatabaseTransactionLog。它就是把一种类型绑定到了它的子类或者实现类上。我们甚至可以把这个实现类DatabaseTransactionLog绑定到它的子类上:

bind(DatabaseTransactionLog.class).to(MySqlDatabaseTransactionLog.class);

Link 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对象。

3. Bindings Annotations

通常情况下,我们的一种类型可能绑定了多个实现或者子类。例如,当你需要使用信用卡消费时,你用的可能是PayPal的卡,也可能是Google的卡。在这种情况下你就需要使用不同的processor 实现类了。为了做到这一点,guice支持注解绑定。注解加上类型组合在一起就是唯一的标识符了。这部分称之为key

自定义注解

定义一个绑定注解,需要在代码中导入相应的库,当然现在的IDE都有自动导入功能了,这个应该没什么问题。

package example.pizza;

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 {}

我们没必要去关心这些元注解,但是如果你真的想要知道的话,可以看下面:

  • @BindingAnnotation:这个注解是告诉Guice,这个是个绑定注解。这样,在后面如果这个类型没有对应多个绑定注解,那么Guice就会报错;
  • @Target({FIELD, PARAMETER, METHOD}): 这个注解可以防止@PayPal在没用的地方被意外引用。
  • @Retention(RUNTIME):让这个注解只在运行时有效。

通过绑定注解,接下来我们就可以通过注解来注入参数了:

public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@PayPal CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }

最后我们通过注解把类型和对应的实现类绑定起来。在bind()方法后调用annotateWith(PayPal.class)即可,如下:

 bind(CreditCardProcessor.class)
        .annotatedWith(PayPal.class)
        .to(PayPalCreditCardProcessor.class);

通过自定义注解实现绑定的过程主要有三步:

  1. 定义注解
  2. 绑定注解
  3. 注解注入

@Name注解

Guice也提供了一个内嵌的注解@Name,这个注解通过一个字段指定绑定的实现类:

public class RealBillingService implements BillingService {

  @Inject
  public RealBillingService(@Named("Checkout") CreditCardProcessor processor,
      TransactionLog transactionLog) {
    ...
  }

在需要注入的地方我们直接通过Names.name就可以指定实现类了:

 bind(CreditCardProsssscessor.class)
        .annotatedWith(Names.named("Checkout"))
        .to(CheckoutCreditCardProcessor.class);

因为编译器无法对字符串做类型检查,所以使用这个Name注解可能不太安全。最好还是通过定义自定义注解来绑定对象,这样可以保证类型安全。

4. Instance Bindings

在Guice中我们可以直接将一种类型绑定到它的对象实例上。这个通常只用于那些自身没有依赖项的对象,例如某些比较特殊的值对象:

 bind(String.class)
        .annotatedWith(Names.named("JDBC URL"))
        .toInstance("jdbc:mysql://localhost/pizza");
    bind(Integer.class)
        .annotatedWith(Names.named("login timeout seconds"))
        .toInstance(10);

避免使用.toInstance创建复杂对象,这会导致我们的程序在启动时比较耗性能。可以使用@Provides方法代替这种方式。

5. @Provides Methods

当需要创建通过代码创建对象时,我们可以使用一个@Provides方法。这个方法必须在Module中绑定,而且必须使用@Provides注解。这个方法返回一个限定类型的对象。当注射器(Injector)需要创建一个这种类型的对象时,它就会调用这个方法。

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;
  }
}

我们也可以给这个方法添加自定义注解,或者@Named注解,这样我们就可以像使用注解绑定那样来使用这个方法创建对象了。我们也可以在这个方法中使用值对象绑定:

  @Provides @PayPal
  CreditCardProcessor providePayPalCreditCardProcessor(
      @Named("PayPal API key") String apiKey) {
    PayPalCreditCardProcessor processor = new PayPalCreditCardProcessor();
    processor.setApiKey(apiKey);
    return processor;
  }

Provides方法不允许抛出异常,如果需要抛出异常可以考虑使用ThrowingProviders extension ——@CheckedProvides 方法 。

6. Provider Bindings

当我们需要使用很多的@Provides方法,而且它们的实现都比较复杂的时候,我们可能就需要考虑把它们各自迁移到一个特定的类中了。provider类需要实现Guice的Provider接口,然后实现一个简单的get方法:

public interface Provider<T> {
  T get();
}

我们自己实现的provider类可以通过构造器上使用@Inject注解注入自己的依赖。下面的例子就是通过实现Provider接口,定义了一个返回复杂类型的方法:

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;
  }
}

最后我们通过.toProvider来绑定provider:

public class BillingModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(TransactionLog.class)
        .toProvider(DatabaseTransactionLogProvider.class);
  }

为了保证代码质量和功能,当我们的provide方法比较复杂的时候,最后保证测试通过。

跟provide方法一样,Provider中的方法也不允许抛出异常,如果需要抛出异常可以考虑使用ThrowingProviders extension ——@CheckedProvides 方法 。

7. Untargeted Bindings

有些时候我可能没法把依赖绑定到一些具体的目标上,这个时候@ImplementedBy或者@ProvidedBy类型注解就比较有用了。非目标绑定会将绑定类型信的相关信息通知给injector,这样的绑定不会使用to方法,具体的使用方法如下:

   bind(MyConcreteClass.class);
   //通知injector这是一个单例对象
   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);

8. Constructor Bindings

Guice 3.0版本新特性

有些时候我们需要把一种类型绑定到任意的构造器上。这个通常出现在@Inject注解无法应用于目标构造器的情况:要么因为它是一个第三方类,要么是因为它有多个构造器参与了依赖注入。@Provides方法提供了最佳的解决方案。通过显式调用我们自己的对象构造方法,我们就可以得到相应的类型了。但是这种方法有个缺陷:手动的构造的对象无法参与到AOP中。

为了解决这个问题,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);
    }
  }
}

在这个例子中,DatabaseTransactionLog 类应该得有一个只接受一个DatabaseConnection类型参数的构造器。这个构造器不需要@Inject注解。Guice会调用它的构造器来适配对应的绑定。

每个toConstructor()绑定都是独立的。如果我们将一种单例类型绑定在多个构造器上,那么每个构造器都是创建出它们对应的对象。

9. Bult-in Bindings

除了显示绑定和即时绑定之外,Guice也内嵌了一些其他的绑定。

Loggers

Guice专门为java.util.logging.Logger内嵌了一个绑定,这样可以节省一些我们的开发时间。这个绑定会自动把logger的名字设置成logger被注入的类名。

@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());
  }

Injector

在我们的框架代码中,有的时候直到代码运行时我们才知道哪一种类型是我们需要的。在这种特殊情况下,我们就需要注入injector了。需要注意的是,注入injector的代码不会自行记录它的依赖关系,所以我们需要谨慎地使用这种方式。

Providers

Guice可以为所有感知到类型注入一个Provider。在Injectiong Providers中可以了解实现的细节。

TypeLiterals

Guice对其注入的所有类型都具有完整的类型信息。如果我们需要参数化这些类型,我们可以注入一个TypeLiteral<T>。这样我们就可以通过一些反射机制获取这些元素的类型了。

The Stage

Guice支持不同的阶段枚举类型,这样就可以区分开发环境和生产环境了。

MembersInjectors

当我们绑定providers或者扩展一些extensions的时候,我们可能需要Guice为我们写的对象注入依赖。想要做到这一点,只要添加一个MemberInjector<T>(其中T是我们自己的对象类型),然后通过调用membersInjector.injectMembers(myNewObject)就可以了。

10. Just-in-time Bindings

当injector需要一种类型的对象实例时,它就需要指定绑定。在modules中的绑定,我们称之为显式绑定,而我们想用的时候就可以通过这些modules创建injector。当我们需要的一个类型没有通过显示绑定到实现类上时,injector就会尝试创建一个即使绑定(Just-In_Time buding),这也被称之为JIT绑定隐式绑定

Eligible Constructor

Guice可以通过类型可注入的构造器为具体的类型穿件绑定。这个要么是一个无私有字段的空参构造器,要么是一个有@Inject注解的构造器:

public class PayPalCreditCardProcessor implements CreditCardProcessor {
  private final String apiKey;

  @Inject
  public PayPalCreditCardProcessor(@Named("PayPal API key") String apiKey) {
    this.apiKey = apiKey;
  }

一些内嵌的类,除非它们static修饰的,否则Guice不会构造这些类。内部类对其无法注入的封闭类有隐式引用。

@ImplementedBy

这个注解可以告诉injector,接口默认的实现类是哪个。@ImplementedBy就跟Link binding差不多,当我们没有通过其他方式指定和注入的时候,默认注入的就是这个注解指定的实现类:

@ImplementedBy(PayPalCreditCardProcessor.class)
public interface CreditCardProcessor {
  ChargeResult charge(String amount, CreditCard creditCard)
      throws UnreachableException;
}

上面这个实现通过Link binding操作就是:

 bind(CreditCardProcessor.class).to(PayPalCreditCardProcessor.class);

如果一个类型同时使用了bind()方法和@ImplementedBy注解。那么注解指定的默认实现类会被bind()方法指定的实现类覆盖。我们在使用@ImplementedBy的时候还是需要小心一点的;它为接口添加了一个运行时的依赖绑定。

@ProvidedBy

@ProvidedBy可以告诉injector生产实例对象的Provider:

@ProvidedBy(DatabaseTransactionLogProvider.class)
public interface TransactionLog {
  void logConnectException(UnreachableException e);
  void logChargeResult(ChargeResult result);
}

这个跟.toProvider()效果是一样的:

 bind(TransactionLog.class)
        .toProvider(DatabaseTransactionLogProvider.class);

@ImplementedBy一样,当我们同时使用注解和方法绑定时,注解会被方法覆盖掉。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270