在大多数应用场景中,容器中的大多数 bean 都是 单例 的。当一个单例 bean 需要与另一个单例 bean 协作或非单例 bean 需要与另一个非单例 bean 协作时,您通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。当 bean 生命周期不同时,就会出现问题。

假设单例 bean A 需要使用非单例(prototype)bean B,可能在 A 上的每个方法调用上。容器只创建一次单例 bean A,因此只有一次设置属性的机会。容器无法在每次需要时为 bean A 提供一个新的 bean B 实例。

一个解决方案是:放弃一些控制反转,使用下面的示例来达到效果

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.beans.BeansException;
  3. import org.springframework.context.ApplicationContext;
  4. import org.springframework.context.ApplicationContextAware;
  5. import java.util.Map;
  6. /**
  7. * 实现 ApplicationContextAware 接口,获得 ApplicationContext 实例
  8. */
  9. public class CommandManager implements ApplicationContextAware {
  10. private ApplicationContext applicationContext;
  11. public Object process(Map commandState) {
  12. // 创建 Command 的实例
  13. Command command = createCommand();
  14. // 设置一些状态
  15. command.setState(commandState);
  16. return command.execute();
  17. }
  18. protected Command createCommand() {
  19. // 通过容器获取 command ,当然这个 command 需要是一个非单例配置,才会每次获取都会返回一个新的实例
  20. return this.applicationContext.getBean("command", Command.class);
  21. }
  22. @Override
  23. public void setApplicationContext(
  24. ApplicationContext applicationContext) throws BeansException {
  25. this.applicationContext = applicationContext;
  26. }
  27. }

CommandManager 是单例,Command 是多列,在单例中使用多例,手动从 ApplicationContext 中获取多例

这个是不可取的,因为业务代码知道并耦合到 Spring 框架。方法注入是 Spring IoC 容器的一项高级功能,可让您干净地处理此用例。您可以在此博客条目 中阅读有关方法注入动机的更多信息 。

查找方法注入

查找方法注入(Lookup method injection) 是容器重写容器管理 bean 上的方法并返回容器中另一个命名 bean 的查找结果的能力。查找通常涉及一个原型 bean,如上面的场景列子所述。Spring 框架通过使用CGLIB 库中的字节码生成来动态生成重写该方法的子类,从而实现这种方法注入。

:::tips 要实现这个机制有一些限制(不遵循这些限制就会无效):

  • 为了使这个动态子类能够工作,SpringBean 容器子类的类不能是 final,要重写的方法也不能是 final。
  • 单元测试具有抽象方法的类需要您自己对该类进行子类化,并提供抽象方法的存根实现。
  • 组件扫描也需要具体的方法,这需要具体的类来拾取。
  • 另一个关键限制是,查找方法不适用于工厂方法,尤其是配置类中的 @Bean 方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类。 :::

对于前面代码段中的 CommandManager 类,Spring 容器会动态重写 createCommand() 方法的实现。CommandManager 类没有任何 Spring 依赖项,如修改后的示例所示:

  1. package cn.mrcode.study.springdocsread;
  2. public abstract class CommandManager {
  3. public Object process(Object commandState) {
  4. // 创建 Command 的实例
  5. Command command = createCommand();
  6. // 设置一些状态
  7. command.setState(commandState);
  8. return command.execute();
  9. }
  10. // 这个方法的实现在哪里呢?
  11. protected abstract Command createCommand();
  12. }

要注入的方法需要以下形式的签名:

  1. <public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是 abstract,则动态生成的子类实现该方法。否则,动态生成的子类将覆盖原始类中定义的具体方法。考虑以下示例:

  1. <!-- 有状态的 bean prototype (不是单例) -->
  2. <bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
  3. <!-- 根据需要在此处注入依赖项-->
  4. </bean>
  5. <!-- commandProcessor uses statefulCommandHelper -->
  6. <bean id="commandManager" class="fiona.apple.CommandManager">
  7. <lookup-method name="createCommand" bean="myCommand"/>
  8. </bean>

一个例子

xml 配置

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.beans.factory.annotation.Lookup;
  3. /**
  4. * @author mrcode
  5. */
  6. public class CommandManager {
  7. public Command process(Object commandState) {
  8. // 创建 Command 的实例
  9. Command command = createCommand();
  10. return command;
  11. }
  12. // 这个方法的实现在哪里呢?
  13. public Command createCommand() {
  14. return null;
  15. }
  16. }
  1. package cn.mrcode.study.springdocsread;
  2. /**
  3. * @author mrcode
  4. */
  5. public class Command {
  6. }

xml 中要这样配置

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://www.springframework.org/schema/beans
  5. https://www.springframework.org/schema/beans/spring-beans.xsd"
  6. default-autowire-candidates="false"
  7. >
  8. <bean id="commandManager" class="cn.mrcode.study.springdocsread.CommandManager">
  9. <!--
  10. bean :是需要从容器中获取的实例
  11. name: 是 CommandManager 中需要容器代理的方法
  12. -->
  13. <lookup-method bean="command" name="createCommand"></lookup-method>
  14. </bean>
  15. <!-- 这里设置成多例 scope="prototype" -->
  16. <bean id="command" class="cn.mrcode.study.springdocsread.Command" scope="prototype"></bean>
  17. </beans>

测试

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.context.support.ClassPathXmlApplicationContext;
  3. /**
  4. * @author mrcode
  5. */
  6. public class TestDemo {
  7. public static void main(String[] args) {
  8. final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
  9. final CommandManager commandManager = context.getBean(CommandManager.class);
  10. for (int i = 0; i < 3; i++) {
  11. final Command command = commandManager.process("");
  12. System.out.println(command);
  13. }
  14. }
  15. }

输出信息

  1. cn.mrcode.study.springdocsread.Command@4310d43
  2. cn.mrcode.study.springdocsread.Command@54a7079e
  3. cn.mrcode.study.springdocsread.Command@26e356f0

注解配置

:::tips 需要注意的是:这里的注解配置,并不是说可以在 boot 那样的环境中使用,因为这个方法的限制在最前面就说明了,不能用于 @Bean 声明的方式,因为使用 @Bean 方式返回 Command 是我们自己 new 出来的,不是 容器 创建的

后补:想要在 boot 那样的环境中使用,是有方式的,只不过是用了另外一种方式实现的,可以参考这个文章 ::: 所以这个例子想要生效,还是只能使用 xml 方式

  1. public abstract class CommandManager {
  2. public Object process(Object commandState) {
  3. Command command = createCommand();
  4. command.setState(commandState);
  5. return command.execute();
  6. }
  7. @Lookup("myCommand")
  8. protected abstract Command createCommand();

比如这样:

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.beans.factory.annotation.Lookup;
  3. public class CommandManager {
  4. public Command process(Object commandState) {
  5. // 创建 Command 的实例
  6. Command command = createCommand();
  7. return command;
  8. }
  9. // 这个方法的实现在哪里呢?
  10. @Lookup("command")
  11. public Command createCommand() {
  12. return null;
  13. }
  14. }

开启注解扫描,然后 xml 中不配置 <lookup-method>

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"
  7. default-autowire-candidates="false"
  8. >
  9. <!-- 开启注解扫描 -->
  10. <context:annotation-config></context:annotation-config>
  11. <bean id="commandManager" class="cn.mrcode.study.springdocsread.CommandManager">
  12. <!--
  13. bean :是需要从容器中获取的实例
  14. name: 是 CommandManager 中需要容器代理的方法
  15. -->
  16. <!-- <lookup-method bean="command" name="createCommand"></lookup-method>-->
  17. </bean>
  18. <!-- 这里设置成多例 scope="prototype" -->
  19. <bean id="command" class="cn.mrcode.study.springdocsread.Command" scope="prototype"></bean>
  20. </beans>

任意方法替换

与查找方法注入相比,一种不太有用的方法注入形式是能够用另一种方法实现替换托管 bean 中的任意方法

比如下面这个类

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.stereotype.Component;
  3. /**
  4. * @author zhuqiang
  5. * @date 2022/2/11 11:36
  6. */
  7. @Component
  8. public class MyValueCalculator {
  9. public String computeValue(String input) {
  10. // some real code...
  11. return null;
  12. }
  13. }

上面返回了 null,可以通过实现 org.springframework.beans.factory.support.MethodReplacer接口提供新的方法实现

  1. package cn.mrcode.study.springdocsread;
  2. import org.springframework.beans.factory.support.MethodReplacer;
  3. import java.lang.reflect.Method;
  4. /**
  5. * @author mrcode
  6. * @date 2022/2/11 11:37
  7. */
  8. public class ReplacementComputeValue implements MethodReplacer {
  9. @Override
  10. public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
  11. String input = (String) args[0];
  12. return input + "替换";
  13. }
  14. }

然后在配置 Bean 的时候指定使用 ReplacementComputeValue 来替换 computeValue 方法的实现

  1. <bean id="myValueCalculator" class="cn.mrcode.study.springdocsread.MyValueCalculator">
  2. <!--
  3. name: 要替换的方法名称
  4. replacementComputeValue:要使用谁来替换
  5. arg-type:由于方法有重写方式,所以需要指定参数的类型
  6. -->
  7. <replaced-method name="computeValue" replacer="replacementComputeValue">
  8. <arg-type>String</arg-type>
  9. </replaced-method>
  10. </bean>
  11. <bean id="replacementComputeValue" class="cn.mrcode.study.springdocsread.ReplacementComputeValue"/>

<arg-type>可以有多个,可以是简写,也可以是完全的类限定名称。比如 java.lang.String