在大多数应用场景中,容器中的大多数 bean 都是 单例 的。当一个单例 bean 需要与另一个单例 bean 协作或非单例 bean 需要与另一个非单例 bean 协作时,您通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。当 bean 生命周期不同时,就会出现问题。
假设单例 bean A 需要使用非单例(prototype)bean B,可能在 A 上的每个方法调用上。容器只创建一次单例 bean A,因此只有一次设置属性的机会。容器无法在每次需要时为 bean A 提供一个新的 bean B 实例。
一个解决方案是:放弃一些控制反转,使用下面的示例来达到效果
package cn.mrcode.study.springdocsread;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import java.util.Map;
/**
* 实现 ApplicationContextAware 接口,获得 ApplicationContext 实例
*/
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// 创建 Command 的实例
Command command = createCommand();
// 设置一些状态
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// 通过容器获取 command ,当然这个 command 需要是一个非单例配置,才会每次获取都会返回一个新的实例
return this.applicationContext.getBean("command", Command.class);
}
@Override
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
CommandManager 是单例,Command 是多列,在单例中使用多例,手动从 ApplicationContext 中获取多例
这个是不可取的,因为业务代码知道并耦合到 Spring 框架。方法注入是 Spring IoC 容器的一项高级功能,可让您干净地处理此用例。您可以在此博客条目 中阅读有关方法注入动机的更多信息 。
查找方法注入
查找方法注入(Lookup method injection) 是容器重写容器管理 bean 上的方法并返回容器中另一个命名 bean 的查找结果的能力。查找通常涉及一个原型 bean,如上面的场景列子所述。Spring 框架通过使用CGLIB 库中的字节码生成来动态生成重写该方法的子类,从而实现这种方法注入。
:::tips 要实现这个机制有一些限制(不遵循这些限制就会无效):
- 为了使这个动态子类能够工作,SpringBean 容器子类的类不能是 final,要重写的方法也不能是 final。
- 单元测试具有抽象方法的类需要您自己对该类进行子类化,并提供抽象方法的存根实现。
- 组件扫描也需要具体的方法,这需要具体的类来拾取。
- 另一个关键限制是,查找方法不适用于工厂方法,尤其是配置类中的 @Bean 方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类。 :::
对于前面代码段中的 CommandManager 类,Spring 容器会动态重写 createCommand() 方法的实现。CommandManager 类没有任何 Spring 依赖项,如修改后的示例所示:
package cn.mrcode.study.springdocsread;
public abstract class CommandManager {
public Object process(Object commandState) {
// 创建 Command 的实例
Command command = createCommand();
// 设置一些状态
command.setState(commandState);
return command.execute();
}
// 这个方法的实现在哪里呢?
protected abstract Command createCommand();
}
要注入的方法需要以下形式的签名:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是 abstract,则动态生成的子类实现该方法。否则,动态生成的子类将覆盖原始类中定义的具体方法。考虑以下示例:
<!-- 有状态的 bean, prototype (不是单例) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- 根据需要在此处注入依赖项-->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
一个例子
xml 配置
package cn.mrcode.study.springdocsread;
import org.springframework.beans.factory.annotation.Lookup;
/**
* @author mrcode
*/
public class CommandManager {
public Command process(Object commandState) {
// 创建 Command 的实例
Command command = createCommand();
return command;
}
// 这个方法的实现在哪里呢?
public Command createCommand() {
return null;
}
}
package cn.mrcode.study.springdocsread;
/**
* @author mrcode
*/
public class Command {
}
xml 中要这样配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd"
default-autowire-candidates="false"
>
<bean id="commandManager" class="cn.mrcode.study.springdocsread.CommandManager">
<!--
bean :是需要从容器中获取的实例
name: 是 CommandManager 中需要容器代理的方法
-->
<lookup-method bean="command" name="createCommand"></lookup-method>
</bean>
<!-- 这里设置成多例 scope="prototype" -->
<bean id="command" class="cn.mrcode.study.springdocsread.Command" scope="prototype"></bean>
</beans>
测试
package cn.mrcode.study.springdocsread;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author mrcode
*/
public class TestDemo {
public static void main(String[] args) {
final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
final CommandManager commandManager = context.getBean(CommandManager.class);
for (int i = 0; i < 3; i++) {
final Command command = commandManager.process("");
System.out.println(command);
}
}
}
输出信息
cn.mrcode.study.springdocsread.Command@4310d43
cn.mrcode.study.springdocsread.Command@54a7079e
cn.mrcode.study.springdocsread.Command@26e356f0
注解配置
:::tips 需要注意的是:这里的注解配置,并不是说可以在 boot 那样的环境中使用,因为这个方法的限制在最前面就说明了,不能用于 @Bean 声明的方式,因为使用 @Bean 方式返回 Command 是我们自己 new 出来的,不是 容器 创建的
后补:想要在 boot 那样的环境中使用,是有方式的,只不过是用了另外一种方式实现的,可以参考这个文章 ::: 所以这个例子想要生效,还是只能使用 xml 方式
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
比如这样:
package cn.mrcode.study.springdocsread;
import org.springframework.beans.factory.annotation.Lookup;
public class CommandManager {
public Command process(Object commandState) {
// 创建 Command 的实例
Command command = createCommand();
return command;
}
// 这个方法的实现在哪里呢?
@Lookup("command")
public Command createCommand() {
return null;
}
}
开启注解扫描,然后 xml 中不配置 <lookup-method>
了
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"
default-autowire-candidates="false"
>
<!-- 开启注解扫描 -->
<context:annotation-config></context:annotation-config>
<bean id="commandManager" class="cn.mrcode.study.springdocsread.CommandManager">
<!--
bean :是需要从容器中获取的实例
name: 是 CommandManager 中需要容器代理的方法
-->
<!-- <lookup-method bean="command" name="createCommand"></lookup-method>-->
</bean>
<!-- 这里设置成多例 scope="prototype" -->
<bean id="command" class="cn.mrcode.study.springdocsread.Command" scope="prototype"></bean>
</beans>
任意方法替换
与查找方法注入相比,一种不太有用的方法注入形式是能够用另一种方法实现替换托管 bean 中的任意方法
比如下面这个类
package cn.mrcode.study.springdocsread;
import org.springframework.stereotype.Component;
/**
* @author zhuqiang
* @date 2022/2/11 11:36
*/
@Component
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
return null;
}
}
上面返回了 null,可以通过实现 org.springframework.beans.factory.support.MethodReplacer
接口提供新的方法实现
package cn.mrcode.study.springdocsread;
import org.springframework.beans.factory.support.MethodReplacer;
import java.lang.reflect.Method;
/**
* @author mrcode
* @date 2022/2/11 11:37
*/
public class ReplacementComputeValue implements MethodReplacer {
@Override
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
String input = (String) args[0];
return input + "替换";
}
}
然后在配置 Bean 的时候指定使用 ReplacementComputeValue 来替换 computeValue 方法的实现
<bean id="myValueCalculator" class="cn.mrcode.study.springdocsread.MyValueCalculator">
<!--
name: 要替换的方法名称
replacementComputeValue:要使用谁来替换
arg-type:由于方法有重写方式,所以需要指定参数的类型
-->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="cn.mrcode.study.springdocsread.ReplacementComputeValue"/>
<arg-type>
可以有多个,可以是简写,也可以是完全的类限定名称。比如 java.lang.String