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 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\lookup-method └─src\ ├─beans.xml ├─lee\ │ └─SpringTest.java └─org\ └─crazyit\ └─app\ └─service\ ├─Dog.java ├─impl\ │ ├─Chinese.java │ └─GunDog.java └─Person.java
|
Chinese.java
下面先将调用者Bean的实现类(Chinese)定义为抽象类,并定义一个抽象方法,该抽象方法用于获取被依赖的Bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package org.crazyit.app.service.impl;
import org.crazyit.app.service.*;
public abstract class Chinese implements Person { private Dog dog; public abstract Dog getDog(); public void hunt() { System.out.println("我带着:" + getDog() + "出去打猎"); System.out.println(getDog().run()); } }
|
上面程序中定义了一个抽象的getDog()方法,在通常情况下,程序不能调用这个抽象方法,程序也不能使用抽象类创建实例。
接下来需要在配置文件中为<bean>元素添加<lookup-method>子元素,<lookup-method>子元素告诉Spring需要实现哪个抽象方法。 Spring为抽象方法提供实现体之后,这个方法就会变成具体方法,这个类也就变成了具体类,接下来Spring就可以创建该Bean的实例了。
lookup-method元素属性
使用<lookup-method>元素需要指定如下两个属性。
| 属性 |
描述 |
name |
指定需要让Spring实现的方法。 |
bean |
指定Spring实现该方法的返回值 |
beans.xml
下面是该应用的配置文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?xml version="1.0" encoding="GBK"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="chinese" class="org.crazyit.app.service.impl.Chinese">
<lookup-method name="getDog" bean="gunDog"/> </bean>
<bean id="gunDog" class="org.crazyit.app.service.impl.GunDog" scope="prototype"> <property name="name" value="旺财"/> </bean> </beans>
|
上面程序中的粗体字代码指定Spring应该负责实现getDog()方法,该方法的返回值是容器中的gunDog Bean实例。
Spring实现方法的逻辑
在通常情况下,Java类里的所有方法都应该由程序员来负责实现,系统无法为任何方法提供实现。但在有些情况下,系统可以实现一些极其简单的方法,例如,此处Spring将负责实现getDog()方法, Spring实现该方法的逻辑是固定的,它总采用如下代码来实现该方法:
1 2 3 4 5 6 7 8
| public Dog getDog() { ... return ctx.getBean("gunDog"); }
|
从上面的方法实现来看,程序每次调用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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package lee;
import org.springframework.context.*; import org.springframework.context.support.*; import org.crazyit.app.service.*;
public class SpringTest { public static void main(String[] args) { @SuppressWarnings("resource") ApplicationContext ctx = new ClassPathXmlApplicationContext( "beans.xml"); Person p1 = ctx.getBean("chinese", Person.class); Person p2 = ctx.getBean("chinese", Person.class); System.out.println(p1 == p2); p1.hunt(); p2.hunt(); } }
|
由于getDog()方法由Spring提供实现, Spring保证每次调用getDog()时都会返回最新的gunDog实例。
运行结果
执行上面的程序,将看到如下运行结果:
1 2 3 4 5
| true 我带着:org.crazyit.app.service.impl.GunDog@757942a1出去打猎 我是一只叫旺财的猎犬,奔跑迅速... 我带着:org.crazyit.app.service.impl.GunDog@4a87761d出去打猎 我是一只叫旺财的猎犬,奔跑迅速...
|
执行结果表明:使用lookup方法注入后,系统每次调用getDog()方法时都将生成一个新的gunDog实例,这就可以保证当singleton作用域的Bean需要prototype Bean实例时,直接调用getDog()方法即可获取全新的实例,从而可避免一直使用最早注入的Bean实例。
lookup注入的目标Bean必须设为prototype作用域
要保证lookup方法注入每次产生新的Bean实例,必须将目标Bean部署成prototype作用域;否则,如果容器中只有一个被依赖的Bean实例,即使采用lookup方法注入,每次也依然返回同一个Bean实例。