在大部分情况下,容器中的 bean 都是 singleton 类型的。如果一个 singleton bean 要引用另外一个 singleton bean,或者一个非 singleton bean 要引用另外一个非 singleton bean 时,通常情况下将一个 bean 定义为另一个 bean 的属性值就可以了。不过对于具有不同生命周期的 bean 来说这样做就会有问题了,比如在调用一个singleton 类型 bean A 的某个方法时,需要引用另一个非 singleton(prototype)类型的 bean B,对于 bean A 来说,容器只会创建一次,这样就没法在需要的时候每次让容器为 bean A 提供一个新的的 bean B 实例。
上述问题的一个解决办法就是放弃控制反转。通过实现 BeanFactoryAware 接口让 bean A 能够感知 bean 容器,并且在需要的时候通过使用 getBean("B") 方式向容器请求一个新的 bean B 实例。看下面这个例子,其中故意使用了这种方法:
package fiona.apple; // 导入 Spring-API 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() { // 手动通过 Spring API 获取名为 command 的依赖 Bean return this.applicationContext.getBean("command", Command.class); } public void setApplicationContext( ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
不推荐像上面的例子这样去做,因为业务代码和 Spring 框架产生了耦合。这里需要用到 Spring 的另一个功能 “方法注入”,作为 Spring IoC 容器的一种高级特性,可以以一种干净的方法来处理这种情况。
Lookup 方法注入利用了容器覆盖受容器管理的 bean 方法的能力,从而返回指定名字的 bean 实例。在上述场景中,Lookup 方法注入适用于原型(prototype)bean。Lookup 方法注入的内部机制是 Spring 利用了 CGLIB 库在运行时生成二进制代码功能,通过动态创建 Lookup 方法 bean 的子类而达到覆写 Lookup 方法的目的。
就前面代码片段中的 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(); } // 那么 createCommand 方法的具体实现在哪里呢? protected abstract Command createCommand(); }
在包含被注入方法的客户类中(此处是 CommandManager),此被注入方法的定义必须按以下形式进行:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是抽象的,动态生成的子类会实现该方法(指 createCommand() 方法)。否则,动态生成的子类会覆盖类里的具体方法。让我们来看个例子:
<!-- 一个 prototype 类型(non-singleton)部署的有状态 Bean --> <bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype"> <!-- 这里需要注入依赖 --> </bean> <!-- commandProcessor 使用 statefulCommandHelper --> <bean id="commandManager" class="fiona.apple.CommandManager"> <!-- 当需要 command Bean 时调用 createCommand 方法 --> <lookup-method name="createCommand" bean="myCommand"/> </bean>
在上面的例子中,名为 commandManager 的 bean 在需要一个新的 command bean 实例时,会调用 createCommand 方法。重要的一点是,必须将 command 部署为prototype。当然也可以指定为 singleton,如果是这样的话,那么每次将返回相同的 command bean 实例。
注意:为了让这个动态子类得以正常工作,需要把 CGLIB 的 jar 文件放在 classpath 里。另外,Spring 容器将要子类化的类不能是 final 的,要覆盖的方法也不能是 final 的。同样的,要测试一个包含抽象方法的类也稍微有些不同,你需要自己编写它的子类提供该抽象方法的桩实现。最后,作为方法注入目标的 bean 不能是序列化的(serialized)。
另外,在基于注解的组件模型中,也可以通过 @Lookup 注解声明 Lookup 方法,如下例所示:
public abstract class CommandManager { public Object process(Object commandState) { Command command = createCommand(); command.setState(commandState); return command.execute(); } // 通过注解声明这是一个 Lookup 方法 @Lookup protected abstract Command createCommand(); }
注意:通常应该使用具体的桩实现来声明这种带注释的查找方法,以便它们与 Spring 的组件扫描规则兼容,其中抽象类默认被忽略。此限制不适用于显式注册或显式导入的 Bean 类。
与 Lookup 方法注入相比,方法注入的一种不太有用的形式是能够用另一个方法实现替换托管 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 接口的类提供了新的方法定义,如下示例显示:
/** * 用于覆盖 MyValueCalculator 中现有的 computeValue(String) 的实现。 */ public class ReplacementComputeValue implements MethodReplacer { public Object reimplement(Object o, Method m, Object[] args) throws Throwable { // 获取输入值,对其进行处理,并返回计算结果 String input = (String) args[0]; ... return ...; } }
定义原始类并指定要覆盖方法的 bean 定义,例如:
<!-- 被方法覆盖的类定义 --> <bean id="myValueCalculator" class="x.y.z.MyValueCalculator"> <!-- 进行任意方法替换 --> <!-- 使用 replacementComputeValue 来替换 computeValue 方法 -->> <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.Type 匹配:
java.lang.String String Str
由于参数的数量通常足以区分每种可能的选择,因此这种快捷方式可以让您只输入与参数类型相匹配的最短字符串,从而节省大量输入时间。
该示例演示如何使用 <lookup-method> 元素实现方法注入。引入 CGLIB 依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.6</version> </dependency>
applicationContext-ioc-demo6.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 http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="beanTwo" class="com.hxstrive.spring5.ioc.demo6.BeanTwo" /> <!-- 声明 BeanThree 为 prototype,即非单例的 --> <bean id="beanThree" class="com.hxstrive.spring5.ioc.demo6.BeanThree" scope="prototype" /> <!-- 验证 lookup 方法注入方式 --> <bean id="beanOne" class="com.hxstrive.spring5.ioc.demo6.BeanOne"> <property name="beanTwo" ref="beanTwo" /> <lookup-method name="createBeanThree" bean="beanThree" /> </bean> <!-- 验证方法替换方式 --> <bean id="beanOne2" class="com.hxstrive.spring5.ioc.demo6.BeanOne2"> <property name="beanTwo" ref="beanTwo" /> <replaced-method name="createBeanThree" replacer="replacementCreateBeanThree" /> </bean> <bean id="replacementCreateBeanThree" class="com.hxstrive.spring5.ioc.demo6.ReplacementCreateBeanThree" /> </beans>
BeanOne.java:
package com.hxstrive.spring5.ioc.demo6; public abstract class BeanOne { private BeanTwo beanTwo; private BeanThree beanThree; @Override public String toString() { // 手动调用方法将 BeanThree 实例赋值给变量,Spring 不会自动调用的 // 注意:createBeanThree() 方法不能放在构造器和 @PostConstruct 修饰的方法中 this.beanThree = createBeanThree(); StringBuilder builder = new StringBuilder(); builder.append(this.getClass() + "{"); builder.append("\n beanTwo=" + beanTwo); builder.append("\n beanThree=" + beanThree + "\n}"); return builder.toString(); } /** * 该抽象类将有 Spring 通过 CGLIB 生成代理类,帮你实现这个方法,返回 BeanThree 实例 * @return */ public abstract BeanThree createBeanThree(); public void setBeanTwo(BeanTwo beanTwo) { this.beanTwo = beanTwo; } }
BeanOne2.java:
package com.hxstrive.spring5.ioc.demo6; public class BeanOne2 { private BeanTwo beanTwo; private BeanThree beanThree; @Override public String toString() { // 手动调用方法将 BeanThree 实例赋值给变量,Spring 不会自动调用的 // 注意:createBeanThree() 方法不能放在构造器和 @PostConstruct 修饰的方法中 this.beanThree = createBeanThree(); StringBuilder builder = new StringBuilder(); builder.append(this.getClass() + "{"); builder.append("\n beanTwo=" + beanTwo); builder.append("\n beanThree=" + beanThree + "\n}"); return builder.toString(); } /** * 该方法将被 ReplacementCreateBeanThree 替换 * @return */ public BeanThree createBeanThree() { return null; } public void setBeanTwo(BeanTwo beanTwo) { this.beanTwo = beanTwo; } }
BeanTwo.java:
package com.hxstrive.spring5.ioc.demo6; public class BeanTwo { //... }
BeanThree.java:
package com.hxstrive.spring5.ioc.demo6; public class BeanThree { //... }
Main.java:
package com.hxstrive.spring5.ioc.demo6; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * Bean 方法注入 @Lookup 或 <lookup-method> * @author hxstrive.com */ public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext-ioc-demo6.xml"); System.out.println(context.getBean(BeanOne.class)); System.out.println(context.getBean(BeanOne.class)); System.out.println("\n==================================================\n"); System.out.println(context.getBean(BeanOne2.class)); System.out.println(context.getBean(BeanOne2.class)); } }
运行示例,输出如下:
class com.hxstrive.spring5.ioc.demo6.BeanOne$$EnhancerBySpringCGLIB$$59422755{ beanTwo=com.hxstrive.spring5.ioc.demo6.BeanTwo@11e21d0e beanThree=com.hxstrive.spring5.ioc.demo6.BeanThree@1dd02175 } class com.hxstrive.spring5.ioc.demo6.BeanOne$$EnhancerBySpringCGLIB$$59422755{ beanTwo=com.hxstrive.spring5.ioc.demo6.BeanTwo@11e21d0e beanThree=com.hxstrive.spring5.ioc.demo6.BeanThree@31206beb } ================================================== class com.hxstrive.spring5.ioc.demo6.BeanOne2$$EnhancerBySpringCGLIB$$a959e9d5{ beanTwo=com.hxstrive.spring5.ioc.demo6.BeanTwo@11e21d0e beanThree=com.hxstrive.spring5.ioc.demo6.BeanThree@3e77a1ed } class com.hxstrive.spring5.ioc.demo6.BeanOne2$$EnhancerBySpringCGLIB$$a959e9d5{ beanTwo=com.hxstrive.spring5.ioc.demo6.BeanTwo@11e21d0e beanThree=com.hxstrive.spring5.ioc.demo6.BeanThree@3ffcd140 }
从上面输出可知,beanThree 每次返回的都是不同的实例,而 beanTwo 返回的是同一个实例。