guice依赖注入

    技术2024-01-22  116

    Guice是一个依赖项注入 (DI)框架。 多年来,我建议开发人员使用DI,因为它可以提高可维护性,可测试性和灵活性。 通过观察工程师对Guice的React,我了解到,说服程序员采用新技术的最佳方法就是使其变得非常容易。 Guice使DI变得非常简单,因此,这种做法在Google上得到了普及。 我希望通过使您轻松学习Guice来继续本文的内容。

    游览,而不是辩论

    Guice紧随一系列突破性的DI框架之后。 (有问题的框架之一PicoContainer的人们在页面上解释了历史和相互关系;请参阅参考资料 。)Guice的到来引发了人们对哪种框架更好以及是否还需要另一个DI框架的评论。 与任何技术选择一样,每个库都有其优缺点。 我认为Guice带来了一些新的东西,但是我将在本文中继续介绍Guice的功能,而不是增加辩论。 (您可以在网上搜索“ guice vs spring”,以进行一些生动的讨论。)

    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领域随附的注释感到不安,或者无法使用它们(例如,正在创建第三方类),则提供程序方法可以很好地解决您的问题。 因为您的提供者方法位于模块中,所以您可以对其进行批注,而不必担心源的其余部分会看到批注。 例如,清单16包含BadgerBoy (第三方英雄)的提供者方法: @Provides @Inject private BadgerBoy provideBadgerBoy(WeaselCopter copter) { return new BadgerBoy(copter); }

    使用此提供者方法,您可以使用Guice来配置BadgerBoy ,甚至可以选择他需要的Vehicle ,而完全不需要更改BadgerBoy 。 这意味着如果您不想使用@Inject或绑定注解,例如@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); } }
    单身人士是邪恶的吗?

    如果在Web上搜索“单身人士是邪恶的”,您会发现很多关于单身人士的博客文章,文章和言论。 事实证明,“应用程序单例”和“ JVM单例”之间是有区别的(这是评论者反对的那种)。 JVM单例通过使用语言技巧并强制客户端使用静态引用对其进行引用,从而实现了其“单一性”。 另一方面,应用程序单例不执行此策略。 另一层(例如Guice)强制每个应用程序存在该类的一个实例。 客户端代码不知道该规则,这使设计变得灵活并且易于编写测试。

    这意味着,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

    Processed: 0.010, SQL: 10