在大多数应用场景中,容器中的大多数 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);}@Overridepublic 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/beanshttps://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@4310d43cn.mrcode.study.springdocsread.Command@54a7079ecn.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/beanshttps://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*/@Componentpublic 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 {@Overridepublic 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
