方法注入
在大多数应用场景中,容器中的大多数bean都是单例。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常是通过将其中一个bean定义为另一个bean的属性来处理这种依赖关系。然而,当这两个bean的生命周期不同时,就会出现问题。假设单例bean A需要在每次调用其方法时使用非单例(原型)bean B,那么容器只会创建单例bean A一次,因此也就只有一次机会来设置它的属性。容器无法在每次需要bean B时都为bean A提供一个新的实例。
一种解决办法是放弃部分控制反转(inversion of control)。你可以通过实现ApplicationContextAware接口来让bean A“意识到”容器的存在(../factory-nature.md#beans-factory-aware),并且每当bean A需要bean B时,就可以通过向容器调用getBean("B")来获取(通常是一个新的)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框架耦合的。方法注入(Method Injection)是Spring IoC容器中较为高级的功能之一,它能够让你干净利落地处理这种用例。
查找方法注入
查找方法注入(Lookup Method Injection)是容器的一种能力,它能够覆盖容器管理的Bean上的方法,并返回容器中另一个同名Bean的查找结果。这种查找通常涉及原型Bean(prototype bean),如前一节所描述的场景所示。Spring框架通过使用CGLIB库生成字节码来动态地生成一个子类,从而实现这种方法注入,该子类会覆盖原有的方法。
- 为了使这种动态子类化能够正常工作,Spring Bean 容器所继承的类不能是
final的,同样,需要被重写的方法也不能是final的。 - 对于包含
abstract方法的类进行单元测试时,你需要自己继承该类,并提供该abstract方法的模拟实现(stub)。 - 另一个关键限制是,查找方法(lookup methods)无法与工厂方法(factory methods)一起使用,特别是在配置类中的
@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 部署为原型(prototype)模式。如果它是单例(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
}
另一种访问不同作用域目标bean的方法是使用ObjectFactory/Provider注入点。请参阅将作用域bean作为依赖项。
您也可能发现ServiceLocatorFactoryBean(位于org.springframework.beans.factory.config包中)会很有用。
随意方法替换
与查找方法注入(lookup method injection)相比,一种效果较差的方法注入形式是能够用另一种方法实现来替换托管Bean(managed 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
因为参数的数量通常足以区分每一个可能的选项,所以这种快捷方式可以节省大量的输入工作,因为你只需要输入与参数类型匹配的最短字符串即可。