7.4.6. 方法注入

在多数程序应用场景下,容器中的bean大部分都是单例的。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将一个bean定义为另一个bean的属性您就可以处理这种依赖。问题出现在bean的生命周期不同时。假设单例bean A需要使用原型bean(非单例的)B,可能调用A上的每个方法都会用到B。容器只会创建单例bean A一次,因此只有一次机会来设置属性,容器无法在每次bean A需要bean B时都为其提供一个B的新实例。

一种解决方法是放弃部分控制反转。您可以通过实现ApplicationContextAware接口使bean A知晓容器,然后每当bean A需要bean B时通过向容器调用getBean("B")方法请求(一般是一个新的)bean B的实例。以下是这种方法的一个例子:

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

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容器的一个高级功能,它能以一种干净的方式处理这种情况。

您可以在此篇博客中读到更多有关方法注入动机的东西。

查找方法注入

查找方法注入是容器的一种能力,让容器能够覆盖被其管理的bean上的方法,以返回对容器中另一个已命名bean的查找结果。这种查找通常涉及前面部分所述的原型bean。Spring Framework通过使用CGLIB库中的字节码生成来动态生成一个覆盖该方法的子类,从而实现了此方法注入。

  • 为使这个动态子类化的机制正常工作,Spring的bean容器将要子类化的这个类不能是final的,而且被覆盖的方法也不能是final的。
  • 对具有abstract方法的类进行单元测试需要您自己对该类进行子类化并提供该abstract方法的stub实现。
  • Concrete methods are also necessary for component scanning which requires concrete classes to pick up.
  • 另一个关键的限制是,查找方法不能与工厂方法特别是不能与配置类中使用@Bean的方法一起工作,因为在那种情况下容器并不负责创建实例,所以无法动态创建运行时生成的子类。

看一下前面代码片段中的CommandManager类,可以看到Spring容器会动态地覆盖掉createCommand()方法的实现。在修订的例子中可以看到,您的CommandManager类中没有任何的Spring依赖:

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

在包含要被注入的方法的客户端类(示例中的CommandManager)中,要被注入的方法需要具有以下形式的签名:

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

如果方法是abstract的,则动态生成的子类实现该方法;否则,动态生成的子类会覆盖掉原始类中定义的具体方法。例如:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

当需要myCommand bean的新实例时,标识为commandManager的bean调用它自己的createCommand()方法。将myCommand部署为原型bean时,您一定要谨慎注意实际上是否需要这样做。如果部署为单例,那么每次返回的都是myCommandbean的同一个实例。

另外,在基于注解的组件模型中,你可以通过@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) {
        MyCommand command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup
    protected abstract MyCommand createCommand();
}

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

访问不同域的目标bean的另一种方式是ObjectFactory/ Provider注入。查看名为“Scoped beans as dependencies”的小节。 感兴趣的读者也可以看一下ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包)以供使用。

任意方法替换

与查找方法注入相比,一种不太有用的方法注入形式是用另一个方法的实现来替换托管bean中的任意方法。在实际需要此功能之前,用户可以安心地跳过本节的其余部分。

使用基于XML的配置元数据,您可以使用replaced-method元素将已部署的bean的现有方法实现替换为其他实现。考虑下面的类,它有一个computeValue方法,我们想要覆盖该方法:

public class MyValueCalculator {

    public String computeValue(String input) {
        // some real code...
    }

    // some other methods...
}

实现了org.springframework.beans.factory.support.MethodReplacer接口的类提供了新的方法定义。

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {

    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        String input = (String) args[0];
        ...
        return ...;
    }
}

部署原始类并指定覆盖方法的bean定义如下所示:

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- arbitrary method replacement -->
    <replaced-method name="computeValue" replacer="replacementComputeValue">
        <arg-type>String</arg-type>
    </replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

您可以在<replaced-method/>元素中使用一个或多个<arg-type/>元素来指示被覆盖方法的方法签名。只有当方法被重载并且类中存在多个变体时,参数的签名才是必需的。为了方便,参数的类型字符串可能是完全限定类型名的子字符串。例如,以下全部匹配java.lang.String

java.lang.String
String
Str

由于参数的数量通常足以区分每种可能的选择,因此只需键入与参数类型匹配的最短字符串,这样可以节省大量输入。


书籍推荐