Guice是一个依赖项注入 (DI)框架。 多年来,我建议开发人员使用DI,因为它可以提高可维护性,可测试性和灵活性。 通过观察工程师对Guice的React,我了解到,说服程序员采用新技术的最佳方法就是使其变得非常容易。 Guice使DI变得非常简单,因此,这种做法在Google上得到了普及。 我希望通过使您轻松学习Guice来继续本文的内容。
Guice 2.0 Beta
在我撰写本文时,Guice团队正在努力开发Guice 2.0,并有望在2008年底之前发布。早期测试版发布在Google Code下载网站上(请参阅参考资料 )。 这是个好消息,因为Guice团队添加了一些功能,这些功能将使您的Guice代码更易于使用和理解。 该Beta缺少一些功能,可以使其成为最终版本,但它既稳定又高质量。 实际上,Google在生产软件中使用beta版本。 我建议您也这样做。 我专门为Guice 2.0写了这篇文章,涵盖了一些新的Guice功能以及对1.0版本中已过时的功能的掩盖。 Guice团队向我保证,我涵盖的功能在当前Beta和最终版本之间不会更改。
如果您已经了解了DI,并且知道为什么想要一个框架来帮助您,那么可以跳到带有Guice的基本注入部分。 否则,请继续阅读以了解DI的好处。
DI的情况
我将从一个例子开始。 假设我正在编写一个超级英雄应用程序,并且正在实现一个名为Frog Man的英雄。 清单1包含代码以及我的第一个测试。 (我希望我不需要说服您编写单元测试的价值。)
清单1.一个基本的英雄和他的测试
public class FrogMan {
private FrogMobile vehicle = new FrogMobile();
public FrogMan() {}
// crime fighting logic goes here...
}
public class FrogManTest extends TestCase {
public void testFrogManFightsCrime() {
FrogMan hero = new FrogMan();
hero.fightCrime();
//make some assertions...
}
}
在尝试运行测试之前,一切似乎都很好,随后清单2中出现了异常:
清单2.依赖关系可能很麻烦
java.lang.RuntimeException: Refinery startup failure.
at HeavyWaterRefinery.<init>(HeavyWaterRefinery.java:6)
at FrogMobile.<init>(FrogMobile.java:5)
at FrogMan.<init>(FrogMan.java:8)
at FrogManTest.testFrogManFightsCrime(FrogManTest.java:10)
似乎FrogMobile
构造了HeavyWaterRefinery
,好吧,我们只能说无法在测试中构造其中之一。 当然,我可以在生产中这样做,但是没有人会给我第二张炼油厂许可证,仅用于测试。 在现实生活中,您不可能精制氧化氘,但是您可能依赖于远程服务器和强大的数据库。 原理是相同的:这些依赖关系很难启动并且与之交互的速度很慢,并且它们使您的测试失败的次数比应该的多。
输入DI
为了避免这个问题,可以创建一个接口(例如Vehicle
), FrogMan
类接受Vehicle
作为构造函数参数,如清单3所示:
清单3.依赖接口,并注入它们
public class FrogMan {
private Vehicle vehicle;
public FrogMan(Vehicle vehicle) {
this.vehicle = vehicle;
}
// crime fighting logic goes here...
}
这个成语是DI的本质-让您的类通过对接口的引用而不是构造它们(或使用静态引用)来接受它们的依赖关系。 清单4显示了DI如何使您的测试更加容易:
清单4.您的测试可以使用模拟而不是麻烦的依赖项
static class MockVehicle implements Vehicle {
boolean didZoom;
public String zoom() {
this.didZoom = true;
return "Mock Vehicle Zoomed.";
}
}
public void testFrogManFightsCrime() {
MockVehicle mockVehicle = new MockVehicle();
FrogMan hero = new FrogMan(mockVehicle);
hero.fightCrime();
assertTrue(mockVehicle.didZoom);
// other assertions
}
此测试使用手写的模拟对象替换FrogMobile
。 DI不仅使测试免于痛苦的炼厂启动成本,而且使测试不了解FrogMobile
。 它所需要的只是Vehicle
界面。 除了简化测试之外,DI还可以帮助您提高代码的整体模块化和可维护性。 现在,如果您想将FrogMobile
切换为FrogBarge
,则无需修改FrogMan
。 FrogMan
依赖的只是接口。
但是有一个陷阱。 如果您是我的第一次阅读DI一样的话,您会想到:“太好了,现在所有FrogMan
的调用者都必须了解FrogMobile
(以及炼油厂和炼油厂的依存关系,依此类推... )。” 但是,如果那是真的,DI永远不会流行。 您可以编写工厂来管理对象及其依赖关系的创建,而不必强迫调用者承担负担。
工厂是框架的用武之地。工厂需要大量乏味,重复的代码。 在最好的情况下,他们使程序作者(和读者)烦恼,而在最坏的情况下,由于不便,他们永远都不会写。 Guice和其他DI框架用作您配置以构建对象的灵活“超级工厂”。 配置框架比编写自己的工厂要容易得多。 结果,程序员以DI风格编写了更多代码。 随后会有更多的测试,更好的代码和满意的程序员。
Guice基础注射
我希望我已经使您相信DI可以为您的设计增加价值,并且使用框架可以使您的生活更加轻松。 让我们从@Inject
批注和模块开始深入Guice。
告诉Guice您想要您的课程@Inject
-ed
在Guice上, FrogMan
和FrogMan
之间的唯一区别是@Inject
。 清单5显示了带有注释的FrogMan
的构造函数:
清单5. FrogMan
已被@Inject
编辑
@Inject
public FrogMan(Vehicle vehicle) {
this.vehicle = vehicle;
}
一些工程师不喜欢在类中添加@Inject
的想法。 他们希望类完全不了解DI框架。 这是一个合理的观点,但我对此并不信服。 随着依赖关系的发展,注释非常温和。 @Inject
标记仅在您要求Guice构造类时才有意义。 如果不要求Guice创建FrogMan
,则注释对代码的行为没有影响。 注释提供了一个很好的线索,Guice将参与该类的构建。 但是,使用它确实需要源级别的访问。 如果注释困扰您,或者您使用的是Guice创建不受控制的源的对象,则Guice具有备用机制(请参阅本文后面的提供程序方法的其他用法侧边栏)。
告诉Guice您想要哪个依赖项
现在Guice知道您的英雄需要一辆Vehicle
,因此它需要知道要提供哪Vehicle
。 清单6包含一个Module
:一个特殊的类,用于告诉Guice哪些实现与哪些接口一起使用:
清单6. HeroModule
将Vehicle
绑定到FrogMobile
public class HeroModule implements Module {
public void configure(Binder binder) {
binder.bind(Vehicle.class).to(FrogMobile.class);
}
}
模块是具有单一方法的接口。 Guice传递给模块的Binder
使您可以告诉Guice如何构造对象。 活页夹API形成了特定于域的语言 (请参阅参考资料 )。 这种迷你语言使您可以编写表达代码,例如bind(X).to(Y).in(Z)
。 在我们继续的过程中,您将看到有关活页夹可以做什么的更多示例。 每次对bind
调用都会创建一个binding ,而绑定的集合就是Guice用来解决注入请求的东西。
带Injector
自举
接下来,使用Injector
类引导Guice。 通常,您想在程序的早期就创建注射器。 这样,Guice可以为您创建大多数对象。 清单7包含一个示例主程序,该主程序使用Injector
启动了英雄般的冒险:
清单7使用Injector
引导您的应用程序
public class Adventure {
public static void main(String[] args){
Injector injector = Guice.createInjector(new HeroModule());
FrogMan hero = injector.getInstance(FrogMan.class);
hero.fightCrime();
}
}
要获取注射器,请在Guice类上调用createInjector
。 您向createInjector
传递了用于配置自身的模块列表。 (此示例只有一个,但是您可以添加一个配置邪恶的VillainModule
。)一旦拥有注入器,您就可以使用getInstance
请求它的对象,并传递您想要返回的.class
。 (精明的读者会注意到,您不需要向Guice讲述FrogMan
。事实证明,如果您要一个具体的类,并且它具有@Inject
构造函数或公共无参数构造函数,则Guice无需调用即可创建它bind
。)
这是让Guice构造对象的第一种方法:明确询问。 但是,您不想在自举程序之外执行此操作。 更好,更简单的方法是让Guice注入依赖项以及依赖项的依赖项,依此类推。 (俗话说,“一直都是乌龟”;请参阅参考资料 )。 乍一看似乎令人不安,但过一会儿您就会习惯了。 作为示例,清单8显示了注入了FrogMobile
的FuelSource
:
清单8. FrogMobile
接受FuelSource
@Inject
public FrogMobile(FuelSource fuelSource){
this.fuelSource = fuelSource;
}
这意味着,即使您的应用程序仅与喷油器进行了一次交互,当您检索FrogMan
,Guice FrogMan
构造一个FuelSource
, FrogMobile
,然后最终是FrogMan
。
当然,您并不总是有机会控制应用程序的main
例程。 例如,许多Web框架自动构造“动作”,“模板”或其他可以作为起点的东西。 通常,您可以找到一个填充Guice的位置,或者使用框架的插件或您自己的一些手写代码。 (例如,Guice项目已经发布了Struts 2的插件,该插件使Guice可以配置Struts动作;请参阅参考资料 。)
其他形式的注射
到目前为止,我已经展示了@Inject
应用于构造函数。 当找到该注释时,Guice将选择构造函数参数,并尝试为每个参数找到已配置的绑定。 这称为构造函数注入 。 根据Guice最佳做法指南,构造函数注入是询问您的依赖项的首选方法。 但这不是唯一的方法。 清单9显示了另一种配置FrogMan
类的方法:
清单9.方法注入
public class FrogMan{
private Vehicle vehicle;
@Inject
public void setVehicle(Vehicle vehicle) {
this.vehicle = vehicle;
}
//etc. ...
请注意,我摆脱了注入的构造函数,而是使用@Inject
标记了一个方法。 Guice构建我的英雄后立即调用此方法。 Spring框架的支持者可能将其视为“注入注入”。 但是,Guice只关心@Inject
; 您的方法可以命名为任意名称,并且可以使用多个参数。 它也可以是受软件包保护的或私有的。
如果您认为Guice决定访问私有方法的决定似乎很麻烦,请等到清单10中的FrogMan
使用字段注入 :
清单10.字段注入
public class FrogMan {
@Inject private Vehicle vehicle;
public FrogMan(){}
//etc. ...
同样,Guice只关心@Inject
批注。 它会找到您注释的任何字段,并尝试注入适当的依赖项。
哪一个最好?
FrogMan
所有三个版本FrogMan
表现出相同的行为:Guice在构造时会注入适当的Vehicle
。 但是,我喜欢构造器注入,就像Guice的作者一样。 以下是这三种样式的快速分析:
- 构造函数注入很简单。 因为Java技术保证了构造函数的调用,所以您不必担心对象以未初始化的状态到达,无论Guice是否创建它们。 您也可以将字段标记为
final
。 - 字段注入会损害可测试性,尤其是如果您将字段标记为
private
。 这违反了DI的主要目标之一。 仅应在非常有限的情况下使用场注入。 - 如果您不控制类的实例化,则方法注入会很有用。 如果您有需要某些依赖项的超类,则也可以使用它。 (构造函数注入使这一点变得困难。)
选择实施
因此,假设您的应用程序中有不止一辆Vehicle
。 同样英雄的黄鼠狼女孩也不会驾驶FrogMobile
! 同时,您不想对WeaselCopter
的依赖项进行硬编码。 Guice通过让您注释依赖项来解决此问题。 清单11显示了Weasel Girl请求更快的运输方式:
清单11.使用注释请求特定的实现
@Inject
public WeaselGirl(@Fast Vehicle vehicle) {
this.vehicle = vehicle;
}
在清单12中, HeroModule
使用绑定器告诉Guice WeaselCopter
是“快速的”:
清单12.向Guice讲解Module
注释
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);
}
}
请注意,我选择了一个注释,该注释以抽象的方式( @Fast
)描述了我想要的车辆种类,而不是与实现紧密联系的车辆( @WeaselCopter
)。 如果使用的注释过于精确地描述了预期的实现,则会在读者的脑海中产生隐式的依赖关系。 如果使用@WeaselCopter
而Weasel Girl借用了Wombat Rocket,则可能会使程序员阅读或调试代码感到困惑。
要创建@Fast
批注,您需要复制清单13中的样板:
清单13.复制粘贴此代码以创建绑定注释
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@BindingAnnotation
public @interface Fast {}
如果您编写了很多BindingAnnotations
,那么您将得到很多这样的小文件,每个小文件的区别仅在于注释的名称。 如果您发现这确实很烦人或想要快速进行原型制作,可以考虑使用Guice的内置@Named
批注,该批注接受字符串属性。 清单14展示了这种选择:
清单14.使用@Named
代替自定义注释
// in WeaselGirl
@Inject
public WeaselGirl(@Named("Fast") Vehicle vehicle) {
//...
}
// in HeroModule
binder.bind(Vehicle.class)
.annotatedWith(Names.named("Fast")).to(WeaselCopter.class);
这可行,但是因为名称位于字符串中,所以您放弃了编译时检查和自动补全的好处。 总体而言,我宁愿编写自己的注释。
如果您根本不想使用注释怎么办? 甚至添加@Fast
或@Named("Fast")
使您的类对自己进行配置负有部分责任。 如果那麻烦您,请继续阅读。
提供者方法
您已经厌倦了在每次冒险中都派青蛙人。 您想要每个新的逃生英雄随机一名。 但是,Guice的默认活页夹API不允许进行类似“每次调用将Hero
类绑定到不同的实现”之类的调用。 但是,您可以告诉Guice使用一种特殊的方法来创建每个新Hero
。 清单15显示了添加到HeroModule
的新方法,并带有特殊的@Provides
注释:
清单15.使用提供者编写自定义创建逻辑
@Provides
private Hero provideHero(FrogMan frogMan, WeaselGirl weaselGirl) {
if (Math.random() > .5) {
return frogMan;
}
return weaselGirl;
}
Guice会自动发现Module
中带有@Provides
批注的所有方法。 根据Hero
的返回类型,可以得出结论,当您请求英雄时,应调用此方法来提供它。 您可以使用逻辑填充提供程序方法来构造对象,随机选择对象,从缓存中查找它或通过其他方式获取它。 提供程序方法是将其他库集成到Guice模块中的绝佳方法。 从Guice 2.0开始,它们也是新的。 (Guice 1.0的方法是编写自定义提供程序类,这些类笨拙且冗长。如果您决定使用Guice 1.0,则用户指南中有旧方法的文档,本文提供的示例代码中也有自定义您可以查看的提供商。)
Guice自动使用正确的参数注入清单15中的provider方法。 这意味着Guice将从绑定列表中找到WeaselGirl
和FrogMan
,而无需在provider方法中手动构造它们。 这说明了“从头到尾都是乌龟”的原理。 即使您正在配置Guice模块本身,也要依靠Guice提供依赖性。
要求Provider
而不是依赖项
假设您要在一个故事中包含多个英雄-一个Saga
。 如果您要求Guice注入Hero
,那么您只会得到一个。 但是,如果您要求“英雄提供者”,则可以根据需要创建任意数量的英雄,如清单17所示:
清单17.注入提供者以控制实例化
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 = 0; i < 3; i++) {
Hero hero = heroProvider.get();
hero.fightCrime();
}
}
}
提供者还可以让您延迟英雄的检索,直到传奇真正开始。 如果英雄依赖于时间或上下文敏感的数据,这将很方便。
Provider
接口具有一种方法: get<T>
。 要访问提供的对象,只需调用该方法。 是否每次都获得一个新对象,以及如何配置该对象,取决于Guice的配置方式。 (有关单例和其他长期存在的对象的详细信息,请参见“ 作用域”的下一部分。)在这种情况下,Guice使用@Provides
方法,因为这是构造新Hero
的注册方法。 这意味着传奇应该由三个随机英雄组成。
提供程序不应与提供程序方法混淆。 (在Guice 1.0中,很难区分它们。)尽管Saga
从您的自定义@Provides
方法获得了英雄,但您可以要求Provider
任何具有Guice实例化依赖性的Provider
。 如果需要,可以根据清单18重写FrogMan
的构造函数:
清单18.您可以要求Provider
而不是依赖项
@Inject
public FrogMan(Provider<Vehicle> vehicleProvider) {
this.vehicle = vehicleProvider.get();
}
(请注意,您根本不需要更改模块代码。)此重写没有任何用途;它只适用于某些用途。 它仅说明您可以始终请求Provider
而不是直接依赖。
范围
默认情况下,Guice为您要求的每个依赖关系创建一个新实例。 如果您的对象是轻量级的,则此策略将很好地为您服务。 但是,如果创建开销很大,则可能需要在多个客户端之间共享一个实例。 在清单19中, HeroModule
以单例方式绑定HeavyWaterRefinery
:
清单19. HeavyWaterRefinery
绑定为一个单例
public class HeroModule implements Module {
public void configure(Binder binder) {
//...
binder.bind(FuelSource.class)
.to(HeavyWaterRefinery.class).in(Scopes.SINGLETON);
}
}
这意味着,Guice将保持精炼厂周围,并且每当另一个实例需要燃料来源时,Guice都将注入同一精炼厂。 这样可以防止在应用程序中启动多个炼油厂。
选择范围时,Guice为您提供了一个选项。 您可以使用活页夹配置它们,也可以直接注释依赖项,如清单20所示:
清单20.选择带有注释的范围
@Singleton
public class HeavyWaterRefinery implements FuelSource {...}
Guice开箱即用提供了Singleton
范围,但是如果您愿意,它可以让您定义自己的范围。 例如,Guice servlet包提供了两个附加范围: Request
和Session
,它们为每个servlet请求和servlet会话提供了类的唯一实例。
常量绑定和模块配置
HeavyWaterRefinery
需要许可证密钥才能启动。 事实证明,Guice可以绑定常量值和新实例。 查看清单21:
清单21.在模块中绑定常量值
public class HeavyWaterRefinery implements FuelSource {
@Inject
public HeavyWaterRefinery(@Named("LicenseKey") String key) {...}
}
// in HeroModule:
binder.bind(String.class)
.annotatedWith(Names.named("LicenseKey")).toInstance("QWERTY");
绑定批注在这里是必需的,因为否则,Guice无法区分不同的String
。
请注意,尽管较早提出了建议,但我还是选择使用@Named
注释。 这是因为我想展示清单22中的代码:
清单22.使用属性文件配置模块
//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=QWERTY1234
此代码使用Guice Names.bindProperties
实用程序函数将app.properties文件中的每个属性绑定到带有正确@Named
批注的常量。 这本身很酷,它还显示了如何使模块代码任意复杂。 如果愿意,可以从数据库或XML文件加载绑定信息。 模块是纯Java代码,因此具有很大的灵活性。
接下来是什么?
总结Guice的主要概念:
- 您使用
@Inject
要求依赖项。 - 您将依赖项绑定到
Module
的实现。 - 您可以使用
Injector
引导。 - 您可以使用
@Provides
方法增加灵活性。
关于Guice的知识还很多,但是您应该可以深入了解我在本文中介绍的主题。 我建议下载它以及本文的示例代码 ,然后进行尝试。 或者更好的是,创建自己的示例应用程序。 不用担心生产代码就可以玩这些概念,这很有趣。 如果您想了解更多关于Guice的高级功能(如它的面向方面编程支持),我建议以下一些链接的相关主题 。
说到生产代码,DI的一个缺点是它可能会产生病毒。 一旦注入一个类,它将导致注入下一个和下一个。 这可能很好,因为DI使您的代码更好。 另一方面,它可能导致现有代码的大量重构。 为了使工作易于管理,您可以将Guice Injector
存放在某个地方并直接调用它。 您应该将其视为拐杖,这是必要的,但从长远来看,您想做些没有的事情。
Guice 2.0应该会很快推出。 我没有介绍的某些功能将使配置模块和支持更大,更复杂的配置方案变得更加容易。 您可以单击“ 相关主题”中的链接以了解即将推出的功能。
我希望您会考虑将Guice添加到您的工具箱中。 以我的经验,DI对于灵活,可测试的代码库至关重要。 Guice使DI轻松甚至有趣。 有什么能比编写爆炸的灵活可测试代码更好的呢?
翻译自: https://www.ibm.com/developerworks/java/library/j-guice/index.html