7.9.3 协调作用域不同步的Bean

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时,会产生不同步的现象。解决该问题有如下两种思路。

  1. 放弃依赖注入:singleton作用域的Bean每次需要prototype作用域的Bean时,主动向容器请求新的Bean实例,即可保证每次注入的prototype Bean实例都是最新的实例
  2. 利用方法注入

推荐使用方法注入

第一种方式显然不是一个好的做法,代码主动请求新的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 BeanChinese bean之间的依赖关系。

使用lookup方法注入的步骤

为了使用lookup方法注入,大致需要如下两步。

  1. 将调用者Bean实现类定义为抽象类,并定义一个抽象方法来获取被依赖的Bean
  2. <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;
// 定义抽象方法,该方法用于获取被依赖Bean
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">
<!-- Spring只要检测到lookup-method元素,
Spring会自动为该元素的name属性所指定的方法提供实现体。-->
<lookup-method name="getDog" bean="gunDog"/>
</bean>
<!-- 指定gunDog Bean的作用域为prototype,
希望程序每次使用该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
// Spring要实现哪个方法由lookup-method元素的name属性指定
public Dog getDog()
{
// 获取Spring容器ctx
...
// 下面代码中"gunDog"有lookub-method元素的bean属性指定
return ctx.getBean("gunDog");
}

从上面的方法实现来看,程序每次调用Chinese对象的getDog()方法时,该方法将可以获取最新的gunDog对象。

Spring实现方法的方式

Spring会采用运行时动态增强的方式来实现<lookup-method>.元素所指定的抽象方法,

  • 如果目标抽象类(如上Chinese类)实现过接口, Spring会采用**JDK动态代理来**实现该抽象类,并为之实现抽象方法;
  • 如果目标抽象类(如上Chinese类)没有实现过接口,Spring会采用cglib实现该抽象类,并为之实现抽象方法。 Spring5.0spring-core-xxx.jar包中已经集成了cglib类库,无须额外添加cgibJAR包.

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)
{
// 以类加载路径下的beans.xml作为配置文件,创建Spring容器
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
Person p1 = ctx.getBean("chinese", Person.class);
Person p2 = ctx.getBean("chinese", Person.class);
// 由于chinese Bean是singleton行为,
// 因此程序两次获取的chinese Bean是同一个实例。
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实例。