浅析Spring中bean的作用域
一、前言
刚刚花了点时间,阅读了一下Spring
官方文档中,关于bean
的作用域这一块的内容。Spring-4.3.21
官方文档中,共介绍了七种bean
作用域,这篇博客就来简单介绍一下这七种作用域的含义。毕竟只是阅读了一下文档,没有实际的使用经验,所有对于这些作用域的理解比较浅显,这篇博客就当是记笔记,或者也可以算是翻译文档中的内容了。
二、正文
2.1 Bean作用域的种类
在Spring
官方文档中,共提到了7
种不同的Bean
作用域,分别是:
- singleton(默认)
- prototype
- request
- session
- globalSession
- application
- websocket
需要注意的是,前两种是Spring
中bean
的基本作用域,而后五种,算是扩展的作用域,只能在web
应用中使用。下面我就来分别介绍一下这7
种不同的作用域。
2.2 singleton作用域
singleton(单例)是Spring中,bean默认的作用域。若一个bean
的作用域是单例的,那么每个IoC
容器只会创建这个bean
的一个实例对象。所有对这个bean
的依赖,以及获取这个bean
的代码,拿到的都是同一个bean
实例。Spring
容器在创建这个bean
后,会将它缓存在容器中(实际上是放在一个ConcurrentHashMap
中)。Spring中的bean不是线程安全的,所以只有在我们只关注bean能够提供的功能,而不在意它的状态(属性)时,才应该使用这个作用域。下面引用一张图来看看单例bean
:
需要注意的一点是,这里所说的单例,和设计模式中所提到的单例模式不同。设计模式中的单例,是强制一个类有且只有一个对象,我们如果不通过特殊的手段,将无法为这个单例类创建多个对象。而Spring
中的单例作用域不同,这里的单例指的是在一个Spring
容器中,只会缓存bean
的唯一对象,所有通过容器获取这个bean
的方式,最终拿到的都是同一个对象。但是在不同的Spring
容器中,每一个Spring
容器都可以拥有单例bean
的一个实例对象,也就是说,这里的单例限定在一个Spring
容器中,而不是整个应用程序。并且我们依然可以通过new
的方式去自己创建bean
。
2.3 prototype作用域
prototype
可以理解为多例。若一个bean
的作用域是prototype
,那么Spring
容器并不会缓存创建的bean
,程序中对这个bean
的每一次获取,容器都会重新实例化一个bean
对象。通常,如果我们需要使用bean的状态(属性),且这个状态是会改变的,那么我们就可以将它配置为这个作用域,以解决线程安全的问题。因为对于单例bean
来说,多个线程共享它的可变属性,会存在线程安全问题。下面引用一张图来描述这个作用域:
前面也提过,如果bean
的作用域是prototype
的,那么容器在创建完这个bean
后,并不会将它保存在容器中,这也就意味着,Spring
容器并不能为我们做这个对象的销毁工作(比如资源释放)。此时我们可以通过Spring
提供的接口,自定义一个后处理器,然后将这些bean
的引用存储在这个后处理器中,当容器回调这个后处理器的方法时,我们可以在方法中通过提前存储的bean
的引用,将它们销毁。
2.4 request作用域
request
作用域将bean
的使用范围限定在一个http
请求中,对于每一个请求,都会单独创建一个bean
,若请求结束,bean
也会随之销毁。使用request
作用域一般不会存在线程安全问题,因为在Web
应用中,每个请求都是由一个单独的线程进行处理,所有线程之间并不会共享bean
,从而不会存在线程安全的问题。
这个作用域只能使用在Web
应用中。如果使用的是注解扫描配置bean
,那么在bean
所属的类上使用@RequestScope
注解即可使用此作用域,若是基于xml
文件,则通过bean
的scope
配置项:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="request"/>
2.5 session作用域
session
作用域将bean
的使用范围一次在一次http会话
中,对于每一个会话,Spring
容器都会创建一个单独的bean
,若session
被销毁,则bean
也随之销毁。我们可以修改bean
的状态,这个修改只对当前会话可见,但是是否线程安全呢?Spring
文档中并未提及,但我认为不是线程安全的,因为每一个session
可以对应于多个request
,这些请求不一定就是串行执行的,比如说用户打开多个界面,同时进行多次操作,那后台将同时处理同一个session
的多个request
,此时并不能保证bean
的线程安全。
与request
作用域一样,session
作用域只能使用在Web
应用中。我们可以使用@SessionScope
将bean
指定为session
作用域,也可以使用xml
配置方式:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
2.6 globalSession作用域
这个作用域就比较特殊了,globalSession
作用域的效果与session
作用域类似,但是只适用于基于portlet
的web
应用程序中。Portlet
规范定义了globalSession
的概念,该概念在组成单个Portlet Web
应用程序的所有Portlet
之间共享(引用自Spring
文档)。说实话,在看到这里之前,我从来没听说过portlet
。我现在所学的,基本上都是基于Servlet
的web
应用程序,所有关于这个作用域,我也不理解。但是Spring
文档中有提到一点,那就是如果我们在基于Servlet的web应用程序中使用globalSession作用域,实际上容器使用session作用域进行处理。
这个作用域也只在web
应用中有效,上面也提过,具体是在基于portlet
的应用中有效。文档中只提出了一种使用方式,就是基于xml
:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>
2.7 application作用域
学过Servlet
的应该对application
作用域有所了解,在Servlet
程序中,有一个全局的ServletContext
对象,这个对象被整个web
应用所共享,我们可以通过setAttribute
方法向其中添加全局共享的数据。而Spring
中,application
作用域就是这么实现的,作用域为application
的bean
,将会被作为ServletContext
的属性,存储在其中,然后可以被全局访问,而且一个ServletContext
只会存储这个bean
的一个实例对象。ServletContext
被销毁,这个bean
自然也跟着被销毁。我们发现,这好像有点类似于singleton
这个作用域,确实非常类似,但是也有一些区别。单例bean
是一个Spring
只会创建一个,而这里的却是每个ServletContext
包含一个,不论有多少Spring
容器,bean
的数量只取决于ServletContext
,而且单例bean
只能通过容器去获取,是隐式的,而这种作用域的bean
却是公开的,存储在ServletContext
中,可直接通过ServletContext
获取。
application
作用域也只能用于web
应用中。使用方式和之前几种类似,可以通过@ApplicationScope
注解,也可以使用xml
配置文件:
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
2.8 websocket
websocket
是一种应用层的通信协议,它提供应用层的全双工通信,关于websocket
协议,可以参考我的这篇博客:计算机网络——简单说说WebSocket协议。而Spring
提供对websocket
协议的支持,于是就有了这么一个作用域。在我看的这个Spring
官方文档中,并没有对这个作用域进行详细描述,但是我们也可以通过名称推断出来。若一个bean
的作用域为websocket
,则只作用于一次websocket
通信,若连接被释放,则bean
自然也会被销毁。
2.9 单例bean依赖于多例bean
在我们的应用程序中,可能有这样一种情况——一个作用域为singleton
的bean
,有一个或多个作用域为prototype
的bean
,此时将会发生什么问题。对于单例bean
来说,属性注入只会发生在创建这个bean
的过程中,这也就意味着,单例bean
只会经历一次属性注入。也就是说,尽管这个单例bean
的属性是多例的,但是由于只有一次注入,所以后续使用到的这个多例属性,永远都是同一个。此时多例就失去了意义。那该如何解决呢?
方法主要有两种,第一种比较容易想到,就是对于单例bean
的多例属性,我们不让Spring
容器帮我们自动注入,而是我们自己编写一个工厂方法,在方法中通过getBean
等方式,手动地向容器请求这个多例bean
。由于bean
是多例的,每一次getBean
,实际上返回的都是一个新的实例对象。而在单例bean
需要用到这个多例bean
时,通过工厂方法获取。但是这种方式比较麻烦,也不利于维护。
第二种方式就比较简单了,Spring
提供了一种机制解决这个问题,那就是——方法注入。关于方法注入,可以参考我的这篇博客:Spring方法注入的使用与实现原理
三、总结
以上就对Spring
中,bean
的作用域做了一个大致的介绍,至少我们知道了每一个作用域是什么,以及大致的功能,不会在被问到的时候,连是什么都不知道。以上内容是我直接参考Spring
官方文档所编写,文档中的内容也不是太详细,有些描述也不是特别清晰,所以上面有些是我自己的理解,若存在不足或者错误,欢迎指正,共同进步。