###7.5 Bean 的作用域
当你创建一个bean定义时,你也创建了一个菜谱.你可以通过一个菜谱来创建bean的多个实例.
你不仅可以向bean里配置各种依赖和配置值,也可以定义bean的作用域.有7种作用域,你可以选择一种.你可以使用5种web项目中独有的scopes. bean作用域
Scope Description
singleton 当ioc容器启动时就生成一个bean实例
prototype 定义一个bean可以生成无数bean实例
request bean定义的生命周期和HTTP请求相关.每个http请求都有自己需要的bean的实例,web项目专用
session 定义一个与Http session生命周期相关的bean,web项目专用
globalSession http全局session的生命周期相关的.一般只用于Portlet环境.一般只在web相关的ApplicationContext中获取
application 定义一个bean定义与ServletContext相关.
websocket 定义一个bean的作用域与一个WebSocket相关.
####7.5.1 单例作用域 一个单例Bean只有一个共享的实例,且所有能通过id或者ids匹配到bean的请求,spring容器都会返回一个特定的bean的实例. 另一方面看,所有的单例bean都被容器统一生成,并存储到缓存里,有请求就到缓存里取出实例对象的引用.
spring 的单例Bean的概念不同于GOF书里提到的那个单例模式.GoF的单例模式是硬编码来控制class的作用域,即每个特定的类的实例只被ClassLoader加载一次.而spring的单例则是计算的是每个容器每个bean.这意味着如果你要在一个单一的spring容器只给一个class定义一个bean,那么spring容器会为该Bean加载有且只有一个实例.singleton是spring的默认作用域.要将一个bean定义为单例,可以这么写:
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<!-- 下面的例子和上面的是一样的 (singleton scope 是默认的) -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
7.5.2 原生类(prototype)作用域
bean的非单例,原生作用域部署结果是当对一个特定的bean发起请求时,会创建一个bean的实例.这样,该bean会注入到其他bean里或者你可以通过getBean()方法调用容器来请求他.通常,使用prototype作用域来定义有状态的bean,使用singleton来定义无状态的bean.
下面的图标表明如何使用spring prototype作用域.DAO对象一般不适合定义为原型,因为它不持有任何状态. 如下定义:
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>
不同于其他的作用域,spring不会管理原生bean的生命周期,容器会实例化,配置,另外还组装一个原生对象,或者把他扔给客户端,但不会对此有记录.因此,尽管初始化调用的方法调用不区分作用域,但对于prototype来说,配置的销毁时的调用不会被调用.客户端必须要清理prototype作用域的对象,并释放它们占用的昂贵的资源.要使spring容器来释放原生作用域持有的bean对象,你可用使用一个自定义的bean post-processor,它持有所有要清理的beans的引用.
一定程度上,spring 容器原生bean的角色是替代new 操作符.所有的生命周期都要客户端处理;
###7.5.3 拥有原生bean依赖的单例bean 当你使用带着原生bean依赖的单例bean时,可用看到这些依赖在实例化时会被释放.如果有原生作用的依赖注入到一个单例bean中,一个新的原生bean就会实例化并注入到单例bean中.这个原生实例是有容器提供的专供给单例bean的. 但是,如果你想单例bean在运行期间获得一个原生bean的新实例.这个是不可能的.因为当容器初始化该singletion bean并解决依赖时,注入只发生一次.如果你想要在运行时得到新的实例,可以参看7.4.6,方法注入.
7.5.4 request,session,global session,application,and Websocket scopes
这些bean是web项目专用的,你只能在spring ApplicationContext的web-aware应用中使用,例如XmlWebApplicationContext中才能获取. ###初始web配置 为了支持这五种作用域,定义这些bean之前需要一些少量配置;(single和prototype不需要). 如何完成这些配置取决于你的特定的Servlet环境.
如果你使用spring web mvc来获取作用域bean,实际上,这个请求是由DispatcherServlet或DispatcherPortlet处理的,而后不需要特定的步骤.DispatcherServlet和DispatcherPortlet早已暴露了一切相关状态.
如果你使用Servlet2.5的web 容器,请求不是有spring的DispatcherServlet来处理的,你需要注册这个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请求对象绑定到你要请求的正在提供服务的线程上.这可以使request,session作用域的bean在调用链中可用.
###请求作用域 如下的xml配置
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
当有请求发生时,spring容器会根据loginAction bean的定义来创建一个loginAction的实例.这是因为loginAction定义为http请求.你可以改变这个实例的状态,因为这个bean定义会产生很多实例,以对应每个请求.当你的请求完成了,request作用域的bean就会被抛弃.
当你使用注解驱动组件或java配置时,@RequestScope注解将会用来标识一个组件为requst作用域.
@RequestScope
@Component
public class LoginAction {
// ...
}
Session scope
思考一下的xml配置.
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
spring容器使用userPerferences的bean定义来创建了一个生命周期和HTTP Session 相同的userPerferences的实例.也就说,userperference Bean的定义是HTTP session 级别的.每个单独的HTTP Session都有一个特定的userPreferences 的bean.当一个Http Seesion被抛弃的话,那么这个bean也会被抛弃.
注解驱动或java的配置形式如下:
@SessionScope
@Component
public class UserPreferences {
// ...
}
###全局Session作用域 思考一下bean的定义:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="globalSession"/>
同session的相同,不过只是基于web的portlet项目使用.因为它只有global portlet Session有关.
当然,你编写了一个标准的Servlet-based的web项目,只要写了session scope的bean,即使有多个globalSession scope的bean,也不会报错. ###Application scope 思考以下的配置
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
spring容器会创建web项目相关的AppPreference Bean.这个bean的作用域是ServletContext级别的,作为一个常规的ServletContext的属性存储.它可以看出是一个singleton,但不同之处在于:它只是每个ServletContext的singleton,singleton是针对每个ApplicationContext的(一个应用中可能有好几个ApplicationContext).它实际是ServletContext的一个属性.
你可以使用 @ApplicationScope注解来表明其作用域
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
###Scoped beans as dependencies
spring 容器不仅管理对象的实例化,还要组装其协作对象.如果你想将一个request 作用域的bean注入到一个更长时间的bean里,你需要选择一个AOP代理的注入来替代这个request scope Bean.你需要注入一个和该scope Bean有相同接口的代理对象,可以在相关的作用域(例如http请求)中得到真实的目标对象,代理要调用该真实对象的方法.
你可以在single Beans中使用aop:scoped-proxy/标签,这个引用就可以通过相关的代理进行序列化和反序列化,这样就能重新获得单例bean的实例
你可以在原型bean之间使用aop:scoped-proxy/,在公共代理上每次方法的调用都可以重新创建该方法目标bean新的实例.
然而,作用域代理不是唯一的以生命周期安全的方法去获取更小作用域bean的方式.你可以简单的将你的注入点(如构造器/set方法 参数或者自动注入字段)宣布为ObjectFactory<MyTargetBean>,它允许你每次使用getObject()方法来按需获取实例,不管是否已持有该对象或存储该对象.
jsr-330 将该变种成为供应商(Provider).使用一个Provider<MyTargetBean>声明,每次获取要调用相应的get()方法.
如下的配置只是简单的一行,但你要明白这背后的原因和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/元素插入到scoped bean定义中.为什么request,session,globalSession,custom-scope级别的bean定义需要aop:scoped-proxy/元素呢?让我们分析一下单例bean的定义并对比为什么你要对上面的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将注入Session-scope 的bean userPreferences的实例.需要注意的是userManager是一个单例Bean,每个容器它只实例化一次,它的依赖也只注入一次.这意味着userManager实际上使用的都是相同的userPreferences对象.
你不需要将短生命的协作bean注入到长生命周期里,例如你不打算将session-scoped 协助bean注入到单例bean里.实际上,你需要一个单例的userManager对象,对于http Session的生命周期,你需要一个特定于每个http Session的UserPreferences对象.因此容器会创建一个拥有userPreference所有接口的对象,它还可以从作用域机制中得到真实UserPreference对象.容器将会把一个代理对象注入到UserManager bean里,bean却发现不到这点.在这个例子中,当UserManager实例调用了UserPreference的一个方法,它实际上是调用了代理对象的一个方法.这个代理对象会从HTTP Session中获取真正的UserPreference对象,并且代理已获得UserPreference对象的需要被调用的方法.
所以当你将request,session,globalSession-scoped 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>
####选择正确的代理类型 Choosing the type of proxy to create 默认的,当spring容器为标记aop:scoped-proxy/元素的bean创建一个代理时,一个CGLIB-based 类代理就会创建.
- cglib 代理只拦截公共方法的调用.不要在这个代理调用非公共方法,他们(代理)无法代理整个实际对象.
另外,你可用通过配置spring容器为这些scoped bean创建标准的JDK 接口代理,你要指明aop:scoped-proxy/元素中的proxy-targer-class属性为false.使用JDK 基于接口的代理意味着你的应用不需要额外的jar包来影响这些代理.但是,这也意味着你的scoped bean的class至少要实现一个接口,并且所有的协作对象必须通过接口才能注入这些scoped 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>
对于选择class-based后者interface-based代理,查看11.6章节,"proxy mechanisms".
###7.5.5自定义作用域(Custom scopes) bean作用域机制的扩展,你可用定义自己的作用域,甚至可以重定义已存在的作用域.尽管重新定义作用域是一个坏的实践,而且你不能重写内置的singleton和prototype作用域. ####新建一个自定义作用域(Creating a custom scope) 要将你的自定义作用域集成到spring容器,你需要实现org.springframework.beans.factory.config.Scope接口,本节会描述. Scope接口有四种方法从scope里获取对象,把它们从scope移除,并使他们销毁.
下面的方法是从以下的scope中返回对象.例如,session scope实现的将返回一个session-scoped 的bean(如果这个bean的实例不存在,这个方法会重新创建一个bean的新实例,并把它绑定到session中以便以后调用).
Object get(String name, ObjectFactory objectFactory)
下面的方法将从作用域中移除对象.还是以session scope 的实现为例,将会从以下的session里移除session-scoped bean.这个对象会返回,但是你如果这个带名词的对象找不到就会返回null.
Object remove(String name)
下面的方法注册作用域应该执行的回调,当这些它被销毁或当在此作用域中特定的对象被销毁时.查看javadoc或spring scoped实现来查看更多的销毁回调信息.
void registerDestructionCallback(String name, Runnable destructionCallback)
下面的方法会得到一个该作用域下的会话标志.这个标志不同于其他scope.对于一个session scoped实现来说,这个标志可能是session标志 ####Using a custom scope (使用自定义注解) 在你编写并测试你的自定义scope时,你需要让你的spring容器知道这些新的scope.下面的方法是spring注册新的scope的核心方法:
void registerScope(String scopeName, Scope scope);
这个方法是由ConfigurableBeanFactory接口申明的,它可以在大部分spring的ApplicationContext接口的实现类通过Beanfactory属性获的. registerScope(..)方法的第一参数是与scope相关的唯一命名,例如spring容器里singleton和prototype.第二个参数是你要注册的已经实现了Scope接口的类的实例.
你编写了一个自己的接口,并如此实现它.
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
然后你可用使用你的自定义的scope来创建bean的定义.
<bean id="..." class="..." scope="thread">
对于已实现的自定义scope,你不仅可以通过编程的方式去注册,也可以通过xml的方式去注册.你需要用到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>
当你将aop:scoped-proxy/放到一个 FactoryBean 实现里,由于这个工厂Bean自身的作用域,所以getObject()方法不会返回任何对象.