likes
comments
collection
share

Spring--跨作用域的依赖注入方案:方法注入

作者站长头像
站长
· 阅读数 18

参考:Method Injection :: Spring Framework

当一个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容器的高级特性提供了更合适的解决方法。

1. 查找方法注入

容器可以重写由它管理的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;
    }
}

查找方法的签名需要遵循以下格式:

<public|protected> [abstract] <return-type> 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--跨作用域的依赖注入方案:方法注入

请注意,你通常应该使用具体的简化实现来声明此类带注解的查找方法,以便它们与 Spring 的组件扫描规则(默认情况下抽象类被忽略)兼容。此限制不适用于显式注册或显式导入的 bean 类。

另一种访问不同作用域中的bean的方式是ObjectFactory/Provider注入点。请查阅:Scoped Beans as Dependencies

org.springframework.beans.factory.config包下的ServiceLocatorFactoryBean或许也行。

2. 替代任意方法

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配置如下(我没有找到相应的基于注解的配置):

<bean id="myValueCalculator" class="com.weedien.ioccfg.methodInjection.MyValueCalculator">
        <!-- 任意需要被替代的方法 -->
        <replaced-method name="computeValue" replacer="replacementComputeValue">
            <arg-type>String</arg-type>
        </replaced-method>
    </bean>

    <bean id="replacementComputeValue" class="com.weedien.ioccfg.methodInjection.ReplacementComputeValue"/>

<arg-type>标签中,java.lang.String,StringStr是等价的。

<replace-method>标签中可以使用多个<arg-type>标签来描述需要被重写方法的签名。<replace-method>中的参数需要与重写方法的签名相一致,否则将无法正常匹配,比如,如果将<arg-type>String</arg-type>注释掉,computeValue方法将不会被替代。

在上述案例中,computeValue方法签名中的参数String不是必需的。如果某个类中存在多个需要被重写的同名方法,参数可以用来区分不同的方法,而上述案例中的方法参数并没有实际意义。

测试代码如下:

@Autowired
private MyValueCalculator valueCalculator;

@Test
void testArbitraryMethodReplacement() {
    System.out.println(valueCalculator.computeValue("test"));
}

从运行结果可以看出,reimplement方法替换了computeValue方法。

Spring--跨作用域的依赖注入方案:方法注入