Method Injection

在大多数应用程序场景中,容器中的大多数 bean 都为 singletons 。当单例 bean 需要与其他单例 bean 协作,或者非单例 bean 需要与其他非单例 bean 协作时,通常通过将一个 bean 定义为另一个 bean 的属性来处理该依赖项。当 bean 生命周期不同时,就会出现问题。假设单例 bean A 需要使用非单例(原型)bean B,也许在对 A 的每次方法调用中使用它。容器仅创建一次单例 bean A,因此只有一次设置属性的机会。容器无法在每次需要时都为 bean A 提供 bean B 的新实例。 一个解决方案是放弃某些控制反转。您可以通过 make bean A aware of the container,实现 ApplicationContextAware 接口,并在 making a getBean("B") call to the container 中每次需要 bean A 时请求(通常是新的)bean B 实例。以下示例演示了此方法:

  • Java

  • Kotlin

// 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;
	}
}
// Spring-API imports
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware

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

	private lateinit var applicationContext: ApplicationContext

	fun process(commandState: Map<*, *>): Any {
		// grab a new instance of the appropriate Command
		val command = createCommand()
		// set the state on the (hopefully brand new) Command instance
		command.state = commandState
		return command.execute()
	}

	// notice the Spring API dependency!
	protected fun createCommand() =
			applicationContext.getBean("command", Command::class.java)

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

前面的方法不可取,因为业务代码感知 Spring 框架并与其耦合。Spring IoC 容器的一种比较高级的功能——方法注入,让你可以干净地处理此用例。 你可以在https://spring.io/blog/2004/08/06/method-injection/[此博客文章]中阅读有关方法注入动机的更多内容。

Lookup Method Injection

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

  • 要使这种动态子类化生效,Spring Bean 容器子类的类不能是 final,并且被重写的函数也不能是 final

  • 单元测试具有 abstract 函数的类需要你自己对该类进行子类化,并提供 abstract 函数的存根实现。

  • 组件扫描还需要具体函数,该扫描要求具体类来选取。

  • 另一个主要限制是查询函数无法使用工厂函数,尤其是配置类中的 @Bean 函数,因为在这种情况下,容器不负责创建实例,因此无法在运行中生成一个子类。

在上一段代码片段中的 CommandManager 类的情况下,Spring 容器动态覆盖了 createCommand() 方法的实现。正如重新编写的示例所示,CommandManager 类没有任何 Spring 依赖关系:

  • Java

  • Kotlin

// 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();
}
// no more Spring imports!

abstract class CommandManager {

	fun process(commandState: Any): Any {
		// grab a new instance of the appropriate Command interface
		val command = createCommand()
		// set the state on the (hopefully brand new) Command instance
		command.state = commandState
		return command.execute()
	}

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

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

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

如果该方法是 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>

标识为 commandManager 的 bean 每当它需要 myCommand bean 的新实例时,它都会调用它自己的 createCommand() 方法。如果你实际上需要这样,就必须小心地将 myCommand bean 部署为原型。如果它是一个 singleton,则每次都会返回 myCommand bean 的同一个实例。

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

  • Java

  • Kotlin

public abstract class CommandManager {

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

	@Lookup("myCommand")
	protected abstract Command createCommand();
}
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

  • Kotlin

public abstract class CommandManager {

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

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

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

	@Lookup
	protected abstract fun createCommand(): Command
}

请注意,你通常应该使用具体的存根实现来声明此类带注释的查找方法,以便其与 Spring 的组件扫描规则兼容,其中默认会忽略抽象类。此限制不适用于显式注册或显式导入的 Bean 类。

访问不同作用域目标 bean 的另一种方法是 ObjectFactory/ Provider 注入点。参见 Scoped Beans as Dependencies。 您还可能发现 ServiceLocatorFactoryBean(在`org.springframework.beans.factory.config` 包中)很有用。

Arbitrary Method Replacement

比查询方法注入功能弱一些的方法注入形式是用另一个方法实现替换受管 Bean 中的任意方法。如果您现在不需要此功能,可以安全地跳过本节的剩余部分。

使用基于 XML 的配置元数据,您可以使用 replaced-method 元素用另一个元素替换现有方法实现,以获取已部署的 Bean。请考虑以下类,其中有一个名为 computeValue 的方法,我们想重写此方法:

  • Java

  • Kotlin

public class MyValueCalculator {

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

	// some other methods...
}
class MyValueCalculator {

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

	// some other methods...
}

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

  • Java

  • Kotlin

/**
 * 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 ...;
	}
}
/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
class ReplacementComputeValue : MethodReplacer {

	override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
		// get the input value, work with it, and return a computed result
		val input = args[0] as String;
		...
		return ...;
	}
}

用于部署原始类并指定方法重写的 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"/>

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

java.lang.String
String
Str

由于参数数量通常足以区分每种可能的选择,因此此快捷方式可以节省很多键入,因为它允许您仅键入与参数类型匹配的最短字符串。