文章作者:Tyan
博客:noahsnail.com
3.5 Bean的作用域
当你创建bean定义时,你创建了一个配方用于创建bean定义中定义的类的实例。bean定义是配方的想法是很重要的,因为这意味着对于一个类,你可以根据一个配方创建许多对象实例。
你不仅能管理要插入对象中的的各种依赖和配置值,而且能管理对象的作用域,对象是从特定的bean定义中创建的。这种方法是强大且灵活的,你可以通过配置文件选择你创建的对象的作用域,从而代替Java类级别对象的内置作用域。定义的beans将部署成多种作用域中的一种:开箱即用,Spring框架支持六种作用域,如果你使用感知web的ApplicationContext
,你只可以使用其中的五种作用域。
下面的作用域支持开箱即用。你也可以创建一个定制的作用域。
表 3.3 bean作用域
作用域 | 描述 |
---|---|
singleton | (默认) 每个Spring IoC容器使单个bean定义只能创建一个对象实例。 |
prototype | 单个bean定义可以创建任何数量的对象实例。 |
request | 单个bean定义的创建实例的作用域为单个HTTP request的声明周期;也就是说,每个HTTP request有它自己的根据bean定义创建的实例。只在感知Spring ApplicationContext 的上下文中有效。 |
session | 单个bean定义的创建实例的作用域为HTTP Session 的生命周期. 只在感知Spring ApplicationContext 的上下文中有效。 |
application | 单个bean定义的创建实例的作用域为ServletContext 的生命周期。 只在感知Spring ApplicationContext 的上下文中有效。 |
websocket | 单个bean定义的创建实例的作用域为WebSocket 的生命周期。 只在感知Spring ApplicationContext 的上下文中有效。 |
从Spring 3.0,引入了
thread scope
作用域,但默认情况下是不注册的。更多的信息请看SimpleThreadScope
文档。关于怎么注册thread scope
作用域或任何其它的定制作用域的介绍,请看『Using a custom scope』小节。
3.5.1 单例作用域
单例bean只管理一个共享实例,id匹配bean定义的所有对beans的请求,Spring容器会返回一个特定的bean实例。
换言之,当你定义一个bean定义时,它的作用域为单例,Spring IoC容器会根据bean定义创建一个确定的对象实例。这个单独的实例存储在单例beans的缓存中,接下来的对这个命名bean的所有请求和引用都会返回那个缓存的对象。
Spring中的单例bean概念不同于《设计模式》书中定义的单例模式。设计模式中的单例是对对象的作用域进行硬编码,为的是每个类加载器只能创建一个特定类的实例。Spring单例作用域最好的描述是每个容器每个类。这意味着如果你在单个的Spring容器中为一个特定的类定义了一个bean,Spring只会根据bean定义创建一个类的实例。在Spring中单例作用域是默认的作用域。为了在XML定义一个单例bean,你可以像下面一样写,例如:
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
3.5.2 原型作用域
非单例模式,bean部署采用原型作用域时,每次产生一个特定bean的请求时都会创建一个新的bean实例。也就是说,这个bean会注入到另一个bean中或你可以在容器中通过调用getBean()
方法来请求它。通常,对于所有有状态的beans使用原型作用域,对于无状态的beans使用单例作用域。
下面的图阐述了Spring原型作用域。数据访问对象(DAO)通常是不会配置为原型的,因为一个典型的DAO不会有任何会话状态;对于作者来说很容易重用单例图的核心。
下面的例子在XML中定义一个原型bean:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
与其它作用域相比,Spring不管理原型bean的完整生命周期:容器初始化、配置,另外组装原型对象,并把它传递给客户端,之后不再记录原型实例。因此,虽然不管什么作用域初始化生命周期回调函数都会在所有对象上调用,但是在原型作用域的情况下,不会调用配置的销毁生命周期回调函数。客户端代码必须清理原型作用域的对象并释放原型bean拥有的昂贵资源。为了使Spring容器释放原型bean拥有的资源,尝试使用定制的bean后处理程序,它拥有需要清理的bean的引用。
在有些方面,关于原型作用域,Spring容器的角色像是Java中new
操作符的替代品。所有生命周期的管理必须由客户端处理。(Spring容器中更多关于bean生命周期的细节,请看3.6.1小节,"生命周期回调")。
3.5.3 含有原型bean依赖的单例bean
当你使用含有原型bean依赖的单例作用域bean时,要意识到依赖解析是在实例化时。因此如果你使用依赖注入将原型作用域的bean注入到单例作用域的bean中时,将会实例化一个新的原型bean并依赖注入到单例bean中。原型bean实例曾经是唯一提供给单例作用域的bean的实例。
假设你想在运行时让单例作用域的bean重复的获得原型作用域bean的新实例。你不能依赖注入原型作用域的bean到你的单例bean中,因为当Spring容器实例化单例bean,解析并注入它的依赖时,注入只发生一次。如果你在运行时不止一次需要原型bean的实例,请看3.4.6小节,"方法注入"。
3.5.4 Request、session、application和 WebSocket作用域
如果你使用感知web的Spring ApplicationContext
实现(例如XmlWebApplicationContext
),request
,session
,application
和websocket
作用域是唯一可用的作用域。如果你通过正规的Spring IoC容器例如ClassPathXmlApplicationContext
来使用这些作用域,会抛出IllegalStateException
异常,投诉使用了一个未知的bean作用域。
web配置初始化
为了支持request
,session
,application
和websocket
标准的bean作用域,在你定义你的bean之前需要进行一些较小的初始化配置。(对于标准作用域singleton
和prototype
,初始化步骤不需要的。)
如果你使用Servlet 2.5的web容器,在Spring的DispatcherServlet
之外处理请求(例如使用JSF或Struts时),你需要注册org.springframework.web.context.request.RequestContextListener
ServletRequestListener
。对于Servlet 3.0+,能通过WebApplicationInitializer
接口以编程方式处理。对于更早的容器,可以在应用程序的web.xml
文件中添加下面的声明来代替:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>
如果你的监听器设置有问题,作为一种选择,你可以考虑Spring的RequestContextFilter
。过滤器映射依赖于web应用程序的相关配置,因此你必须适当的更改它。
<web-app>
...
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
DispatcherServlet
,RequestContextListener
和RequestContextFilter
都是在做同样的事,也就是说将HTTP请求对象绑定到服务请求的Thread
上。这使得request作用域和session作用域的beans在更深一层的调用链中是可用的。
Request作用域
考虑下面的bean定义的XML配置:
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
对于每一个HTTP请求,Spring容器通过使用loginAction
定义创建一个新的LoginAction
bean实例。也就是说,loginAction
bean的作用域是在HTTP请求级别的。你可以任意改变创建的实例的内部状态,因为其它的根据loginAction
bean定义创建的实例不会看到这些状态的改变;它们对于每个单独的请求都是独有的。当请求处理完成时,请求作用域的bean被丢弃。
当使用注解驱动的组件或Java配置时,@RequestScope
注解能用来指定一个组件的作用域为request。
@RequestScope
@Component
public class LoginAction {
// ...
}
Session作用域
考虑下面的bean定义的XML配置:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
对于单个HTTP Session的生命周期,Spring容器通过userPreferences
bean定义创建一个UserPreferences
bean实例。换句话说,userPreferences
bean的有效作用域是HTTP Session级别的。正如request作用域的beans一样,你可以任意改变你想改变的创建的bean实例的内部状态,知道其它的使用根据userPreferences
bean定义创建的HTTP Session实例也不会看到这些内部状态的改变,因为它们对于每个单独的HTTP Session都是独有的。当HTTP Session被最终销毁时,Session作用域的bean也被销毁。
当使用注解驱动的组件或Java配置时,@SessionScope
注解能用来指定一个组件的作用域为session。
@SessionScope
@Component
public class UserPreferences {
// ...
}
Application作用域
考虑下面的bean定义的XML配置:
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
对于整个web应用而言,Spring容器根据appPreferences
bean定义只创建一次AppPreferences
bean的新实例。也就是说,appPreferences
bean的作用域是ServletContext
级别的,作为一个正规的ServletContext
特性来存储。这有点类似于Spring的单例bean,但在两个方面是不同的:它对于每个ServletContext
是单例的,而不是每个Spring ApplicationContext
(在任何给定的web应用中可能有几个ApplicationContext
),它是真正显露的,因此作为一个ServletContext
特性是可见的。
当使用注解驱动的组件或Java配置时,@ApplicationScope
注解能用来指定一个组件的作用域为Application。
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
具有作用域的bean作为依赖项
Spring IoC容器不仅管理对象的实例化,而且管理协作者(或依赖)的绑定。例如,如果你想将一个具有HTTP request作用域的bean注入到另一个具有更长生命周期作用域的bean中,你可能选择注入一个AOP代理来代替具有作用域的bean。也就是说,你需要注入一个代理对象,这个对象能显露与具有作用域的对象相同的接口,但也能从相关的作用域中(例如HTTP request作用域)得到真正的目标对象,能通过委派方法调用到真正的对象。
你也可以在作用域为
singleton
的beans之间使用<aop:scoped-proxy/>
,将通过中间代理的引用进行序列化,因此能通过反序列化重新获得目标的单例bean。
当将作用域为
prototype
的bean声明为<aop:scoped-proxy/>
时,每个在共享代理上的方法调用会引起一个新目标实例(调用朝向的)的创建。
通过生命周期安全的方式访问更短的作用域中beans,作用域代理也不是唯一的方式。你也可以简单的声明你的注入点(例如,构造函数/setter参数或自动装配领域)为
ObjectFactory<MyTargetBean>
,考虑到每次需要的时候通过getObject()
调用来取得索要的当前实例——没有分别控制实例或储存它。
JSR-300变量被称作
Provider
,对于每一次取回尝试使用Provider<MyTargetBean>
声明和对应的get()
调用。关于JSR-330整体的更多细节请看这儿。
下面例子中的配置只有一行,但对于理解它背后的"why"和"how"是重要的。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- an HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<!-- instructs the container to proxy the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.foo.SimpleUserService">
<!-- a reference to the proxied userPreferences bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
为了创建这样一个代理,你插入一个子元素<aop:scoped-proxy/>
到具有作用域的bean定义中(看"选择创建的代理类型"小节和38章,基于XML Schema的配置)。为什么bean定义的作用域为request
,session
和定制作用域级别需要<aop:scoped-proxy/>
元素?让我们检查下面的单例bean定义,并将它与你需要定义的前面提到的作用域进行比较(注意下面的userPreferences
bean定义按目前情况是不完全的)。
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在上面的例子中,单例bean userManager
通过引用被注入到具有HTTP Session
作用域的bean userPreferences
中。这的突出点是userManager
bean是单例:每个容器它将确定的被实例化一次,它的依赖(在这个例子中只有一个,userPreferences
bean)也只注入一次。这意味着userManager
bean只能对确定的同一个userPreferences
对象进行操作,也就是最初注入的那个对象。
当将一个短期作用域的bean注入到一个长期作用域的bean中时,这不是你想要的行为,例如将一个具有HTTP Session
作用域的协作bean作为一个依赖注入到一个单例bean中。当然,你需要一个单一的userManager
对象,对于HTTP Session
的生命周期,你需要一个特定的被称为HTTP Session
的userPreferences
对象。因此容器创建了一个与UserPreferences
类暴露相同的公共接口的对象(理想情况下是一个UserPreferences
实例),这个对象能从作用域机制中(HTTP request,Session等)取得真正的UserPreferences
对象。容器将这个代理对象注入到userManager
bean中,userManager
bean不会意识到UserPreferences
引用是一个代理。在这个例子中,当UserManager
实例调用依赖注入的UserPreferences
对象的方法时,它实际上调用的是代理中的一个方法。代理能从HTTP Session
中(在这个例子)取得真正的UserPreferences
对象,将方法调用委托到取得的真正的UserPreferences
对象上。
因此当注入具有request或session作用域的bean到协作对象中时,你需要下面的,正确的,完整的配置:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<aop:scoped-proxy/>
</bean>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
选择创建的代理类型
当Spring容器为具有<aop:scoped-proxy/>
标记的bean创建代理时,默认情况下,创建一个基于CGLIB的类代理。
CGLIB代理只拦截公有方法调用。在这个代理上不调用非公有方法;它们不能委托给实际作用域目标对象。
作为一种选择,对于这种具有作用域的bean你可以配置Spring容器创建标准JDK基于接口的代理,通过指定<aop:scoped-proxy/>
元素的proxy-target-class
特定的值为false
。使用JDK基于接口的代理意味着在你应用程序类路径中你不需要额外的库来支持这种代理的使用。然而,它也意味着具有作用域的bean的类必须实现至少一个接口,并且注入这个bean的所有协作者必须通过它接口中的一个来引用它。
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.foo.DefaultUserPreferences" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
<bean id="userManager" class="com.foo.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
关于选择基于类或基于接口代理的更多细节信心,请看7.6小节,"代理机制"。
3.5.5 定制作用域
bean作用域机制是可扩展的;你可以定义你自己的作用域,甚至重新定义现有的作用域,虽然后者被认为是一种不好的实践,你不能覆盖内置的singleton
作用域和prototype
作用域。
创建一个定制作用域
为了将你的定制作用域集成到Spring容器中,你需要实现org.springframework.beans.factory.config.Scope
接口,这一节将描述这个接口。对于怎样实现你自己作用域的想法,请看Spring框架本身提供的Scope
实现和Scope
文档,它们解释了你需要实现的方法的更多细节。
Scope
接口有四个方法,从作用域中取得对象,从作用域中移除对象,并且允许它们被销毁。
下面的方法从潜在的作用域返回对象。session作用域实现,例如,返回具有session作用域的bean(如果它不存在,这个方法返回一个bean的新实例,然后绑定到session中准备将来引用)。
Object get(String name, ObjectFactory objectFactory)
下面的方法从潜在作用域中移除对象。以session作用域实现为例,从潜在的session中移除session作用域的bean。对象应该被返回,但如果没有找到指定名字的对象会返回空。
Object remove(String name)
下面的方法是注册当作用域销毁时或当作用域中的指定对象销毁时,作用域应该执行的回调函数。销毁回调函数的更多信息请看文档或Spring作用域实现。
void registerDestructionCallback(String name, Runnable destructionCallback)
下面的方法是获得潜在作用域的会话标识符。每个作用域的标识符都是不同的。对于session作用域实现,标识符是session标识符。
String getConversationId()
使用定制作用域
在你编写和测试一个或多个定制Scope
实现之后,你需要让Spring容器感知到你的新作用域。下面是在Spring容器中注册一个新Scope
的主要方法:
void registerScope(String scopeName, Scope scope);
这个方法是在ConfigurableBeanFactory
接口中声明的,在大多数具体的ApplicationContext
实现中都可获得,在Spring中通过BeanFactory
属性得到。
registerScope(..)
方法中的第一个参数是关于作用域的唯一名字;Spring容器本身中的这种名字的例子是singleton
和prototype
。registerScope(..)
方法中的第二个参数是你想注册和使用的定制Scope
实现的真正实例。
假设你编写了你的定制Scope
实现并按如下注册。
下面的例子使用Spring包含的
SimpleThreadScope
,但默认是不注册的。这个用法说明与你自己的定制Scope
是一样的。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
然后创建具有你自己定制的Scope
规则的bean定义:
<bean id="..." class="..." scope="thread">
在定制Scope
实现后,你不会受限于作用域的程序注册。你也可以声明式的进行Scope
注册,使用CustomScopeConfigurer
类:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<bean id="bar" class="x.y.Bar" scope="thread">
<property name="name" value="Rick"/>
<aop:scoped-proxy/>
</bean>
<bean id="foo" class="x.y.Foo">
<property name="bar" ref="bar"/>
</bean>
</beans>
当你在
FactoryBean
实现中放入<aop:scoped-proxy/>
时,它是工厂bean本身具有作用域,不是从getObject()
中返回的对象。