前言:之前看到Spring中一个有趣的、但值得注意的问题,就是Spring中关于如何协调Bean作用域不同步?正常来讲,两个singleton作用域的bean存在依赖关系时,或者当prototype作用域的bean依赖singleton作用域的bean时,使用Spring默认提供的依赖注入管理即可,但是如果出现这种情况:singleton作用域的Bean依赖prototype作用域Bean时,会出现使用单例Bean时,每次其依赖的多例Bean都是同一个,这与多例Bean的设计初衷相违背。
本篇文章只关注以下问题:
- 如何将多例Bean正确注入单例Bean中
1. 问题描述
singleton作用域的Bean只有一次实例化机会,它的依赖关系也只在初始化阶段被设置,当singleton作用域的Bean依赖prototype作用域的Bean时,Spring容器会在初始化singleton作用域的Bean之前,先创建被依赖的prototype Bean,然后才初始化singleton Bean,并将prototype Bean注入到singleton Bean,这会导致以后无论何时通过singleton Bean去访问prototype Bean时,得到的永远是最初那个prototype Bean——这就相当于singleton Bean把它所依赖的prototype Bean变成了singleton行为。
由上产生出问题:如果客户端通过singleton Bean去调用prototype Bean的方法时,始终都是调用同一个prototype Bean实例,这就违背了设置prototype Bean的初衷——本来希望它具有prototype行为,但实际上它却表现出了singleton行为。
2. 解决思路
- 放弃依赖注入:singleton作用域的Bean每次需要prototype作用域的Bean时,主动向容器请求新的Bean实例,即可保证每次注入的prototype Bean实例都是最新的;
- 利用Spring提供的方法注入
第一种方式实际上是放弃Spring带来的控制反转的优势,代码主动请求新的Bean实例,必然导致程序代码与SpringAPI耦合,造成代码污染。
方法注入通常使用lookup方法注入,使用lookup方法注入可以让Spring容器重写容器中Bean的抽象或具体方法,返回查找容器中其他Bean的结果,被查找的Bean通常是一个not-singleton Bean。Spring通过使用JDK动态代理或cglib库修改客户端的二进制代码,从而达到上述要求。
3. Demo输出
假设有一个Chinese类型的Bean,该Bean包含一个signName()方法,执行该方法时需要依赖于Pen的方法——而且程序希望每次执行signName()方法时都使用不同的Pen Bean,因此首先需要将Pen Bean设置为prototype作用域。
除此之外,不能直接使用普通依赖注入将Pen Bean注入Chinese Bean中,还需要使用lookup方法注入来管理Pen Bean与Chinese Bean之间的依赖关系。其大致需要两步:
- 将调用者Bean的实现类定义为抽象类,并定义一个抽象方法来获取被依赖的Bean;(经测试,具体类也能成功)
- 在
元素中添加 子元素,让Spring为调用者Bean的实现类实现指定的抽象方法。
3.1 实现被依赖Bean
被依赖的Bean为多例模式:
package com.wj.chapter5.life.lookup; public class Pencil implements Pen { @Override public String signName(String name) { return "我的名字是" + name; } }
3.2 实现主调Bean
主调Bean(Chinese Bean)为单例模式,其依赖于多例Bean(Pen):
package com.wj.chapter5.life.lookup; /** * Chinese为单例模式,其依赖的Pen为多例 */ public abstract class Chinese implements Person { private String name; private Pen pen; public void setName(String name) { this.name = name; } // 定义抽象方法,该方法用于获取被依赖Bean public abstract Pen getPen(); @Override public void signName() { pen = getPen(); System.out.println("签名的笔型号:" + pen); System.out.println("我是中国人," + pen.signName(name)); } }
3.3 XML配置文件
- name:指定需要让Spring实现的方法
- bean:指定Spring实现该方法的返回值
3.4 测试代码
package com.wj.chapter5.life.lookup; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringTest { // 1.指明xml配置文件位置,便于Spring读取,从而知道Bean的相关信息 private static final String PATH_XML = "com/wj/chapter5/life/lookup/applicationContext-lookup.xml"; @SuppressWarnings("resource") public static void main(String[] args) { // 2.以类加载路径下的xml文件作为配置文件,创建Spring容器 ApplicationContext ctx = new ClassPathXmlApplicationContext(PATH_XML); // 3.两次获取单例Person对象 Person p1 = ctx.getBean("chinese" , Person.class); Person p2 = ctx.getBean("chinese" , Person.class); // 4. 首先验证两次获取的单例Person是否是同一个对象 System.out.println("两次获取的单例Person是否是同一对象:" + (p1 == p2)); // 5. 验证依赖的多例Pencil对象是否确实是多例的 p1.signName(); p2.signName(); } }
测试结果如下:
可见,lookup方法注入可以正确实现singleton Bean依赖prototype Bean。
3.5 补充说明
前面实现Person接口时,将Chinese定义为抽象类,并在里面定义抽象方法getPen()用于获取多例Pen。但这与我们习惯不符:Chinese Bean是业务上需要使用的,并不需要定为abstract抽象类,我们在开发中也确实没明确拿到抽象类Chinese的实现类(实际上Spring给我们返回的Bean就是Chinese的实现,只不过对我们透明)。
经过测试,可以将Chinese定义为具体类,getPen()定义为空实现即可。Spring会自动帮我们实现getPen()方法的逻辑:
package com.wj.chapter5.life.lookup; /** * Chinese为单例模式,其依赖的Pen为多例 * 此处将Chinese定义为具体类,getPen为空实现 */ public class Chinese2 implements Person { private String name; private Pen pen; public void setName(String name) { this.name = name; } // 该方法用于获取被依赖Bean,此处未空实现 public Pen getPen() { return null; } @Override public void signName() { pen = getPen(); System.out.println("签名的笔型号:" + pen); System.out.println("我是中国人," + pen.signName(name)); } }
测试结果与之前一样。
代码地址链接:http://pan.baidu.com/s/1dFJ5UQt 密码:dnjf