Spring IOC 之Bean作用域

时间:2021-02-26 09:28:28

当你创建一个bean定义的时候,你创建了一份通过那种bean定义的bean的创建类的真正实力的处方。bean的定义是一个处方

的想法是很重要的的。因为这意味着,对于一个类你可以创建很多对象实例从一个单独的处方中。

你不但可以控制不同种类的依赖和配置值设置到从一个指定的bean定义中创建的对象中去,而且可以控制从一个指定的bean定义中

产生的对象的作用范围。这种方式是强大和灵活的,因为你可以选择你通过配置创建的对象的作用范围而不是在Java类级别来限制对象的作用范围。

Beans可以被定义为多个作用范围:直接来说,Spring框架支持五个作用范围,但是只用使用webaware ApplicationContext的时候,其中的三个才是可用的。

下面的五个范围是可以可以使用的,你可以创建一个个性化的范围:

范围 描述
单例(singleton) (缺省情况下)指定一个bean的定义是一个单独的对象实例对于每一个IoC容器
原型(prototype) z指定一个bean的定义可是是任何数目的对象实例
(请求)request 指定一个bean的定义的生命周期是一个单独的HTTP 请求;也就是说,每一个HTTP请求有自己的bean创建的实例,这个只是在一个web-aware的Spring ApplicationContext上下文中是合法有效的。
(会话)session 指定一个bean的定义的生命周期是一个HTTP session。这个只有在web-aware的Spring ApplicationContext的上下文环境中是合法有效的。
(全局会话)global session 指定一个bean的生命周期是一个全局的HTTP Session.一般来讲,在使用了组建上下文中是合法的;这个只有在web-aware的Spring ApplicationContext的上下文环境中是合法有效的
application 指明一个bean的定义的生命周期是一个servletContext。这个只有在web-aware的Spring ApplicationContext的上下文环境中是合法有效的

对于Spring3.0来说,一个线程作用范围是可用的,但是并不是默认情况下的设置。

1.单例作用域

只一个单例的bean只会有一个共享的实例,所有的通过id或者 ids对这个bean的请求都会通过Spring 容器返回一个明确的bean实例。

从另一个角度说,当你定义一个bean并且指定它的范围是单例的话,Spring IOc容器通过bean定义创建一个对象的实例。这个单独的实例存储在单例bean的缓存中,而且所有的后来的请求或者应用都会返回那个缓存的对象。

Spring IOC 之Bean作用域

Spring中的单例概念有别于Gang of Four设计模式中的单例模式。GoG 单例硬编码了对象的作用域范围,就像一个且仅有一个特别类的实例对象被每一个类加载器创建的。Spring单例的作用范围最好的诠释了一个容器一个bean。这以为找如果你定义

义了一个特别的类在一个单独的Spring容器中,然后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"/>

2.原型作用域

非单例,原型作用域的bean会在每次请求指明的bean创建的时候就会产生一个新的bean实例。也就是说,这个bean在注入其他bean的时候活在你通过容器中的getBean()方法调用的时候。

一般说来,对于所有的状态bean使用原型作用域,对于无状态bean使用单例作用域。

下面的图阐述了Spring的 原型作用域。 一个 DAO 并不是总是配置为一个原型类型,因为一个一般的DAO并不维持任何对话的状态;它只是更加容易去复用单例图的核心罢了:

Spring IOC 之Bean作用域

下面的例子在XML中定义了一个bean是原型类型:

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

和其他作用域相比较,Spring 并不管理一个原型类型bean的完整的生命周期:容器实例化、配置和其他的组装一个原型对象,而且把它分发给客户端,

并不记录太多关于原型实例的。因此,尽管初始化生命周期回调方法在所有的对象中被调用而不管他的作用域,在原型中,配置破坏的生命周期中并没有被调用。客户端代码

必须清理原型作用域的对象,并且释放通过原型作用域bean 所持有的资源,可以试着使用一个个性化的bean post-processor,

它持有需要被清理的bean的引用。

在某些方面,Spring 容器的角色在原型作用域的bean中是一个Java new 操作符的替代。所有的生命周期的管理都需要被客户端所处理。

3.含有原型bean依赖的单例bean

当你使用一个含有原型bean依赖的单例bean,记得通知依赖在实例化的时候被解析。如果你把一个原型作用域的bean注入到

一个单例的bean中,那么一个新的原型bean被实例化的时候就需要被依赖注入到单例bean中。 原型的实例是唯一的实例被设置到单例作用域的bean中的。

但是,假设在运行时你想把单例作用域的bean去请求一个原型作用域的bean重复的的。你不能把一个原型作用域范围的bean注入到一个单例的bean中,因为这种注入只会发生一次,当容器实例化单例bean并且解析它的依赖时。

如果你需要一个运行时的原型作用域的bean实例多次的话,可以看下“方法注入”。

4.Request, session, and global session作用域

Request, session, and global session作用域只会在你使用了ApplicationContext实现上下文web环境的时候才会起作用(XmlWebApplicatioinContext).

如果你在一个常规的IOC容器中使用的话(比如:ClassPathXmlApplicationContext),你就会得到一个IllegalStateException关于未知的bean作用域。

4.1初始化web配置

为了支持在request, session, 和 global session级别(基于web作用域的bean)的bean的作用域,在你定义你的bean之前,一些小的初始化配置是必须的。

怎样完成这个初始化步骤取决于你的特殊的Servlet环境。

如果你通过Spring web MVC来访问作用域范围的bean,在请求的内部这是通过Spring的 DispatcherServlet,或者是DispatcherPortlet,没有其他特殊的步骤需要:

因为DispatherServlet和DispatcherPortlet已经有了所有相关的状态。

如果你使用一个Servlet2.5的web 容器,这些请求不是使用Spring的 DispatcherServlet来处理的,你就需要注册org.springframework.web.context.request.RequestContextListener

ServletRequestListener了,对于Servlet3.0以上的话,这个可以通过WebApplicationInitializer接口自动的完成。

作为可选项,可以再你的web.xml文件中添加下面的声明:

    <web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>

可选项,如果你的监听器启动的时候出现问题,考虑提供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 在接下来的请求链中能够可用。

4.2 Request作用域

来看看下面的bean定义:

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

Spring容器通过使用loginAction bean 定义为每一个HTTP 请求创建了一个LoginAction bean 实例。也就是说,loginAction bean的作用域在

HTTP request级别。只要你愿意你可以改变创建的历史的内部状态,因为从相同bean 创建的其他实例不会看到

这些在状态上的改变;他们只是针对一个单独的请求。当请求完成过程中的时候,bean的作用域才会被失效。

4.3 Session作用域

看看下面的bean定义:

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

Spring 容器通过使用userPreferences bean定义创建了一个UserPreferences新的实例在单独的HTTP Session生命周期中。

换句话说,userPreferences bean 在作用在 HTTP Session 级别。和request-scoped的bean 相比,知道了其他的HTTP Session 实例只使用从userPreference bean定义中创建的实例看不到在状态中的改变,因为他们在针对单独的HTTP Session,所以只要你愿意你都可以修改创建的实例的状态。

当一个HTTP Session最终失效的时候,作用与指定HTTP Session作用域的bean也会失效。

4.4 Global Session作用域

看下面的bean 定义:

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

global session作用域和标准的HTTP Session作用域类似,而且只应用在基于组建的wweb 应用的上下文中。这个组建规范定义了

一个全局的Session的概率,它可以在组成单组建web应用中的所有张建忠共享。在global session级别的bean 定义

作用域在全局组件Session的整个生命周期中。

如果你写一个标准的基于Servlet的web 应用,而且你定义了一个或者多个拥有global session作用域的bean,

那么标准的HTTP session作用域就会被使用,也不会出现错误。

4.5 Application作用域

看下面的bean 定义:

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

Spring 容器通过使用appPreference bean 定义一次就为整个web应用创建一个新的AppPreferences实例。也就是说,

appPreference bean 作用在ServletContext 级别,作为ServletContext属性来存储。这个多少有点像Spring单例了,

但你两个方面有差异:每一个ServletContext都是一个单例,而不是每一个Spring ApplicationCOntext,而且他实

际上暴露了因此作为ServletContext属性是可见到。

4.5 Scoped beans as dependencies

Spring Ioc 容器管理了不仅仅是你的对象的实例化而且管理组装了协作对象。如果你想注入一个HTTP请求作用域的bean的bean

到另外一个bean中,你必须注入一个AOP代理在作用域bean的地方。也就是说,你需要注入一个代理的对象而且

暴露了相同的公共接口和作用域对象一样,但是可以得到真正的目标对象而且代理方法调用到真正的对象。

说明:你需要使用<aop:scoped-proxy/>在联合作用域为单例或者原型的bean时。

下面例子中的配置只是一行,但是它在理解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的定义中。让我们来检查

下下面的单例定义并且和你想去定义的前面的作用域范围做个对比。

<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 是一个单例:你只会在每个容器中实例化一次,并且它的依赖只会注入一次。这意味着

userManager bean 将只会操作在相同的userPreferences对象,也就是说,它就是原始注入的bean。

这不是你想要的行为在注入一个短作用域的bean到一个长作用域的bean的时候。例如,注入一个HTTP Session作于域的协作bean

到一个单例bean中。相当于,你需要一个单例的userManager对象,而且对于一个HTTP Session的生命周期中,

你需要一个指明了HTTP Session作用期的userPreference对象。然后容器创建了一个对象并且和UserPreferences类一样暴露相同的公共接口

,它可以通过作用域机制获取真正的UserPreferences对象。这个容器注入到这个代理对象到userManager bean,而它不知道UserPreference引用时一个代理。

在这个例子中,当一个UserManager实例调用在依赖注入的UserPreferences对象的方法,它实际上调用了在代理中的方法。

这个代理然后通过HTTP Session 获取到真正的UserPreferences对象,并且将这个方法调用委托到检真正的UserPreferences对象。

因此当注入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>

4.6选择需要去创建的代理的类型

在默认情况下,当Spring容器为一个用aop:scoped-proxy/元素标记的bean创建代理的时候,一个基于CGLIB类代理就被创建了。

注意:CGLIB代理只拦截公共方法的调用,所以不要在这个代理中调用一个非公共的方法。他们将不会委托给实际的目标对象。

作为可选项,通过指定aop:scoped-proxy/元素的proxy-target-class属性值为false,你可以配置Spring 容器去为这些作用域bean创建标准的JDK 基于接口的代理。使用JDK基于接口的代理意味着你不需要在你的应用程序中classpath中添加额外的库。但是,这个也意味着作用域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>

5.自定义作用域

bean的作用域机制是扩展的:可以定义自己的作用域也可是重写已经存在的作用域,尽管后者是被认为不好的做法而且你不能覆盖内置的单例和原型bean的作用域。

5.1创建一个自定义作用域

为了将你自定义的作用域整合到Spring容器中,你需要实现org.springframework.beans.factory.config.Scope接口。

Sscope接口有四个方法从作用域中获取对象,把他们从作用域中移除,并且允许他们被销毁。

下面的方法可以从下面的作用域中返回对象。举个例子,Session作用域的实现返回了session作用域的bean(如果它不存在的话,这个方法返回这个bean的一个新的实例,并且约束它为将来的引用在session的范围内)。

Object get(String name, ObjectFactory objectFactory)

下面的方法从下面的作用域中移除了对象。举个例子,Session作用域的实现移除了session作用域的bean。这个对象应该被返回,但是如果指明的名字没有被发现的时候你可以返回null。

Object remove(String name)

下面的方法注册了回调,当作用域销毁或者当在作用域中指明的对象被销毁的时候它应该运行的。

void registerDestructionCallback(String name, Runnable destructionCallback)

下面的方法对于下面的作用域有几个会话标识符。这个标识符对于每一个作用域都是不同的。对于一个session作用域的实现,这个标识符是session标识符。

String getConversationId()

5.2 使用自定义作用域

在你写完并且测试一个或者多个scope实现后,你需要告诉Spring容器你的心得作用域。下面的方法就是注册一个新的作用域到Spring容器的核心方法:

void registerScope(String scopeName, Scope scope);

这个方法是在ConfigurableBeanFactory声明的,这个借口是在在大部分ApplicationContext具体实现中通过BeanFactory属性作用在一起的。

registerScope(..) 方法的第一个参数是和作用域相关的唯一的名字;在Spring容器中像这样的名字有singletion 和 prototype。registerScope(..) 的第二个参数是一个你打算注册和使用的自定义scope实现的具体实现。

如果你写完了自定义的scope实现,可以像下面这样注入它:

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

然后你可以创建一个bean的定义,并且让它和你的自定义作用联系在一起:

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

对于一个自定义scope实现,你没有限制在程序中注册scope。你可以使用CustomScopeConfigurer类来声明式的scope注册:

gurer class:
<?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()返回的对象。