方法注入

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

几个月前,在我还没写博客的时候,CedricBob 曾讨论过“Getter 注入”。

基本概念是,IoC 容器可以在部署时覆盖受管理对象的抽象或具体方法。容器注入的是一个方法(例如 getter 方法),而不是像 Setter 注入那样注入引用或基本类型。碰巧的是,我当时正在为 Spring 1.1 开发一个容器方法覆盖机制,该机制后来在 Spring 1.1 RC1 中发布。这是一个有趣的概念,肯定是一个完整的 IoC 容器的一部分。然而,我认为这个概念更普遍,需要一个更通用的名称。此外,它应该只在相当狭窄的场景范围内使用。

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

我不太赞同“无用方法”的论点,因为 IoC 容器使用依赖注入 调用 setter 方法,并且在单元测试中,完全不使用容器也 调用它们。如果对象在容器外部使用,应用程序代码也会调用它们。此外,getter/setter 组合是一种很好的设置默认值的方式,以防你选择不配置一个或多个 setter 被调用:如果你需要,setter 就在那里。虽然我理解 Cedric 的动机,但这里有一个权衡:如果我们去除那些据说无用的 setter,剩下的就是不完整的类。如果 getter 是抽象的,我们就回到了 CMP 2.x 的测试场景,需要测试抽象对象。如果 getter 是具体的,我们就是在常规地编写那些将在运行时被覆盖的方法。在我看来,这才是真正的无用代码。(一般来说,我不太喜欢覆盖具体方法,并尽可能避免它。我想我第一次在 UML Reference Manual 中读到这个建议,它很有道理。)“setter 注入”中也存在一些魔法元素。如果我可以使用一个简单的 POJO,而无需复杂的容器子类化,我更喜欢那样。正如 Cedric 自己去年五月在 TSSS 的一次小组讨论中说得很好:“只有在科学失败时才使用魔法。”

我认为这个概念应该改名为 方法注入,并且它在其他一些不那么常见的场景中价值更大。

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

本质上,我认为方法注入在某些特殊情况下可以替代子类化,在这些情况下,超类应与容器依赖隔离,并且容器可以比常规子类更容易地实现必要的行为。相关方法不必是 getter 方法(如 Setter 注入中的 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" >

这些方法可以是 protected 或 public 的。可以覆盖任意数量的方法。<lookup-method> 元素可以像 property 或 constructor-arg 元素一样用在 bean 定义元素内部。

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

通过这种方式,应用程序代码中不再依赖于 Spring 或任何其他 IoC 容器。无需导入 Spring API,就可以解决一个特殊情况。正如我所说,这个功能是直接由我正在参与的一个客户项目中的需求推动的,并且在实践中证明了它的实用性。

Lookup 方法可以与 Setter 注入或构造函数注入结合使用。它们不接受参数,因此方法重载不是问题。

实现使用了 CGLIB 来对类进行子类化。(只有当 CGLIB 在 classpath 中时才可用,这是为了避免让 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 社区中所有即将举行的活动。

查看全部