跳到主要内容

方法注入

DeepSeek V3 中英对照 Method Injection

在大多数应用场景中,容器中的大多数 bean 都是 单例。当一个单例 bean 需要与另一个单例 bean 协作,或者一个非单例 bean 需要与另一个非单例 bean 协作时,通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。当 bean 的生命周期不同时,问题就出现了。假设单例 bean A 需要使用非单例(原型)bean B,可能是在 A 的每次方法调用时。容器只创建单例 bean A 一次,因此只有一次机会设置属性。容器无法在每次需要时都为 bean A 提供一个新的 bean B 实例。

一个解决方案是放弃一些控制反转。你可以通过实现 ApplicationContextAware 接口让 bean A 感知容器,并通过向容器发起 getBean("B") 调用来请求(通常是新的)bean B 实例,每当 bean A 需要它时。以下示例展示了这种方法:

package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
* A class that uses a stateful Command-style class to perform
* some processing.
*/
public class CommandManager implements ApplicationContextAware {

private ApplicationContext applicationContext;

public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}

public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
java

前面所述的情况并不理想,因为业务代码意识到了 Spring 框架并与之耦合。方法注入(Method Injection)是 Spring IoC 容器的一个较为高级的特性,它可以让您更干净地处理这种用例。

你可以在这篇博客文章中阅读更多关于方法注入的动机:这篇博客文章

查找方法注入

查找方法注入是容器的一种能力,它能够覆盖容器管理的 bean 上的方法,并返回容器中另一个命名 bean 的查找结果。查找通常涉及一个原型 bean,如前一节中描述的场景。Spring 框架通过使用 CGLIB 库的字节码生成来实现这种方法的注入,动态生成一个子类来覆盖该方法。

备注
  • 为了使这种动态子类化生效,Spring bean 容器子类化的类不能是 final 的,并且要重写的方法也不能是 final 的。

  • 对包含 abstract 方法的类进行单元测试时,需要你自己对类进行子类化,并提供 abstract 方法的存根实现。

  • 另一个关键限制是查找方法不适用于工厂方法,特别是配置类中的 @Bean 方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类。

在前面的代码片段中,CommandManager 类的情况下,Spring 容器动态地覆盖了 createCommand() 方法的实现。如重构后的示例所示,CommandManager 类没有任何 Spring 依赖:

public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}

protected abstract Command createCommand();
}
java

在这个示例中,CommandManager 类通过抽象方法 createCommand() 来创建命令对象,Spring 容器可以在运行时动态提供该方法的实现,而不需要在类中显式引入 Spring 依赖。

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
java

在包含要注入方法的客户端类中(在本例中为 CommandManager),要注入的方法需要满足以下形式的签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);
xml

如果方法是 abstract 的,动态生成的子类将实现该方法。否则,动态生成的子类将覆盖原始类中定义的具体方法。考虑以下示例:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>

<!-- commandManager uses myCommand prototype bean -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
xml

标识为 commandManager 的 bean 在需要 myCommand bean 的新实例时,会调用自己的 createCommand() 方法。如果确实需要,你必须小心地将 myCommand bean 部署为原型。如果它是 单例,则每次都会返回相同的 myCommand bean 实例。

或者,在基于注解的组件模型中,你可以通过 @Lookup 注解声明一个查找方法,如下例所示:

public abstract class CommandManager {

public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}

@Lookup("myCommand")
protected abstract Command createCommand();
}
java

或者,更符合习惯的做法是,您可以依赖于目标 bean 根据查找方法的声明返回类型进行解析:

public abstract class CommandManager {

public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}

@Lookup
protected abstract Command createCommand();
}
java
提示

访问不同作用域的目标 bean 的另一种方法是 ObjectFactory/Provider 注入点。请参阅 作为依赖项的作用域 Bean

你可能还会发现 ServiceLocatorFactoryBean(位于 org.springframework.beans.factory.config 包中)很有用。

任意方法替换

相较于查找方法注入,方法注入的一种不太有用的形式是能够用另一个方法实现替换托管 bean 中的任意方法。你可以安全地跳过本节其余部分,直到你真正需要此功能为止。

使用基于 XML 的配置元数据,你可以使用 replaced-method 元素来替换已部署 bean 的现有方法实现。考虑以下类,其中有一个名为 computeValue 的方法,我们想要覆盖它:

public class MyValueCalculator {

public String computeValue(String input) {
// some real code...
}

// some other methods...
}
java

一个实现了 org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义,如下例所示:

/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {

public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
java

要部署原始类并指定方法覆盖的 bean 定义将类似于以下示例:

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
xml

你可以在 <replaced-method/> 元素内使用一个或多个 <arg-type/> 元素来指示被重写方法的方法签名。只有在方法被重载并且类中存在多个变体时,参数的签名才是必要的。为了方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下所有内容都匹配 java.lang.String

java.lang.String
String
Str
java

由于参数的数量通常足以区分每个可能的选择,这种快捷方式可以节省大量的输入时间,因为它允许你只输入与参数类型匹配的最短字符串。