Skip to content

方法注入 (Method Injection)

在大多数应用场景中,容器中的大多数 Bean 都是单例 (Singleton)。当一个单例 Bean 需要与另一个单例 Bean 协作,或者一个非单例 Bean 需要与另一个非单例 Bean 协作时,你通常通过将一个 Bean 定义为另一个 Bean 的属性来处理依赖关系。

当 Bean 的生命周期不同时,问题就出现了。假设单例 Bean A 需要使用非单例(原型/Prototype)Bean B,也许是在 A 的每次方法调用上都需要。容器仅创建一次单例 Bean A,因此只有一次机会来设置属性。容器无法在每次需要时都为 Bean A 提供 Bean B 的新实例。

一种解决方案是放弃部分的控制反转(IoC)。你可以通过实现 ApplicationContextAware 接口使 Bean A 感知容器,并在每次 Bean A 需要时向容器调用 getBean("B") 来请求(通常是新的)Bean B 实例。以下示例显示了这种方法:

java
package fiona.apple;

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

/**
 * 一个使用状态化的 Command 风格类来进行某些处理的类。
 */
public class CommandManager implements ApplicationContextAware {

	private ApplicationContext applicationContext;

	public Object process(Map commandState) {
		// 获取相应 Command 的新实例
		Command command = createCommand();
		// 在(希望是全新的)Command 实例上设置状态
		command.setState(commandState);
		return command.execute();
	}

	protected Command createCommand() {
		// 注意这里对 Spring API 的依赖!
		return this.applicationContext.getBean("command", Command.class);
	}

	public void setApplicationContext(
			ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
}
kotlin
package fiona.apple

import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware

// 一个使用状态化 Command 风格类执行处理的类
class CommandManager : ApplicationContextAware {

	private lateinit var applicationContext: ApplicationContext

	fun process(commandState: Map<*, *>): Any {
		// 获取相应 Command 的新实例
		val command = createCommand()
		// 在(希望是全新的)Command 实例上设置状态
		command.state = commandState
		return command.execute()
	}

	// 注意这里对 Spring API 的依赖!
	protected fun createCommand() =
			applicationContext.getBean("command", Command::class.java)

	override fun setApplicationContext(applicationContext: ApplicationContext) {
		this.applicationContext = applicationContext
	}
}

上述做法并不理想,因为业务代码感知并耦合到了 Spring 框架。方法注入(Method Injection)是 Spring IoC 容器的一个高级功能,它可以让你干净地处理这种用例。

查询方法注入 (Lookup Method Injection)

查询方法注入(Lookup method injection)是容器重写容器管理 Bean 上的方法并返回容器中另一个命名 Bean 的查询结果的能力。查询通常涉及原型(Prototype)Bean。Spring 框架通过使用 CGLIB 库生成的字节码动态生成覆盖该方法的子类来实现此方法注入。

注意

  • 为了使这种动态子类化工作,Spring Bean 容器进行子类化的类不能是 final,要覆盖的方法也不能是 final
  • 对具有 abstract 方法的类进行单元测试需要你自己对该类进行子类化,并提供 abstract 方法的桩(Stub)实现。
  • 另一个关键限制是,查询方法不适用于工厂方法,特别是配置类中的 @Bean 方法,因为在这种情况下,容器不负责创建实例,因此无法即时创建运行时生成的子类。

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

java
package fiona.apple;

// 不再有 Spring 导入!

public abstract class CommandManager {

	public Object process(Object commandState) {
		// 获取相应 Command 接口的新实例
		Command command = createCommand();
		// 在(希望是全新的)Command 实例上设置状态
		command.setState(commandState);
		return command.execute();
	}

	// 好的... 但是这个方法的实现在哪里?
	protected abstract Command createCommand();
}
kotlin
package fiona.apple

// 不再有 Spring 导入!

abstract class CommandManager {

	fun process(commandState: Any): Any {
		// 获取相应 Command 接口的新实例
		val command = createCommand()
		// 在(希望是全新的)Command 实例上设置状态
		command.state = commandState
		return command.execute()
	}

	// 好的... 但是这个方法的实现在哪里?
	protected abstract fun createCommand(): Command
}

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

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

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

xml
<!-- 一个部署为原型(非单例)的状态化 Bean -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
	<!-- 根据需要在此注入依赖项 -->
</bean>

<!-- commandManager 使用 myCommand 原型 Bean -->
<bean id="commandManager" class="fiona.apple.CommandManager">
	<lookup-method name="createCommand" bean="myCommand"/>
</bean>

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

或者,在基于注解的组件模型中,你可以通过 @Lookup 注解声明查询方法:

java
public abstract class CommandManager {

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

	@Lookup("myCommand")
	protected abstract Command createCommand();
}
kotlin
abstract class CommandManager {

	fun process(commandState: Any): Any {
		val command = createCommand()
		command.state = commandState
		return command.execute()
	}

	@Lookup("myCommand")
	protected abstract fun createCommand(): Command
}

或者,更惯用的做法是,你可以依靠目标 Bean 根据查询方法的声明返回类型进行解析:

java
@Lookup
protected abstract Command createCommand();

提示

访问不同作用域的目标 Bean 的另一种方法是 ObjectFactory/Provider 注入点。请参阅作为依赖项的作用域 Bean。你可能还会发现 ServiceLocatorFactoryBean(在 org.springframework.beans.factory.config 包中)也很有用。

任意方法替换 (Arbitrary Method Replacement)

与查询方法注入相比,另一种不太常用的方法注入形式是用另一种方法实现替换受管 Bean 中的任意方法。在实际需要此功能之前,你可以安全地跳过本节的其余部分。

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

java
public class MyValueCalculator {

	public String computeValue(String input) {
		// 某些真实代码...
	}

	// 其他方法...
}
kotlin
class MyValueCalculator {

	fun computeValue(input: String): String {
		// 某些真实代码...
	}

	// 其他方法...
}

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

java
/**
 * 用于覆盖 MyValueCalculator 中现有的
 * computeValue(String) 实现
 */
public class ReplacementComputeValue implements MethodReplacer {

	public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
		// 获取输入值,对其进行操作,并返回计算结果
		String input = (String) args[0];
		...
		return ...;
	}
}
kotlin
/**
 * 用于覆盖 MyValueCalculator 中现有的
 * computeValue(String) 实现
 */
class ReplacementComputeValue : MethodReplacer {

	override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
		// 获取输入值,对其进行操作,并返回计算结果
		val input = args[0] as String
		...
		return ...
	}
}

部署原始类并指定方法覆盖的 Bean 定义如下例所示:

xml
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
	<!-- 任意方法替换 -->
	<replaced-method name="computeValue" replacer="replacementComputeValue">
		<arg-type>String</arg-type>
	</replaced-method>
</bean>

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

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

  • java.lang.String
  • String
  • Str

补充教学 —— 解决“单例注入原型”的终极方案

1. 核心矛盾:生命周期不匹配 这是 Spring 面试中的高频考点。

  • 场景Service (单例) 依赖 Task (原型)。
  • 问题:由于 Service 只会被创建一次,它持有的 Task 引用也会被由于注入而固定死。即便 Task 配置了 scope="prototype",你在 Service 里多次调用这个 Task,它永远是最初注入的那个,不会变。

2. 为什么不推荐 ApplicationContextAware 虽然代码里手动 context.getBean() 能解决问题,但它带来了代码侵入性。你的业务代码里出现了 import org.springframework...,这意味着你的代码离开了 Spring 环境就跑不起来,违背了 POJO 的初衷。

3. Lookup 的魔法@Lookup 的本质是 Spring 帮你写了 getBean 的代码

  • 实现原理:Spring 会动态生成一个你的类的子类(利用 CGLIB),并帮你实现那个抽象方法。底层逻辑其实还是 context.getBean(),但你看不到,代码很干净。
  • 要求:类不能是 final,方法也不能是 final

4. 现代推荐:ObjectProvider<T> 虽然文档提到了 Lookup,但在现代 Spring Boot 开发中,更推荐使用 ObjectProvider

java
@Service
public class MyService {
    @Autowired
    private ObjectProvider<MyPrototypeBean> prototypeBeanProvider;

    public void doWork() {
        // 每次调用 getObject() 都会获取一个新的原型实例
        MyPrototypeBean bean = prototypeBeanProvider.getObject();
        bean.execute();
    }
}

这种方式不需要 abstract 类,也不需要 CGLIB 子类化,更符合现代编程习惯。

5. 总结:方法替换 (Method Replacement) 凉了吗? 是的。replaced-method 虽然看起来很黑科技,但在实际开发中几乎没人用。如果你想修改某个方法的行为,现代 Spring 更有 AOP (切面编程) 这种更优雅、更强大的武器。所以,这一块你只需要知道有这么回事即可,不需要深入研究。

Based on Spring Framework.