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