Spring | 3.5 Bean的作用域

3.5 Bean的作用域

当你构造bean定义时,你正在构建一个定义bean的类的实例的“处方”。bean定义成“处方”的思想很重要,因为它意味着,与类一样,你可以从一个“处方”创建许多对象实例。
你不仅可以控制要插入到由特定bean定义创建的对象中的各种依赖项和配置值,还可以控制由特定bean定义创建的对象的范围。这种方法功能强大且灵活,因为你可以选择通过配置创建的对象的范围,而不必在Java类级别上考虑对象的范围。bean可以被定义为部署在许多范围中的一个:开箱即用,Spring框架支持6种范围,其中5个只有在使用web感知的ApplicationContext时才可用。
以下范围是支持开箱即用。你还可以创建自定义范围。
表3.3 bean范围

范围描述
singleton(默认值)为每个Spring IoC容器将一个bean定义的范围限定为一个对象实例。
prototype将单个bean定义的范围限定为任意数量的对象实例。
request将单个bean定义的范围限定为单个HTTP请求的生命周期;也就是说,每个HTTP请求都有自己的bean实例,该实例是在单个bean定义的基础上创建的。仅在可感知web的Spring ApplicationContext上下文中有效。
session将单个bean定义的范围限定为HTTP会话的生命周期。仅在可感知web的Spring ApplicationContext上下文中有效。
application将单个bean定义的范围限定为ServletContext的生命周期。仅在可感知web的Spring ApplicationContext上下文中有效。
websocket将单个bean定义的范围限定为WebSocket的生命周期。仅在可感知web的Spring ApplicationContext上下文中有效。

注意:从Spring 3.0开始,线程范围是可用的,但默认情况下没有注册。有关更多信息,请参阅SimpleThreadScope的文档。有关如何注册此或任何其他自定义范围的说明,请参阅“使用自定义范围”一节。

3.5.1 单例范围

只管理一个单例bean的一个共享实例,所有对具有与该bean定义匹配的id或id的bean的请求都会导致Spring容器返回该特定的bean实例。
换句话说,当你定义一个bean定义,并且它的作用域是一个单例对象时,Spring IoC容器将创建该bean定义定义的对象的一个实例。此单一实例存储在此类单例bean的缓存中,对于该命名bean的所有后续请求和引用都返回缓存的对象。
单利范围
Spring的单例bean概念不同于Gang of Four (GoF)模式书中定义的单例模式。GoF单例硬编码对象的范围,这样每个类加载器只能创建一个特定类的一个实例。Spring单例的范围最好描述为每个容器和每个bean。这意味着,如果你在单个Spring容器中为特定类定义一个bean,那么Spring容器将创建由该bean定义定义的类的一个且仅一个实例。单例范围是Spring中的默认范围。要将bean定义为XML中的单例对象,可以这样写:

<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()方法调用请求它。通常,对所有有状态bean使用原型范围,对无状态bean使用单例范围。
下图说明了Spring prototype作用域。数据访问对象(DAO)通常不配置为原型,因为典型的DAO不包含任何会话状态;对于本文作者来说,重用单例图的核心更容易。
原型范围
下面的例子将bean定义为XML中的原型:

<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>

与其他作用域不同,Spring不管理原型bean的完整生命周期:容器实例化、配置和以其他方式组装原型对象,并将其传递给目标,而没有该原型实例的进一步记录。因此,尽管初始化生命周期回调方法会在所有对象上调用,而不考虑范围,但是在原型的情况下,配置的销毁生命周期回调不会被调用。目标代码必须清理原型范围的对象,并释放原型bean所持有的昂贵资源。要让Spring容器释放原型作用域bean所持有的资源,可以尝试使用自定义bean后处理器,它持有对需要清理的bean的引用。
在某些方面,Spring容器在原型作用域bean方面的角色是Java new操作符的替代。超过此点的所有生命周期管理都必须由客户机处理。(有关Spring容器中bean的生命周期的详细信息,请参见3.6.1节“生命周期回调”。)

3.5.3 具有原型bean依赖项的单例bean

当你使用依赖于原型bean的单作用域bean时,请注意依赖项是在实例化时解析的。因此,如果将一个原型作用域bean注入到一个单例作用域bean中,就会实例化一个新的原型bean,然后将依赖注入到单例bean中。原型实例是惟一提供给单例作用域bean的实例。
但是,假设你希望单例作用域bean在运行时重复获取原型作用域bean的新实例。你不能依赖—将一个原型作用域bean注入到你的单例bean中,因为当Spring容器实例化单例bean并解析和注入它的依赖项时,这种注入只发生一次。如果在运行时不止一次需要原型bean的新实例,请参见3.4.6节“方法注入”。

3.5.4请求、会话、应用程序和WebSocket作用域

只有在使用web的Spring ApplicationContext实现(例如XmlWebApplicationContext)时,才可以使用请求、会话、应用程序和websocket范围。如果将这些作用域与常规Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,则会抛出一个IllegalStateException异常,产生未知的bean作用域。

最初的web配置

为了在请求、会话、应用程序和websocket级别(web范围bean)上支持bean的作用域,在定义bean之前需要进行一些较小的初始配置。(标准范围、单例和原型不需要这个初始设置。)
如何完成这个初始设置取决于特定的Servlet环境。
如果在Spring Web MVC中访问作用域bean,实际上是在由Spring DispatcherServlet处理的请求中访问,那么不需要进行特殊设置:DispatcherServlet已经公开了所有相关状态。
如果使用Servlet 2.5 web容器处理Spring DispatcherServlet之外的请求(例如,当使用JSF或Struts时),则需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。对于Servlet 3.0+,这可以通过WebApplicationInitializer接口以编程方式完成。或者,对于较旧的容器,将以下声明添加到web应用程序的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请求对象绑定到服务该请求的线程。这使得请求和会话范围的bean在调用链的更下方可用。

Request作用域

考虑bean定义的以下XML配置:

<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>

Spring容器通过为每个HTTP请求使用LoginAction bean定义来创建LoginAction bean的新实例。也就是说,loginAction bean的作用域在HTTP请求级别。你可以随意更改创建的实例的内部状态,因为从相同的loginAction bean定义创建的其他实例不会在状态中看到这些更改;它们是针对单个请求的。当请求完成处理时,将丢弃作用域为请求的bean。
当使用注解驱动的组件或Java配置时,可以使用@RequestScope注解将组件分配给请求范围。

@RequestScope
@Component
public class LoginAction {
	// ...
}
Session作用域

考虑bean定义的以下XML配置:

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

Spring容器通过为单个HTTP会话的生命周期使用UserPreferences bean定义来创建UserPreferences bean的新实例。换句话说,userPreferences bean有效地限定在HTTP会话级别的范围内。与请求范围内bean一样,你可以随意改变创建的内部状态的实例,然而使用相同的userPreferences bean定义创建实例的其他HTTP会话实例看不到这些变化状态,因为他们是特定于一个单独的HTTP会话。当HTTP会话最终被丢弃时,作用域为该特定HTTP会话的bean也被丢弃。
当使用注解驱动的组件或Java配置时,可以使用@SessionScope注解将组件分配给会话范围。

@SessionScope
@Component
public class UserPreferences {
	// ...
}
Application作用域

考虑bean定义的以下XML配置:

<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>

Spring容器通过为整个web应用程序使用一次AppPreferences bean定义来创建AppPreferences bean的新实例。也就是说,appPreferences bean的作用域在ServletContext级别,存储为一个常规的ServletContext属性。这有点类似于Spring单例bean,但在两个重要方面有所不同:它是每个ServletContext的单例bean,而不是每个Spring ‘ApplicationContext’(在任何给定的web应用程序中都可能有多个应用程序);它实际上是公开的,因此作为ServletContext属性可见。
当使用注解驱动的组件或Java配置时,可以使用@SessionScope注解将组件分配给会话范围。

@ApplicationScope
@Component
public class AppPreferences {
	// ...
}
注入范围作用域的bean

Spring IoC容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的注入。如果你想将(例如)一个HTTP请求作用域bean注入到另一个作用域更长的bean中,你可以选择注入AOP代理来代替作用域bean。也就是说,你需要注入一个代理对象,它公开与作用域对象相同的公共接口,但也可以从相关作用域(例如HTTP请求)检索实际目标对象,并将委托方法调用注入到实际对象。
你还可以在范围为单例的bean之间使用< aop:scope -proxy/>,然后引用通过一个可序列化的中间代理,从而能够在反序列化时重新获得目标单例bean。
当针对范围原型的bean声明< aop:scope -proxy/>时,共享代理上的每个方法调用都将创建一个新的目标实例,然后将调用转发到该实例。
而且,范围代理不是以生命周期安全的方式从较短范围访问bean的唯一方式。你还可以简单地将注入点(即构造函数/setter参数或自动装配字段)声明为ObjectFactory< MyTargetBean>,允许每次需要时调用getObject()根据需要检索当前实例——而不需要保存实例或单独存储实例。
作为扩展的变体,你可以声明ObjectProvider< MyTargetBean>,它提供了几个额外的访问变体,包括getIfAvailable和getIfUnique。
JSR-330变体称为Provider,在每次检索尝试时都使用Provider< MyTargetBean>声明和相应的get()调用。有关JSR-330的详细信息,请参阅这里

下面示例中的配置只有一行,但是理解它背后的“为什么”和“如何”非常重要。

<?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模式的配置)。为什么定义在请求、会话和自定义范围级别上作用域的bean需要< 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会话范围的bean userPreferences的引用。这里的要点是userManager bean是一个单例:它将针对每个容器实例化一次,并且它的依赖项(在本例中只有一个,userPreferences bean)也只注入一次。这意味着userManager bean将只操作完全相同的userPreferences对象,即最初注入它的对象。
当将一个较短生存期的作用域bean注入到一个较长生存期的作用域bean中时,这不是你想要的行为,例如将一个HTTP会话作用域的协作bean作为依赖项注入到单例bean中。相反,在HTTP会话的生存期内,你需要一个单一的userManager对象注入到一个HTTP会话的userPreferences对象。因此容器创建了一个对象,该对象公开了与UserPreferences类完全相同的公共接口(理想情况下,该对象是UserPreferences实例),该对象可以从作用域机制(HTTP请求、会话等)获取真正的UserPreferences对象。容器将此代理对象注入userManager bean,该bean不知道此UserPreferences引用是代理。在本例中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,它实际上正在调用代理上的方法。然后代理从HTTP会话(在本例中)获取实际的UserPreferences对象,并将方法调用委托给检索到的实际UserPreferences对象。
因此,在将请求和会话范围的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:scope -proxy/>元素标记的bean创建代理时,将创建一个基于cglib的类代理。

注意:CGLIB代理只拦截公共方法调用!不要在这样的代理上调用非公共方法;它们不会被委托给实际作用域的目标对象。

或者,你可以配置Spring容器,通过为< aop:scoped-proxy/>元素的proxy-target-class属性的值指定false,为这种范围bean创建标准的基于JDK接口的代理。使用基于JDK接口的代理意味着在应用程序类路径中不需要额外的库来实现这种代理。但是,这也意味着作用域bean的类必须实现至少一个接口,并且所有注入作用域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作用域机制是可扩展的;你可以定义自己的范围,甚至重新定义现有的范围,尽管后者被认为是不好的实践,并且你不能覆盖内置的单例和原型范围。

创建一个自定义范围

要将自定义范围集成到Spring容器中,需要实现org.springframework.beans.factory.config.Scope接口,这将在本节中描述。要了解如何实现你自己的范围,请参阅Spring框架本身提供的范围实现和范围javadocs,这将更详细地解释你需要实现的方法。
Scope接口有四种方法来从范围中获取对象、从范围中删除对象并允许销毁对象。
下面的方法从基础范围返回对象。例如,会话范围实现返回会话范围的bean(如果它不存在,则方法在将其绑定到会话以供将来引用后返回bean的新实例)。

Object get(String name, ObjectFactory objectFactory)

下面的方法从基础范围中删除对象。例如,会话范围实现从基础会话中删除会话范围bean。应该返回该对象,但是如果没有找到具有指定名称的对象,则可以返回null。

Object remove(String name)

以下方法注册范围在销毁或销毁范围中的指定对象时应该执行的回调。有关销毁回调的更多信息,请参考javadocs或Spring作用域实现。

void registerDestructionCallback(String name, Runnable destructionCallback)

下面的方法获取底层范围的对话标识符。这个标识符对于每个范围都是不同的。对于会话范围的实现,此标识符可以是会话标识符。

String getConversationId()
使用自定义范围

在编写和测试一个或多个自定义范围实现之后,你需要让Spring容器知道你的新范围。下面的方法是向Spring容器注册新范围的中心方法:

void registerScope(String scopeName, Scope scope);

这个方法是在ConfigurableBeanFactory接口(大多数具体的ApplicationContext实现都可以使用这个接口)上声明的,通过BeanFactory属性随Spring一起发布。
registerScope(…)方法的第一个参数是与范围关联的惟一名称;Spring容器本身中此类名称的例子有singleton和prototype。registerScope(…)方法的第二个参数是你希望注册和使用的自定义范围实现的实际实例。
假设你编写了自定义范围实现,然后像下面这样注册它。
注意:下面的示例使用SimpleThreadScope,它包含在Spring中,但默认情况下没有注册。对于你自己的自定义范围实现,说明将是相同的。

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

然后创建符合自定义范围的范围规则的bean定义:

<bean id="..." class="..." scope="thread">

使用自定义范围实现,你不局限于范围的编程注册。你还可以使用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()返回的对象。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值