方法注入

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

一个解决方案是放弃一些控制反转。您可以使 Bean A 了解容器,方法是实现ApplicationContextAware接口,并通过对容器进行getBean("B")调用,在每次 Bean A 需要它时请求(通常是新的)Bean B 实例。以下示例显示了这种方法

  • Java

  • Kotlin

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;
	}
}
package fiona.apple

// 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 容器的一个稍微高级的功能,它允许您干净地处理此用例。

您可以在此博文中阅读更多关于方法注入动机的相关信息。

查找方法注入

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

  • 为了使这种动态子类化工作,Spring Bean 容器子类化的类不能是final,要覆盖的方法也不能是final

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

  • 具体方法对于组件扫描也是必要的,组件扫描需要具体类来获取。

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

在前面代码片段中的CommandManager类的情况下,Spring 容器会动态覆盖createCommand()方法的实现。正如修改后的示例所示,CommandManager类没有任何 Spring 依赖关系

  • Java

  • Kotlin

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();
}
package fiona.apple

// 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 部署为原型,如果这确实是需要的。如果它是单例,则每次都会返回相同的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注入点。请参阅作用域 Bean 作为依赖项

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

任意方法替换

与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一个方法实现替换托管 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

由于参数的数量通常足以区分每个可能的选项,因此此快捷方式可以通过让您只键入与参数类型匹配的最短字符串来节省大量输入。