方法注入

在大多数应用场景中,容器中的大多数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 需要时都会这样。以下示例展示了这种方法:

  • Java

  • Kotlin

package fiona.apple;

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

/**
 * 使用有状态的 Command-style 类执行一些处理的类。
 */
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;
	}
}
package fiona.apple

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

// 使用有状态的 Command-style 类执行一些处理
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 Framework 耦合。方法注入是 Spring IoC 容器的一项相对高级的功能,可以让您干净地处理这种用例。

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

查找方法注入

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

  • 为使此动态子类化工作,Spring bean容器子类化的类不能是final,要被覆盖的方法也不能是final

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

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

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

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

  • Java

  • Kotlin

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();
}
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)中,要注入的方法需要以下形式的签名:

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

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

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

<!-- commandProcessor使用statefulCommandHelper -->
<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) {
		// 一些真实的代码...
	}

	// 其他一些方法...
}
class MyValueCalculator {

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

	// 其他一些方法...
}

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

  • Java

  • Kotlin

/**
 * 用于覆盖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 ...;
	}
}
/**
 * 用于覆盖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定义将类似于以下示例:

<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

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