1.4.6.方法注入
在大多数应用程序场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将一个bean定义为另一个bean的属性来处理依赖关系。当bean的生命周期不同时,就会出现一个问题。假设单例Bean A需要使用非单例(原型)Bean B,容器只创建一次单例Bean A,因此只获得一次设置属性的机会。容器不能在每次需要bean B时向bean A提供一个新的bean B实例。
解决办法是放弃一些控制反转。您可以通过实现ApplicationContextAware接口使bean了解容器,每次调用bean A需要执行getBean("B") 来请求(通常是新的)bean B实例。下面的示例显示了这种方法:
//一个使用有状态命令样式类执行某些处理的类
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// 获取适当命令的新实例
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
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;
}
}
前面的内容是不可取的,因为业务代码了解并耦合到Spring框架。方法注入是Spring IOC容器的一个高级特性,它允许您清晰地处理这个用例。
查找方法注入
查找方法注入是重写bean上的方法并返回另一个命名bean的查找结果的能力。查找通常涉及一个原型bean,如前一节中描述的场景中所示。Spring框架通过使用cglib库中的字节码生成来动态生成覆盖该方法的子类来实现该方法注入。
为了使这个动态子类工作,Spring Bean容器子类不能是final类,要重写的方法也不能是final类。
对具有抽象方法的类进行单元测试,需要您自己对类进行子类化,并提供抽象方法的实现。
组件扫描还需要具体的方法,这需要具体的类别来获取。
另一个关键限制是查找方法不适用于工厂方法,尤其不适用于配置类中的@Bean方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类。
对于前面代码段中的CommandManager类,Spring容器会动态重写createCommand()方法的实现。CommandManager类没有任何Spring依赖项,正如重新编写的示例所示:
package fiona.apple;
//没有任何Spring引入!
public abstract class CommandManager {
public Object process(Object commandState) {
// 获取适当命令接口的新实例
Command command = createCommand();
// 在(希望是全新的)命令实例上设置状态
command.setState(commandState);
return command.execute();
}
// 好了... 但是这个方法的实现在哪里?
protected abstract Command createCommand();
}
在包含要注入的方法的客户机类(本例中为CommandManager)中,要注入的方法需要以下形式的签名:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是抽象的,则动态生成的子类实现该方法。否则,动态生成的子类将覆盖在原始类中定义的具体方法。请考虑以下示例:
<!-- 作为原型部署的有状态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>
无论何时需要一个myCommand bean的新实例, 标识为commandManager的Bean需要调用自己的createCommand()方法。如果实际上需要这样做,那么必须小心地将myCommand bean部署为原型。如果它是单例的,则每次返回相同的myCommand bean实例。
或者,在基于注解的组件模型中,可以通过 @Lookup 注解声明查找方法,如下例所示:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
或者,更习惯地说,您可以依赖目标bean根据lookup方法声明的返回类型进行解析:
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
注意,通常应该用具体的存根实现声明这种带注解的查找方法,以便它们与Spring的组件扫描规则兼容,默认情况下抽象类会被忽略。此限制不适用于显式注册或显式导入的bean类。
访问不同作用域目标bean的另一种方法是ObjectFactory/Provider注入点。将作用域bean视为依赖项。
您还可能发现ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包中)非常有用。
任意方法替换
与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一个方法实现替换托管bean中的任意方法。您可以安全地跳过本节的其余部分,直到实际需要此功能为止。
对于基于XML,可以使用替换的方法元素为部署的bean替换现有的方法实现。考虑下面的类,它有一个我们想要重写的名为computeValue的方法:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
一个实现org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义,如下例所示:
/**
* 用于重写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 ...;
}
}
部署原始类并指定方法重写的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
因为参数的数量经常足以区分每个可能的选项,所以通过只允许键入与参数类型匹配的最短字符串,此快捷方式可以节省大量的键入内容。