Spring方法查找与替换

122 阅读4分钟

引入

大多数场景中,Spring 容器中管理的 bean 都是单例的。当一个 bean 需要与另外的 bean 进行协作时,我们通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。然而,如果bean的生命周期不同,就会出现问题。假设单例 bean A 需要使用原型 bean B,容器只创建一次单例 bean A,因此只有一次机会来设置属性。容器不能在每次需要 bean B 的新实例时为 bean A 提供一个新实例。

对于以上问题,Spring官方提供的解决方案是:放弃依赖的控制反转,可以通过实现 ApplicationContextAware 接口获取到容器,当每次bean A 需要 bean B的最新实例时,通过 getBean("B")调用请求最新的bean B 实例。

public class CommandManager implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;
    
    public Object process(Map commandState) { 
    
        // 获取最新的实例bean 
        Command command = createCommand(); 
        // 设置原型bean最新状态(实际开发中通常是设置最新属性或者bean的最新依赖)
        Command instance command.setState(commandState); 

        return command.execute(); 
    } 

    protected Command createCommand() { 
        // 通过容器获取最新的command原型bean
        return this.applicationContext.getBean("command", Command.class); 
    } 
    
    public void setApplicationContext( ApplicationContext applicationContext) throws BeansException { 
        this.applicationContext = applicationContext; 
    } 
}

上述编码Spring官方是不推荐的,原因在于业务代码与Spring 框架耦合在一起,不利于后期的代码维护

方法查找注入(Lookup Method Injection)

方法查找注入是Spring 容器重写被容器管理的 bean 上的方法并返回容器中另一个命名 bean 的查找结果的能力。查找通常涉及一个原型 bean,如上述描述的场景所示。Spring 框架通过CGLIB 字节码生成库来动态生成覆盖该方法的子类,从而实现此方法注入。

注意:由于CGLIB框架通过生成子类重写父类方法来实现方法替换,因此该父类不能用final修饰并且要覆盖的方法也不能用final修饰。此外,查找方法不能和工厂方法一起工作,特别是不能与配置类中的@Bean方法一起工作,因为在这种情况下,容器不负责创建实例,因此不能动态创建运行时生成的子类

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

public abstract class CommandManager { 

    public Object process(Object commandState) { 
    
        // 获取适当 Command 接口的新实例
        Command command = createCommand(); 
        // 设置 Command 实例的状态(希望是全新的) 
        Command instance command.setState(commandState);
        return command.execute(); 
    } 
    
    // 思考 方法的具体实现在哪里?
    protected abstract Command createCommand(); 
}

注意:在要注入方法的客户端类的方法有一定要求,必须符合以下的方法签名:

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

说明:该方法是不能有任何参数!!!如果该方法是抽象方法,生成的子类则实现该方法,否则重写该方法。

<!-- 原型bean -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean> 

<!-- 客户端bean,每次获取最新的原型bean,并通过lookup-method属性替换抽象方法,返回原型bean -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

标识为 commandManager 的 bean 在需要 myCommand 这个 bean 的新实例时调用自己的 createCommand()方法。如果确实需要将 myCommand 作为原型部署,则必须小心。如果它是一个单例,那么每次返回 myCommand bean 的相同实例。

此外,也可以通过@Lookup 注解声明一个查找方法,如下面的示例所示:

public abstract class CommandManager { 
    public Object process(Object commandState) { 
        Command command = createCommand();
        command.setState(commandState); 
        return command.execute(); 
    } 
    
    @Lookup("myCommand")
    protected abstract Command createCommand(); 
}

或者,更惯用的方法是,可以依赖于根据查找方法的声明返回类型解析目标 bean:

public abstract class CommandManager {
    public Object process(Object commandState) { 
        Command command = createCommand(); 
        command.setState(commandState); 
        return command.execute(); 
    }
    
    @Lookup
    protected abstract Command createCommand(); 
}

注意,通常情况下应该使用具体的存根实现声明这种带注解的查找方法,以便它们与 Spring 的组件扫描规则兼容,在这些规则中,抽象类在默认情况下被忽略。这个限制不适用于显式注册或显式导入的 bean 类。

任意方法替换

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

下面使用基于 XML 的配置元数据,可以使用替换方法元素将现有的方法实现替换为另一个方法实现。考虑下面的类,它有一个名为 computeValue 的方法,我们希望重写它:

public class MyValueCalculator { 
    public String computeValue(String input) { 
        // 这里的方法体将被替换
    } 

    // 其他方法
}

方法替换需要实现 org.springframework.beans.factory.support.MethodReplace 接口,该接口提供的方法是替换后的方法逻辑体,示例如下:


/** 
* 用来替换已经存在的方法 computeValue(String) 
*/ 
public class ReplacementComputeValue implements MethodReplacer { 
    
    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // 拦截到目标方法,替换为此段代码逻辑
        String input = (String) args[0]; 
        // 直接返回第一个方法参数
        return input; 
    } 
}

XML配置方法替换

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- 
        任意方法替换
        name:指定原对象待替换的方法名
        replacer:指定替换方法的实现实例
    --> 
    <replaced-method name="computeValue" replacer="replacementComputeValue"> 
        <!--指定方法的参数类型(必须),此处待替换方法只有一个参数-->
        <arg-type>String</arg-type>
    </replaced-method>
</bean> 

<!--替换方法实现实例-->
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>