方法注入

工程 | Rod Johnson | 2004年8月6日 | ...

几个月前,在我还没有博客的日子里,CedricBob 就“Getter Injection”进行了一场讨论。

其基本概念是,IoC 容器可以在部署时覆盖托管对象上的抽象或具体方法。容器注入的是一个方法,例如 getter 方法,而不是像 Setter Injection 中那样注入引用或基本类型。碰巧的是,我当时已经在为 Spring 1.1 开发容器方法覆盖机制,该机制已在 Spring 1.1 RC1 中发布。这是一个有趣的想法,并且绝对是完整 IoC 容器的一部分。但是,我相信这个概念更普遍,需要一个更通用的名称。此外,它应该只用于范围相当狭窄的场景。

为什么要这样做?Cedric 的动机是 setter 方法“无用”,并且“在 Java 对象中拥有永远不会调用的方法是一种设计异味”。在他看来,对象中最重要的方法实际上是getter方法,这些方法通常返回在 setter 中保存的对象引用。因此,他建议让容器实现 getter 方法,并取消 setter 方法。在实践中,这意味着容器实际上将覆盖作为应用程序代码一部分定义的 getter 方法,否则无法使用它们。因此,容器将最终使用类似于 CMP 2.x 的机制来实现它(尽管希望任何相似之处都将到此结束)。

我并不认同“无用方法”的论点,因为 setter 方法被使用依赖注入的 IoC 容器调用,并且在没有任何容器的情况下在单元测试中调用。如果对象在容器外部使用,它们将由应用程序代码调用。此外,getter/setter 组合是建立默认值的一种好方法,以防您不选择配置要调用的一个或多个 setter:如果需要,setter 就在那里。虽然我理解 Cedric 的动机,但这里存在权衡:如果我们摆脱了所谓的无用 setter,我们就只剩下不完整的类了。如果 getter 是抽象的,我们将回到 CMP 2.x 测试场景,需要测试抽象对象。如果 getter 是具体的,我们就会经常编写在运行时将被覆盖的方法。在我看来,这才是真正的无用代码。(总的来说,我不喜欢覆盖具体方法,并在可能的情况下避免它。我认为我第一次在UML 参考手册中读到这个建议,它很有道理。)“Setter Injection”中也包含了一些魔法元素。如果我可以使用一个简单的 POJO,并且没有花哨的容器子类化,我更喜欢它。正如 Cedric 本人在去年 5 月的 TSSS 讨论小组中所说,“只有在科学失败时才使用魔法”。

我认为这个概念应该重命名为方法注入,并且它对其他一些不太常见的场景的价值要大得多。

我不会在使用依赖注入配置对象的典型情况下将其用作 Setter 或 Constructor Injection 的替代方案。Setter 方法和构造函数是普通的 Java 结构,在容器中工作得很好,但并不依赖于容器。这很好。IoC 容器提供的魔法方法会稍微增加对容器的依赖,尽管当然仍然可以在容器外部子类化对象,并且它们仍然只是 Java。

从本质上讲,我认为方法注入是在某些极端情况下替代子类化的一种方法,其中超类应该与容器依赖项隔离开来,并且容器比常规子类更容易实现必要的行为。相关方法不需要是 getter 方法(如 Setter Injection getter),尽管通常它是一个返回值的方法。

我认为有三种主要情况可以使用容器实现的方法

它们可以将容器依赖项移出应用程序代码。它们可以依赖于直到部署才知晓的基础设施。它们可以根据运行时环境自定义遗留代码的行为。但是,普通的旧子类化在这里也有意义。容器子类化也比常规子类化更具动态性。我们可以潜在地采用一个基类并以不同的方式部署它,而无需管理多个类的源代码。但是,由于它的魔法成分高于常规子类化、策略接口或各种替代方案,因此我认为不应过早地使用方法注入。

对我而言,方法注入的主要吸引力在于它是一种摆脱我曾经在使用 Spring 1.0 时必须承担的容器依赖项的方法,并且它将适用于任何支持“非单例”或“原型”对象概念的容器。(也就是说,一个容器,根据配置,您可以选择在请求时获取共享或新实例的 IoC 托管对象。)我喜欢使用 Spring,但我讨厌为了配置而不得不导入 Spring API。

导致我实现此功能的具体用例是,当通过 Spring 配置的一个“单例”对象需要创建非单例对象的实例时——例如,一个单线程、单次使用处理对象——但希望该对象使用依赖注入进行配置,而不是仅仅使用 new。例如,假设 ThreadSafeService 需要创建 SingleShotHelper 的实例,而 SingleShotHelper 本身也通过依赖注入进行配置。在 Spring 1.0.x 中,ThreadSafeService 需要实现 BeanFactoryAware 生命周期接口,保存 BeanFactory 引用并调用

(SingleShotHelper) beanFactory.getBean("singleShotHelper")

每次它需要创建一个帮助程序时。这工作得很好,测试起来并不难(BeanFactory 是一个简单的接口,因此很容易模拟),但它是一个 Spring 依赖项,理想情况下,可以使它更接近一个完全非侵入式框架。类型转换也略显不优雅,尽管没什么大不了的。

我通常在大概 10 个类中遇到这种情况的一个案例。我有时会重构它以提取一个方法,如下所示

protected SingleShotHelper createSingleShotHelper() { return (SingleShotHelper) context.getBean("singleShotHelper"); } 我现在可以子类化来实现这一点并将 Spring 依赖项保留在超类之外,但这似乎有点过分了。

这种方法是容器而不是应用程序开发人员实现的理想候选对象。它返回容器知道的对象;整个过程实际上可以用配置而不是代码更简洁地表达(当您允许使用保存 BeanFactory 引用的少量代码时)。

使用 Spring 1.1 中引入的新方法注入功能,可以使用抽象(或具体)方法,例如

protected abstract SingleShotHelper createSingleShotHelper();

并告诉容器在部署时覆盖该方法以从相同或父工厂返回特定 bean,如下所示

<lookup-method name="createSingleShotHelper" bean="singleShotHelper" >

这些方法可以是受保护的或公开的。可以覆盖任意数量的方法。<lookup-method> 元素可以像属性或 constructor-arg 元素一样在 bean 定义元素中使用。

我认为方法注入最引人注目的案例是返回容器管理的命名对象的查找结果。(当然,这并非 Spring 特有:任何容器都可以实现这一点。)查找通常是针对非单例 bean(用 Spring 的术语来说)。

这样,应用程序代码中就没有对 Spring 或任何其他 IoC 容器的依赖项。一个极端案例被关闭,而无需导入 Spring API。正如我所说,此功能直接受我正在参与的一个客户项目的需要驱动,并且在实践中已被证明是有用的。

查找方法可以与 Setter Injection 或 Constructor Injection 结合使用。它们不带参数,因此方法重载不是问题。

实现使用 CGLIB 对类进行子类化。(只有在类路径上存在 CGLIB 时才可用,以避免使 Spring 核心容器依赖于 CGLIB。)

Spring 更进一步,允许您为覆盖的方法定义任意行为——而不仅仅是 bean 查找。例如,您可能希望这样做,以使用基于运行时基础设施的通用行为——例如使用 Spring TransactionInterceptor 类进行事务回滚。(当然,通常应使用回滚规则来避免这种情况。)或者可能存在对通用覆盖行为的令人信服的案例——例如“如果存在活动事务,则返回事务性数据源 DS1,否则返回非事务性数据源 DS2”。同样,如果我们能够将这种逻辑隐藏在应用程序代码之外,那将是一次胜利。在这里,我们超出了纯“getter”的范围:例如,我们可以覆盖方法来发布事件。

通常有替代任意容器覆盖的方法,例如子类化该类并以常规方式覆盖该方法(科学,而不是魔法),或使用 AOP。在像示例中那样的 bean 查找的情况下,容器进行覆盖具有明显的优势,因为它消除了对 Spring API 的依赖。它在 XML 中描述起来也简单得多。对于更普遍的情况,有必要有一种方法来解决重载方法。

这已经比我计划的要长了——而且花了一些时间!——因此,如果有人感兴趣,我将在以后的文章中讨论 Spring 1.1 的任意覆盖机制(包括它如何解决重载方法)。我对那些不知疲倦的博主(如 Dion 和 Matt Raible)越来越敬佩,他们似乎每天要发 3 次博客。

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部