使用 Google Guice 进行依赖注入(2)

阿超 发表于 2009-08-21 15:50 | 来源: | 阅读 247 次

     经过上一章的学习 我展示了 @Inject 应用于构造函数的用法。当 Guice 找到注释时,它会挑选构造函数参数,并试图为每个参数找到一个配置绑定。这称为 构造函数注入。根据 Guice 的最佳实践指南,构造函数注入是询问依赖项的首选方式。但这不是唯一的方式。 显示了配置 FrogMan 类的另一种方式:

方法注入

<pre class="java">
public class FrogMan{  
  private Vehicle vehicle;  
  @Inject 
  public void setVehicle(Vehicle vehicle) {  
    this.vehicle = vehicle;  
  }  
//etc. …
</pre>
注意,我没有使用注入的构造函数,而是改用一个带有 @Inject 标记的方法。Guice 会在构造好 hero 之后立即调用此方法。Spring 框架的忠实用户可以将此方法视为 “setter 注入”。不过,Guice 只关心 @Inject;您可以任意命名这个方法,它可以带有多个参数。此方法可以是包保护的,也可以是私有方法。

如果您认为 Guice 访问私有方法不是很好,可以参见 ,其中 FrogMan 使用了字段注入:

. 字段注入

<pre class="java">
public class FrogMan {  
  @Inject private Vehicle vehicle;  
  public FrogMan(){}  
//etc. … 
</pre>
同样,所有 Guice 都只关心 @Inject 注释。字段注入查找注释的所有字段并试图注入相应的依赖项。

哪种方法最好
三个 FrogMan 版本都展示了相同的行为:Guice 在构建时注入相应的 Vehicle。不过,像 Guice 的作者一样,我更喜欢构造函数注入。下面简单分析这三种方式:

•构造函数注入 很简单。因为 Java 技术能保证构造函数调用,您不用担心出现未初始化的对象 — 不管是不是由 Guice 创建的。您还可以将字段标记为 final。
•字段注入 会影响可测试性,特别是将字段标记为 private 时。这破坏了使用 DI 的主要目的。应该尽量少使用字段注入。
•方法注入 在您不控制类的实例化时很有用。如果您有一个需要某些依赖项的超类,也可以使用方法注入(构造函数注入会使这种情况变得很复杂)。
选择实现
现在,假设应用程序中有多个 Vehicle。一样英勇的 Weasel Girl 无法驾驭 FrogMobile!同时,您不想在 WeaselCopter 上硬编码依赖项。  显示了 Weasel Girl 请求一种更快的传输模式:

. 使用注释请求某种特定的实现

<pre class="java">
@Inject 
public WeaselGirl(@Fast Vehicle vehicle) {  
  this.vehicle = vehicle;  
}
</pre>
在  中,HeroModule 使用绑定函数告诉 Guice WeaselCopter 是 “很快” 的:

. 告诉 Guice Module 中的相关注释

<pre class="java">
public class HeroModule implements Module {  
public void configure(Binder binder) {  
    binder.bind(Vehicle.class).to(FrogMobile.class);  
    binder.bind(Vehicle.class).annotatedWith(Fast.class).to(WeaselCopter.class);  
  }  

</pre>
注意,我选择了一个注释,描述我想以抽象形式描述的工具种类(@Fast),而不是与实现太接近的注释(@WeaselCopter)。如果您使用的注释将想要的实现描述得太精确,就让读者觉得创建一个隐式依赖项。如果使用 @WeaselCopter,而且 Weasel Girl 借用了 Wombat Rocket,就会对程序员阅读和调试代码造成混淆。

要创建 @Fast 注释,需要复制  中的模板:

. 复制粘贴这段代码以创建一个绑定注释

<pre class="java">
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.FIELD, ElementType.PARAMETER})  
@BindingAnnotation 
public @interface Fast {} 
</pre>
如果您编写了大量 BindingAnnotations,就会得到许多这样的小文件,每个文件只是注释名称不同。如果您觉得这很繁琐,或者需要执行快速的原型设计,可以考虑 Guice 的内置 @Named 注释,它接受一个字符串属性。  展示了这种替代方法:

. 使用 @Named 代替自定义注释

<pre class="java">
// in WeaselGirl  
@Inject 
public WeaselGirl(@Named("Fast") Vehicle vehicle) {  
  //…  
}  
// in HeroModule  
binder.bind(Vehicle.class)  
  .annotatedWith(Names.named("Fast")).to(WeaselCopter.class);
</pre>
这种方法是可行的,但由于名称只在字符串内有效,所以这不能利用编译时检查和自动补齐。总的来说,我更愿意自己编写注释。

如果您根本不想使用注释,怎么办?即使添加 @Fast 或 @Named("Fast") 都会使类在某种程度上影响配置本身。如果想知道如何解决这个问题,请接着阅读。

provider 方法
如果每次探险都派遣 Frog Man,您可能会厌烦。您喜欢在每个场景中出现的 hero 是随机的。但是,Guice 的默认绑定程序 API 不允许出现 “每次调用时将 Hero 类绑定到一个不同的实现” 这样的调用。不过,您可以 告诉 Guice 使用一种特殊的方法来创建每个新的 Hero。  显示了将一个新方法添加到 HeroModule 中,并用特殊的 @Provides 注释进行注释:

. 使用 provider 编写自定义创建逻辑

<pre class="java">
@Provides 
private Hero provideHero(FrogMan frogMan, WeaselGirl weaselGirl) {  
  if (Math.random() > . {  
    return frogMan;  
  }  
  return weaselGirl;  

</pre>
Guice 会自动发现具有 @Provides 注释的 Module 中的所有方法。根据 Hero 的返回类型,在您请求某个 hero 时,Guice 会进行计算,它应该调用 provider 方法来提供 hero。您可以为 provider 方法添加逻辑以构建对象并在缓存中查询它,或者通过其他方式获得它。provider 方法是将其他库集成到 Guice 模块中的很好方式。它们也是从 Guice 开始提供的新方法(Guice 中只编写自定义 provider 类,这比较乏味,而且更加繁琐。如果您已经决定使用 Guice 用户指南中有这种旧方法的文档,而且在本文随附的 示例代码 中,您可以找到一个自定义 provider)。

在  中,Guice 自动使用正确的参数注入 provider 方法。这意味着 Guice 将从它的绑定列表中找到 WeaselGirl 和 FrogMan,您无需在 provider 方法中手动构建它们。这演示了 “海龟背地球” 原则(海龟背地球,哪海龟下面是什么呢?是由另一只海龟背着,如此反复)。您依靠 Guice 来提供依赖项,即使是配置 Guice 模块本身。

请求一个 Provider 而不是一个依赖项
假设一个故事(Saga)中要有多个 hero。如果要求 Guice 注入一个 Hero,只会得到一个 hero。但如果您请求一个 “hero provider”,就可以根据需要创建任意多的 hero,如  所示:

. 注入 provider 来控制实例化

<pre class="java">
public class Saga {  
  private final Provider<Hero> heroProvider;  
  @Inject 
  public Saga(Provider<Hero> heroProvider) {  
    this.heroProvider = heroProvider;  
  }  
  public void start() throws IOException {  
    for (int i =  i <  i++) {  
      Hero hero = heroProvider.get();  
      hero.fightCrime();  
    }  
  }  
}
</pre>
提供者也可以推迟英雄的出场时间,直到传奇真正开始。如果英雄依赖于时间敏感或上下文敏感的数据,这就会很方便。

Provider 接口有一个方法:get<T>。要访问提供的对象,调用这个方法即可。每次有没有获取新对象以及对象如何配置取决于 Guice 是如何配置的(参阅下面的 作用域 部分,了解单实例对象和其他长生命周期对象的详细信息)。在本例中,Guice 使用 @Provides 方法,因为它是构建新 Hero 的注册方式。这意味着该传奇应该由任意三位英雄混合而成。

不要把提供者与 provider 方法弄混淆了(在 Guice 这两者更难区分开来)。尽管该 Saga 是从自定义 @Provides 方法中获得它的英雄,但您可以请求任意 Guice 实例化依赖项的一个 Provider。如果需要,可以根据  重新编写 FrogMan 的构造函数:

. 请求 Provider 而不是依赖项

<pre class="java">
@Inject 
public FrogMan(Provider<Vehicle> vehicleProvider) {  
  this.vehicle = vehicleProvider.get();  

</pre>
(注意您完全不用更改这个模块代码)。重新编写没有任何作用;只是说明您总是可以请求 Provider,而不用直接请求依赖项。

作用域
默认情况下,Guice 为每个请求的依赖项创建一个新实例。如果对象是轻量级的,这个策略可以很好地工作。但是,如果有一个创建开销很大的依赖项,就可能需要在几台客户机之间共享实例。在  中,HeroModule 将 HeavyWaterRefinery 作为一个单实例对象绑定:

. 将 HeavyWaterRefinery 绑定为一个单实例对象

<pre class="java">
public class HeroModule implements Module {  
  public void configure(Binder binder) {  
    //…  
    binder.bind(FuelSource.class)  
      .to(HeavyWaterRefinery.class).in(Scopes.SINGLETON);  
  }  

</pre>
这意味着 Guice 会一直保持 “提炼厂” 可用,只要另一个实例需要燃料源,Guice 就会注入相同 的 “提炼厂”。这避免了在应用程序中启动多个 “提炼厂”。

在选择作用域时,Guice 提供了一个选项。可以使用绑定程序配置它们,或者直接注释依赖项,如  所示:

. 改用注释选择作用域

<pre class="java">
@Singleton 
public class HeavyWaterRefinery implements FuelSource {…} 
</pre>
Guice 提供了超出范围的 Singleton 作用域,但它允许您定义自己的作用域(如果您愿意)。例如,Guice servlet 包提供了两个其他作用域:Request 和 Session,它们为 servlet 请求和 servlet 会话提供类的一个独特实例。

常量绑定和模块配置
HeavyWaterRefinery 需要一个许可密钥才能启动。Guice 可以绑定常量值和新实例。请查看 :

. 在模块中绑定常量值

<pre class="java">
public class HeavyWaterRefinery implements FuelSource {  
  @Inject 
  public HeavyWaterRefinery(@Named("LicenseKey") String key) {…}  
}  
// in HeroModule:  
binder.bind(String.class)  
  .annotatedWith(Names.named("LicenseKey")).toInstance("QWERTY"); 
  </pre>
这里有必要使用绑定注释,否则 Guice 将不能区分不同的 String。

注意,尽管前面不推荐使用,我还是选择使用 @Named 注释。因为我想显示  所示的代码:

. 使用属性文件配置模块

<pre class="java">
//In HeroModule:  
private void loadProperties(Binder binder) {  
  InputStream stream =  
    HeroModule.class.getResourceAsStream("/app.properties");  
  Properties appProperties = new Properties();  
  try {  
    appProperties.load(stream);  
    Names.bindProperties(binder, appProperties);  
  } catch (IOException e) {  
    // This is the preferred way to tell Guice something went wrong  
    binder.addError(e);  
  }  
}  
//In the file app.properties:  
LicenseKey=QWERTY 
</pre>
这段代码使用 Guice Names.bindProperties 实用函数,通过恰当的 @Named 注释将 app.properties 文件中的每个属性与一个常量绑定。这本身就很好,而且还显示了您可以使模块代码更复杂。如果喜欢,可以从数据库或 XML 文件加载绑定信息。模块是纯 Java 代码,这提供了很大的 灵活性。

结束语
Guice 主要概念小结:

•使用 @Inject 请求依赖项。
•将依赖项与 Module 中的实现绑定。
•使用 Injector 引导应用程序。
•使用 @Provides 方法增加灵活性。
需要了解的 Guice 知识还很多,但您应该先掌握这篇文章中讨论的内容。我建议下载它,以及本文的 示例代码。当然,您也可以创建自己的示例应用程序,这就更好了。通过示例深入了解概念但又不用考虑生产代码是很有意思的。如果要了解更多 Guice 高级功能(比如面向方面编程支持),建议您访问 参考资料 中的一些链接。

说到生产代码,DI 的一个缺点是它可能感染病毒。注入一个类后,它会导致注入下一个类,依此类推。不过这很好,因为 DI 使代码更好。另一方面,这需要大量重构现有代码。为了使工作易于管理,可以将 Guice Injector 存储在某处并直接调用它。应该将这当作一根临时需要的拐杖,但最后一定可以摆脱它。

Guice 即将推出。有一些功能我还没有讨论,它可以使模块的配置更简单,而且能支持更大、更复杂的配置方案。

我希望您会考虑将 Guice 添加到工具包中。根据我的经验,DI 对于实现灵活的可测试代码库特别有用。Guice 使 DI 变得简单而有趣。还有什么比容易编写的、灵活的、可测试的代码更好呢?

喜欢Java豆技术站点的文章,那就通过 RSS Feed 功能订阅阅读吧!

我要评论

*

* 绝不会泄露



返回首页 | 关于我们 | 联系我们 | 广告合作 | 网站地图 | 友情链接 | 版权声明 | 模板设计