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

2023年 8月 16日 62.6k 0

参考: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实例:

img

请注意,你通常应该使用具体的简化实现来声明此类带注解的查找方法,以便它们与 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,StringStr是等价的。

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

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

测试代码如下:

@Autowired
private MyValueCalculator valueCalculator;

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

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

image-20230814151047268

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论