7.9.3 协调作用域不同步的Bean
当两个singleton作用域的Bean存在依赖关系时,或者当prototype作用域的Bean依赖singleton作用域的Bean时,使用Spring提供的依赖注入进行管理即可。singleton作用域的Bean只有一次初始化的机会,它的依赖关系也只在初始化阶段被设置,当singleton作用域的Bean依赖prototype作用域的Bean时, Spring容器会在初始化singleton作用域的Bean之前,先创建被依赖的prototype Bean,然后才初始化singleton Bean,并将prototype Bean注入singletonBean,这会导致以后无论何时通过singleton Bean去访问prototype Bean时,得到的永远是最初那个prototype Bean。这样就相当于singleton bean把它所依赖的prototype Bean变成了singleton行为。
假如有如图7.13所示的依赖关系。
对于图7.13所示的依赖关系,当Spring容器初始化时,容器会预初始化容器中所有的singleton Bean,由于singleton Bean依赖于prototype Bean,因此Spring在初始化singleton bean之前,会先创建prototype bean—然后才创建singleton Bean,接下来将prototype Bean注入singleton Bean。一旦singleton Bean初始化完成,它就持有了一个prototype Bean,容器再也不会为singleton bean执行注入了。
由于singleton Bean具有单例行为,当客户端多次请求singleton Bean时, Spring返回给客户端的将是同一个singleton bean实例,这不存在任何问题。
问题是:如果客户端通过该singleton Bean去调用prototype Bean的方法时—始终都是调用同一个prototype Bean实例,这就违背了设置prototype Bean的初衷——本来希望它具有prototype行为,但实际上它却表现出singleton行为。
如何解决singleton作用域依赖prototype作用域时的不同步现象
问题产生了:当singleton作用域的Bean依赖于prototype作用域的Bean时,会产生不同步的现象。解决该问题有如下两种思路。
- 放弃依赖注入:
singleton作用域的Bean每次需要prototype作用域的Bean时,主动向容器请求新的Bean实例,即可保证每次注入的prototype Bean实例都是最新的实例 - 利用方法注入
推荐使用方法注入
第一种方式显然不是一个好的做法,代码主动请求新的Bean实例,必然导致程序代码与Spring API耦合,造成代码污染。
在通常情况下,建议使用方法注入。
方法注入
方法注入通常使用lookup方法注入,使用lookup方法注入可以让Spring容器重写容器中Bean的抽象或具体方法,返回查找容器中其他Bean的结果,被查找的Bean通常是一个non-singleton Bean(尽管也可以是一个singleton的)。 Spring通过使用JDK动态代理或cglib库修改客户端的二进制码,从而实现上述要求。
假设程序中有一个Chinese类型的Bean,该Bean包含一个hunt方法,执行该方法时需要依赖于Dog的方法—而且程序希望每次执行hunt方法时都使用不同的Dog Bean,因此首先需要将Dog Bean配置为prototype作用域。
除此之外,不能直接使用普通依赖注入将Dog Bean注入Chinese bean中,还需要使用lookup方法注入来管理Dog Bean与Chinese bean之间的依赖关系。
使用lookup方法注入的步骤
为了使用lookup方法注入,大致需要如下两步。
- 将调用者
Bean的实现类定义为抽象类,并定义一个抽象方法来获取被依赖的Bean。 - 在
<bean>元素中添加<lookup-method>子元素让Spring为调用者Bean的实现类实现指定的抽象方法。
程序示例
项目结构
1 | E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\lookup-method |
Chinese.java
下面先将调用者Bean的实现类(Chinese)定义为抽象类,并定义一个抽象方法,该抽象方法用于获取被依赖的Bean。
1 | package org.crazyit.app.service.impl; |
上面程序中定义了一个抽象的getDog()方法,在通常情况下,程序不能调用这个抽象方法,程序也不能使用抽象类创建实例。
接下来需要在配置文件中为<bean>元素添加<lookup-method>子元素,<lookup-method>子元素告诉Spring需要实现哪个抽象方法。 Spring为抽象方法提供实现体之后,这个方法就会变成具体方法,这个类也就变成了具体类,接下来Spring就可以创建该Bean的实例了。
lookup-method元素属性
使用<lookup-method>元素需要指定如下两个属性。
| 属性 | 描述 |
|---|---|
name |
指定需要让Spring实现的方法。 |
bean |
指定Spring实现该方法的返回值 |
beans.xml
下面是该应用的配置文件。
1 |
|
上面程序中的粗体字代码指定Spring应该负责实现getDog()方法,该方法的返回值是容器中的gunDog Bean实例。
Spring实现方法的逻辑
在通常情况下,Java类里的所有方法都应该由程序员来负责实现,系统无法为任何方法提供实现。但在有些情况下,系统可以实现一些极其简单的方法,例如,此处Spring将负责实现getDog()方法, Spring实现该方法的逻辑是固定的,它总采用如下代码来实现该方法:
1 | // Spring要实现哪个方法由lookup-method元素的name属性指定 |
从上面的方法实现来看,程序每次调用Chinese对象的getDog()方法时,该方法将可以获取最新的gunDog对象。
Spring实现方法的方式
Spring会采用运行时动态增强的方式来实现<lookup-method>.元素所指定的抽象方法,
- 如果目标抽象类(如上
Chinese类)实现过接口,Spring会采用JDK动态代理来实现该抽象类,并为之实现抽象方法; - 如果目标抽象类(如上
Chinese类)没有实现过接口,Spring会采用cglib实现该抽象类,并为之实现抽象方法。Spring5.0的spring-core-xxx.jar包中已经集成了cglib类库,无须额外添加cgib的JAR包.
SpringTest.java
主程序两次获取chinese这个Bean,并通过该Bean来执行hunt方法,将可以看到每次请求时所使用的都是全新的Dog实例
1 | package lee; |
由于getDog()方法由Spring提供实现, Spring保证每次调用getDog()时都会返回最新的gunDog实例。
运行结果
执行上面的程序,将看到如下运行结果:
1 | true |
执行结果表明:使用lookup方法注入后,系统每次调用getDog()方法时都将生成一个新的gunDog实例,这就可以保证当singleton作用域的Bean需要prototype Bean实例时,直接调用getDog()方法即可获取全新的实例,从而可避免一直使用最早注入的Bean实例。
lookup注入的目标Bean必须设为prototype作用域
要保证lookup方法注入每次产生新的Bean实例,必须将目标Bean部署成prototype作用域;否则,如果容器中只有一个被依赖的Bean实例,即使采用lookup方法注入,每次也依然返回同一个Bean实例。
原文链接: 7.9.3 协调作用域不同步的Bean