参考:Method Injection :: Spring Framework
Spring默认支持两种bean的作用域,singleton和prototype,使用@Scope
可以指定bean的作用域,默认值为singleton,比如在一个Springboot构建的web项目中,controller层中的bean都是singleton。singleton即单例,在Spring IoC容器中只存在一个实例;prototype意味着一个bean可以存在多个实例,每当这个bean被注入的时候,都会创建一个新的实例。
关于作用域的详细介绍可参考:Spring--Bean的作用域
1. 提出问题
当一个singleton bean需要注入多个prototype bean实例时,可能会产生一个问题:singleton bean只会初始化一次,只有在初始化的时候才会去解析依赖项并注入它,所以只会有唯一的prototype bean实例被注入到singleton bean中。
要想给singleton bean注入多个prototype bean实例,就需要放弃一些控制反转:通过实现ApplicationContextAware
,让singleton bean感知到容器,通过调用getBean("beanName")
去获取一个新的prototype bean实例。
这种情况下,prototype bean不会在singleton bean初始化的时候被注入,而是在运行时通过getBean
方法创建prototype bean实例,每次调用该方法都会创建一个新的实例。
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;
}
}
但是,不建议使用上面的方法,因为这让业务代码与Spring Framework相耦合。方法注入和Spring IoC容器的高级特性提供了更合适的解决方法。
2. 查找方法注入
容器可以重写由它管理的bean的方法,并返回容器中的另一个bean。Spring Framework使用了CGLIB库的字节码生成技术,动态生成重写了查找方法(Lookup Method)的子类。这个查找方法实际上是由Spring控制的,通过查找方法可以在运行时从IoC容器中获取bean实例。
需要动态生成子类的bean不能使用
final
修饰(类定义和需要重写的方法)查找方法无法和工厂方法一起使用,尤其是
@Bean
方法
@Lookup
标注的方法将被视为查找方法。下面的代码中,Spring容器会创建CommandManager
的子类,并重写createCommand()
的实现方法。这个子类会作为bean被注入到其他组件中。
@Component
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract Command createCommand();
}
一个可能的Command
类定义:
@Component
@Scope("prototype")
public class Command {
private Object state;
public void setState(Object commandState) {
this.state = commandState;
}
public int execute() {
System.out.println(this + ": " + state);
return 0;
}
}
查找方法的签名需要遵循以下格式:
[abstract] theMethodName(no-arguments);
如果查找方法是
abstract
,动态生成的子类将实现该方法;否则,动态生成的子类将覆盖父类中的方法。
测试代码如下:
@Autowired
private CommandManager commandManager;
@Test
void testCommandManager() {
System.out.println(commandManager);
for (int i = 0; i < 7; i++) {
commandManager.process("cmd" + i);
}
}
从运行结果中可以看出,commandManager
是通过CGLIB创建的类,而每次调用createCommand
方法都将创建一个新的Command
实例:
请注意,你通常应该使用具体的简化实现来声明此类带注解的查找方法,以便它们与 Spring 的组件扫描规则(默认情况下抽象类被忽略)兼容。此限制不适用于显式注册或显式导入的 bean 类。
另一种访问不同作用域中的bean的方式是
ObjectFactory
/Provider
注入点。请查阅:Scoped Beans as Dependencies
org.springframework.beans.factory.config
包下的ServiceLocatorFactoryBean
或许也行。
3. 替代任意方法
Spring可以重写由容器管理的bean中的任意方法,但相比于查找方法,这种方式显得没那么有用。
下面的类中,computeValue
方法将会被重写:
public class MyValueCalculator {
public String computeValue(String input) {
return input;
}
// some other methods...
}
实现了org.springframework.beans.factory.support.MethodReplacer
接口的类ReplacementComputeValue
将提供computeValue
的重写方法:
public class ReplacementComputeValue implements MethodReplacer {
@Override
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
if ("computeValue".equals(m.getName()) && args.length == 1 && args[0] instanceof String) {
return "replaced " + args[0];
} else {
throw new IllegalArgumentException("Unable to reimplement method " + m.getName());
}
}
}
XML配置如下(我没有找到相应的基于注解的配置):
String
标签中,
java.lang.String
,String
和Str
是等价的。
在标签中可以使用多个
标签来描述需要被重写方法的签名。
中的参数需要与重写方法的签名相一致,否则将无法正常匹配,比如,如果将
String
注释掉,computeValue
方法将不会被替代。
在上述案例中,computeValue
方法签名中的参数String
不是必需的。如果某个类中存在多个需要被重写的同名方法,参数可以用来区分不同的方法,而上述案例中的方法参数并没有实际意义。
测试代码如下:
@Autowired
private MyValueCalculator valueCalculator;
@Test
void testArbitraryMethodReplacement() {
System.out.println(valueCalculator.computeValue("test"));
}
从运行结果可以看出,reimplement
方法替换了computeValue
方法。