Bean的作用域

Spring容器创建的Bean默认是单例的。Spring容器调用配置方法完成Bean的创建之后,Bean就缓存在Spring容器里。之后每次调用同一配置方法创建Bean,Spring容器只会返回缓存在Spring容器里的Bean,不再创建新的Bean。这意味着同一配置方法在同一Spring容器里无论被调用了多少次,都只会返回同一实例的Bean。因此,Spring容器创建的Bean默认是单例的。同时我们也应注意到,这里的单例与单例设计模式里的单例是有区别的,不能混为一谈。单例设计模式里的单例指的是类的实例由类的加载器方法创建,无论类的加载器方法被调用了多少次,都只会返回同一实例。

在Web开发中,我们通常只需创建单例的Bean。因为诸如控制器之类的Bean是无状态的。无论哪个用户发来请求,都能使用同一控制器实例处理,根本就不需要再创建新的控制器实例。然而对于一些类,比如数据模型类,每个请求所产生或获取的数据都是不一样的。这意味着这样的类是有状态的。把这些有状态的类创建为单例的显然不妥。作为替代,我们通常选择创建这些类的域对象(Domain Object),通过new关键字在Bean的方法中创建这些类的实例。因此,在Web开发中,我们往往只需告诉Spring容器创建单例的Bean

然而,在某些罕见的应用场景中,我们可能需要创建非单例的Bean。这意味着除了单例(Singleton)作用域之外,Spring容器还需支持创建具有其它作用域的Bean。具体如下:
1.原型(Prototype):Spring容器每次调用配置方法创建Bean时都会重新创建Bean的实例,调用几次就创建几个实例。
2.请求(Request):请求指的是Web请求,只有Web相关的Spring容器(比如XmlWebApplicationContext)才支持请求作用域。指定作用域为请求后,同一配置方法在同一Web请求里无论被调用了多少次,都只会创建一个Bean的实例。
3.会话(Session):会话指的是Web会话,只有Web相关的Spring容器(比如XmlWebApplicationContext)才支持会话作用域。指定作用域为会话后,同一配置方法在同一Web会话里无论被调用了多少次,都只会创建一个Bean的实例。

Bean的作用域可由@Scope注解配置。@Scope注解有个String类型的value属性。我们可把singleton(单例),prototype(原型),request(请求)或session(会话)这些字符串指给@Scope注解,告诉Spring容器创建具有相应作用域的Bean。如下所示:

1 @Bean("music")
2 @Scope(value="singleton")
3 public Music produceMusic() {
4     return new Music("Dream");
5 }

当然,@Scope注解除了可以加到配置方法之外,也能以同样的方式加到带有@Component注解的组件上。至于XML配置文件,则可使用XML的scope属性这样配置:

1 <bean id="music" class="com.dream.Music" scope="singleton">
2     <constructor-arg value="Dream" />
3 </bean>

于是,我们弄清楚了单例,原型,请求,会话这些作用域。却也开始感到困惑:“假如把原型作用域的Bean注入到单例作用域的Bean中,这时会怎么样?”

毫无疑问,这是一个问题。Spring容器创建单例的Bean时就把原型的Bean注入进去了。之后,Spring容器每次用到单例的Bean时都是从Spring容器那里获取的,没再创建新的实例。这意味着原型的Bean只在注入的时候创建了一次,之后一直被单例的Bean引用着,无论单例的Bean用了多少次原型的Bean,原型的Bean始终是注入时的那个实例。如果我们希望单例的Bean每次用到原型的Bean时,原型的Bean都会返回一个新的实例,则需要做些额外的配置。而这配置,其中之一就是查找方法注入(Lookup Method Injection)

简单来说,查找方法注入就是单例的Bean每次用到原型的Bean时,都会调用指定的方法从Spring容器那里获取原型的Bean。而从Spring容器那里获取原型的Bean时,Spring容器总会返回新的实例。如此一来,单例的Bean每次用到原型的Bean时,原型的Bean的实例就总是新的了。

假如现有这样一个原型作用域的Bean:

 1 @Scope("prototype")
 2 @Component("music")
 3 public class Music {
 4     private String musicName = null;
 5 
 6     public Music(@Value("Dream") String musicName) {
 7         this.musicName = musicName;
 8     }
 9 
10     // 省略getter, setter方法
11 }

我们希望把它注入到单例作用域的Bean里。这时可以这样定义单例作用域的Bean:

1 @Component("player")
2 public abstract class Player {
3     @Lookup(value="music")
4     protected abstract Music getPlayingMusic();
5 
6     // 省略其它代码
7 }

这是一个抽象类,定义了一个抽象方法,用于获取Music类型的Bean。特别引人注目的是,抽象方法上面带着一个神秘的@Lookup(value="music")注解。

这是怎么回事呢?

原来,Spring容器瞧见@Lookup注解之后就会生成一个代理类。代理类将重写带有@Lookup注解的抽象方法,使之具有这样的功能:从Spring容器那里查找@Lookup注解指定的Bean,并在找到之后进行返回。这样一来,单例的Bean每次用到原型的Bean时,都会调用代理方法从Spring容器那里获取原型的Bean。而从Spring容器那里获取的原型的Bean的实例总是新的,从而使单例的Bean每次用到原型的Bean时,用的都是新的实例。

因此,@Lookup注解有个String类型的value属性,用于指定即将查找的Bean的ID。如果没有指定value属性,代理方法就会查找与代理方法的返回值的类型一样的Bean

在我们的配置中,我们在抽象方法getPlayingMusic上添加了@Lookup(value="music")注解,告诉Spring容器生成代理类,使单例的Player每次用到的原型的Music都是新的实例。

还有,XML也支持同样的配置。具体如下:

1 <beans  /* 省略命名空间和XSD模式文件声明 */>
2     <bean id="music" class="com.dream.Music" scope="prototype">
3         <constructor-arg value="Dream" />
4     </bean>
5 
6     <bean id="player" class="com.dream.Player">
7         <lookup-method name="getPlayingMusic" bean="music" />
8     </bean>
9 </beans>

这段代码使用XML配置了两个Bean:

1.一个Bean是Music类型的,其作用域是原型的。
2.一个Bean是Player类型的,其作用域没有指定,默认是单例的。

特别需要留意的是,配置Player类型的Bean时用到了 <lookup-method name="getPlayingMusic" bean="music" /> 元素。Spring容器瞧见这个元素之后,就会生成一个代理类。代理类将重写<lookup>元素的name属性指定的方法,使之每次被调用的时候,都从Spring容器那里获取<lookup>元素的bean属性指定的Bean。如此一来,单例的Player每次用到原型的Music时,用的就都是新的实例了。

于是,我们弄清楚了怎样把原型的Bean注入单例的Bean里,可这并不意味着我们可以停下探索的脚步。因为把请求作用域的Bean注入单例作用域的Bean里也有同样的问题。

假如现有这样一个单例作用域的Bean:

1 @Component
2 public class Player {
3     private Music playingMusic = null;
4 
5     @Autowired
6     public Player(Music playingMusic) {
7         this.playingMusic = playingMusic;
8     }
9 }

我们希望注入Player构造函数的是一个请求作用域的Music类型的Bean。这时可以这样配置Music:

 1 @Component
 2 @Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS)
 3 public class Music {
 4     private String musicName = null;
 5 
 6     public String getMusicName() {
 7         return this.musicName;
 8     }
 9 
10     @Value("Dream")
11     public void setMusicName(String musicName) {
12         this.musicName = musicName;
13     }
14 }

Music类上带着@Scope(value="request", proxyMode = ScopedProxyMode.TARGET_CLASS)注解,其value属性的值是 request ,表明该Bean的作用域是请求。同时我们也注意到了,@Scope注解还有一个proxyMode属性,其值是ScopedProxyMode.TARGET_CLASS

这是怎么回事呢?

原来,proxyMode属性是ScopedProxyMode枚举类型的,能够告诉Spring容器生成代理的方式。具体如下:
1.NO:告诉Spring容器无需生成代理。
2.TARGET_CLASS:告诉Spring容器基于类生成代理。
3.INTERFACES:告诉Spring容器基于接口生成代理。
4.DEFAULT:默认的代理方式。默认与NO一样,用于告诉Spring容器无需生成代理。也可通过配置,使之告诉Spring容器默认基于类或接口生成代理。

由此可知,如果把ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES指给proxyMode属性,Spring容器就会生成代理类。Spring容器创建Bean的时候,只会创建代理类的Bean。因此,Spring容器把请求作用域的Bean注入到单例作用域的Bean时,注入的实际是代理类的Bean。如此一来,单例的Bean用到请求的Bean时,用的实际是代理类的Bean。代理类的Bean会先判断一下当前是不是在同一Web请求里:如果是,则返回缓存在Spring容器里的Bean;如果不是,则再创建一个新的实例进行返回。从而使单例的Bean用到请求的Bean时,不同的Web请求将会返回Bean的不同实例。

Spring容器生成代理的方式有两种:一种是基于类生成代理;一种是基于接口生成代理。如果希望基于类生成代理,可把@Scope注解的proxyMode属性的值置为ScopedProxyMode.TARGET_CLASS。Music类上的@Scope注解的proxyMode属性的值就是ScopedProxyMode.TARGET_CLASS;如果希望基于接口生成代理,则必须让我们的类实现某个接口。因此,配置之前我们首先需要定义一个接口:

1 public interface IMusic {
2     public String getMusicName();
3     public void setMusicName(String musicName);
4 }

之后让Music类实现IMusic接口,并把proxyMode属性的值置为ScopedProxyMode.INTERFACES:

 1 @Component
 2 @Scope(value="request", proxyMode = ScopedProxyMode.INTERFACES)
 3 public class Music implements IMusic {
 4     private String musicName = null;
 5 
 6     @Override
 7     public String getMusicName() {
 8         return this.musicName;
 9     }
10 
11     @Override
12     @Value("Dream")
13     public void setMusicName(String musicName) {
14         this.musicName = musicName;
15     }
16 }

最后把注入Player的Music改成IMusic接口:

 1 @Component
 2 public class Player {
 3     private IMusic playingMusic = null;
 4 
 5     public IMusic getPlayingMusic() {
 6         return this.playingMusic;
 7     }
 8 
 9     @Autowired
10     public Player(IMusic playingMusic) {
11         this.playingMusic = playingMusic;
12     }
13 }

于是,基于接口生成代理的配置就完成了。当然,这里只讲了怎样进行请求作用域的注入。可实际上,会话作用域也有同样的问题。我们只需进行同样的配置就行了,不再赘叙。还有,如果想用XML进行同样的配置,则可提供一个XML配置文件配置如下:

 1 <beans /* 省略命名空间和XSD模式文件声明 */
 2        xmlns:aop="http://www.springframework.org/schema/aop"
 3        xsi:schemaLocation="
 4        /* 省略命名空间和XSD模式文件声明 */
 5        http://www.springframework.org/schema/aop
 6        http://www.springframework.org/schema/aop/spring-aop.xsd">
 7 
 8     <bean id="music" class="com.dream.Music" scope="request">
 9         <aop:scoped-proxy proxy-target-class="false" />
10         <property name="musicName" value="Dream" />
11     </bean>
12 
13     <bean id="player" class="com.dream.Player">
14         <constructor-arg ref="music" />
15     </bean>
16 </beans>

这段配置引入了spring-aop.xsd模式文件。这是一个用于配置面向切面编程的模式文件,我们将在介绍面向切面编程的时候另行介绍。现在只需知道这个模式文件定义了个<aop:scoped-proxy>元素,用于配置作用域代理。里面有个proxy-target-class属性,用于配置生成代理的方式:如果proxy-target-class属性的值是TRUE,则基于类生成代理;如果proxy-target-class属性的值是FALSE,则基于接口生成代理。proxy-target-class属性的值默认是TRUE

至此,关于Bean的作用域的介绍也就告一段落了。下章,我们将会开始介绍事件的监听与发布。欢迎大家继续阅读,谢谢大家!

返回目录    下载代码

posted @ 2022-04-10 19:27  林雪波  阅读(184)  评论(0编辑  收藏  举报