Spring Bean Scope

Posted carl-zhao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Bean Scope相关的知识,希望对你有一定的参考价值。

当你创建一个 bean 定义时,可以在这个 bean 定义当中设置你需要创建对象实例的 Class 来创建实例,这可以认为是一次。可以把 bean 定义当成一个食谱。把 bean 定义当是一个菜谱的想法很重要,因为这意味着使用同一个 Class 可以从一个菜谱创建许多对象实例。

你不仅可以控制插入到从特定bean定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的范围。这种方法功能强大且灵活,因为你可以通过配置选择创建的对象的范围,而不必在 Java 类级别固定对象的范围。bean 可以被定义为部署在多个作用域中的一个:开箱即用,Spring 框架支持 5 个作用域,其中 3 个只有在你使用 web 感知的 ApplicationContext 时才可用。

以下范围是开箱即用的。还可以创建自定义范围。

ScopeDescription
singleton(默认)将每个Spring IoC容器的单个bean定义定义为 singleton 对象实例。
prototype将单个bean定义作用于任意数量的对象实例。
request将单个bean定义作用于单个 HTTP 请求的生命周期;也就是说,每个HTTP请求都有自己的bean实例,该实例是根据单个bean定义创建的。只在 web 感知Spring ApplicationContext 的上下文中有效。
session将单个bean定义作用于 HTTP 会话的生命周期。只在 web 感知 Spring ApplicationContext 的上下文中有效。
global session将单个bean定义作用于全局 HTTP 会话的生命周期。通常只有在portlet上下文中使用时才有效。只在web感知Spring ApplicationContext的上下文中有效。
application将单个bean定义作用于 ServletContext 的生命周期。只在 web 感知 Spring ApplicationContext 的上下文中有效。

从Spring 3.0开始,线程作用域是可用的,但默认情况下没有注册。有关更多信息,请参阅SimpleThreadScope 的文档。

1、singleton scope

只管理一个单例 bean 的一个共享实例,对具有一个或多个id与该bean定义匹配的bean的所有请求都会导致 Spring 容器返回一个特定的 bean 实例。

换句话说,当你定义一个 bean 定义,并且它的作用域为单例时,Spring IoC 容器恰好创建该bean 定义所定义的对象的一个实例。此单一实例存储在此类单例 bean 的缓存中,对该命名bean 的所有后续请求和引用将返回缓存的对象。


Spring 的单例 bean 概念不同于四人组(GoF)模式书中定义的单例模式。单例GoF对对象的作用域进行了硬编码,使得每个 ClassLoader 只创建一个特定类的实例。Spring 单例的范围最好描述为每个容器和每个 bean 。这意味着,如果你为单个 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"/>

2、prototype scope

bean 部署的非单例、原型范围会导致每次对特定bean发出请求时创建一个新的 bean 实例。也就是说,该 bean 被注入到另一个 bean 中,或者你通过容器上的 getBean() 方法调用请求它。作为一条规则,对所有有状态 bean 使用原型作用域,对无状态 bean 使用单例作用域。

下图说明了 Spring 原型的范围。数据访问对象(DAO)通常不会配置为原型,因为典型的DAO不包含任何会话状态;对于这个作者来说,重用单例图的核心更加容易。

下面的例子将 bean 定义为 XML 中的原型:

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

与其他作用域相比,Spring 不管理原型 bean 的完整生命周期:容器实例化、配置和组装一个原型对象,然后将其交给客户端,而不需要进一步记录该原型实例。因此,尽管初始化生命周期回调方法在所有对象上都被调用,而不考虑作用域,但在原型的情况下,配置的销毁生命周期回调不会被调用。客户机代码必须清理原型作用域的对象,并释放原型bean所持有的昂贵资源。要让Spring容器释放由原型作用域 bean 持有的资源,请尝试使用自定义 bean 后处理器,它持有对需要清理的 bean 的引用。

在某些方面,Spring 容器对于原型作用域 bean 的作用是 Java new 操作符的替代品。超过该点的所有生命周期管理都必须由客户机处理。

3、Singleton beans 依赖于 prototype Bean

当你使用具有依赖原型bean的单例作用域 bean 时,请注意依赖关系在实例化时被解析。因此,如果您将依赖注入原型作用域的 bean 到单例作用域的 bean 中,则会实例化一个新的原型bean,然后将依赖注入到单例 bean 中。原型实例是曾经提供给单例作用域 bean 的唯一实例。

但是,假设你希望单例作用域 bean 在运行时反复获得原型作用域 bean 的新实例。您不能依赖—将原型作用域的 bean 注入到您的单例 bean 中,因为这种注入只发生一次,即 Spring 容器实例化单例 bean 并解析和注入它的依赖时。

4、Request, session 以及 global session scopes

request, session, 和 global session 作用域仅在使用 web 感知 Spring ApplicationContext 实现(如 XmlWebApplicationContext )时可用。如果您将这些作用域与常规Spring IoC 容器(如ClassPathXmlApplicationContext)一起使用,你将得到一个 IllegalStateException,它会报未知的bean 作用域。

4.1 Initial web configuration

request, session, 和 global session 级别(web范围的bean)支持bean的作用域,在定义bean之前需要进行一些次要的初始配置。(对于标准作用域:单例原型,这个初始设置是不需要的。)

如何完成这个初始设置取决于特定的 Servlet 环境。

如果你在 Spring Web MVC 中访问作用域 bean,实际上,在 Spring DispatcherServletDispatcherPortlet处理的请求中,那么不需要特殊的设置: DispatcherServletDispatcherPortlet 已经公开了所有相关的状态。

如果你使用一个 Servlet 2.5 web容器,请求在 Spring 的 DispatcherServlet 之外处理(例如,当使用 JSFStruts 时),你需要注册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>

DispatcherServletRequestContextListenerRequestContextFilter 都做完全相同的事情,即将HTTP请求对象绑定到服务该请求的线程。这使得请求作用域和会话作用域的bean在调用链的更下游可用。

4.2 Request scope

考虑下面的bean定义:

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

Spring容器通过为每个 HTTP 请求使用 LoginAction bean定义来创建 LoginAction bean 的新实例。也就是说,loginAction bean的作用域在 HTTP 请求级别。您可以随心所欲地更改创建的实例的内部状态,因为从相同的 loginAction bean 定义创建的其他实例将看不到这些状态的更改;它们是针对个别要求的。当请求完成处理时,将丢弃作用域为该请求的bean。

4.3 Session scope

考虑下面的bean定义:

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

Spring 容器通过在单个 HTTP 会话的生命周期中使用 UserPreferences bean定义来创建UserPreferences bean 的新实例。换句话说,userPreferences bean 的作用域有效地限定在HTTP Session 级别。与使用请求范围的 bean 一样,您可以随心所欲地更改创建的实例的内部状态,因为其他 HTTP Session 实例也使用从相同 userPreferences bean 定义创建的实例,它们不会看到这些状态变化,因为它们是特定于单个 HTTP Session 的。当HTTP Session 最终被丢弃时,作用域为该特定 HTTP Session 的 bean 也被丢弃。

4.4 Global session scope

考虑下面的bean定义:

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

global session 作用域类似于标准的 HTTP Session 作用域(如上所述),并且仅应用于基于portlet的web应用程序的上下文中。portlet 规范定义了在组成单个 portlet web 应用程序的所有portlet之间共享的全局 Session 的概念。在全局会话作用域中定义的bean限定(或绑定)到global session portlet Session 的生命周期。

如果你编写了一个标准的基于 servlet 的 web 应用程序,并将一个或多个 bean 定义为具有global session 作用域,那么将使用标准的HTTP Session 作用域,并且不会引发错误。

4.5 Application scope

考虑下面的bean定义:

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

通过对整个web应用程序使用一次AppPreferences bean定义,Spring容器创建了一个AppPreferences bean的新实例。也就是说,appPreferences bean的作用域在 ServletContext 级别,存储为常规的 ServletContext 属性。这有点类似于 Spring 的单例 bean,但在两个重要的方面有所不同:它是每个 ServletContext 的单例,而不是每个Spring 的 ApplicationContext’(或者在任何给定的 web 应用程序中可能有几个),它实际上是公开的,因此作为 ServletContext 属性可见。

4.6 scope bean 作为依赖

Spring IoC容器不仅管理对象(bean)的实例化,而且还管理协作者(或依赖项)的连接。例如,如果您想将(一个)有作用域的HTTP请求bean注入到另一个有更长的作用域的bean中,您可以选择注入一个AOP代理来代替有作用域的bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(例如HTTP请求)检索真正的目标对象,并将方法调用委托给真正的对象。

您还可以在作用域为单例的bean之间使用<aop:scoped-proxy/>,然后引用通过一个可序列化的中间代理,因此能够在反序列化时重新获得目标 singleton bean。

当针对范围原型bean声明 <aop:scope -proxy/> 时,共享代理上的每个方法调用都将导致创建一个新的目标实例,然后将调用转发给该目标实例。

而且,限定作用域的代理并不是以生命周期安全的方式从更短的作用域访问bean的唯一方法。你也可以简单地声明你的注入点(即构造函数/setter参数或自动连接字段)为ObjectFactory<MyTargetBean>,允许 getObject() 调用在每次需要时按需检索当前实例——而不需要保留实例或单独存储它。

它的 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 定义中。为什么在 request, session, globalSession和自定义作用域级别定义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 session 作用域的协作 bean 作为依赖项注入到单例 bean 中。相反,你需要一个单一的userManager 对象,并且在HTTP会话的生命周期内,你需要一个特定于上述HTTP会话的userPreferences 对象。因此容器创建了一个对象,它公开了与 UserPreferences 类完全相同的公共接口(理想情况下是一个 UserPreferences 实例对象),它可以从作用域机制(HTTP请求,Session等)中获取真正的 UserPreferences 对象。容器将这个代理对象注入到 userManager bean中,它不知道这个 UserPreferences 引用是一个代理。在这个例子中,当 UserManager 实例调用依赖注入 UserPreferences 对象上的一个方法时,它实际上是在调用代理上的一个方法。然后代理从HTTP会话(在本例中)获取真实的 UserPreferences 对象,并将方法调用委托给检索到的真实 UserPreferences 对象。

因此,当将 request- session- global 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>

4.7 选择要创建的代理类型

默认情况下,当Spring容器为使用 <aop:scoped-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>

5、自定义 scope

bean的作用域机制是可扩展的;您可以定义自己的作用域,甚至可以重新定义现有的作用域,尽管后者被认为是不好的实践,并且您不能覆盖内置的 singleton prototype 作用域。

5.1 Creating a custom scope

为了将您的自定义范围集成到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()

5.2 Using a custom scope

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

void registerScope(String scopeName, Scope scope);

该方法是在 ConfigurableBeanFactory 接口上声明的,该接口通过 BeanFactory 属性在 Spring附带的大多数具体 ApplicationContext 实现上都是可用的。

registerScope(..) 方法的第一个参数是与作用域关联的唯一名称; Spring容器本身中的这类名称的例子有 singleton prototype registerScope(..) 方法的第二个参数是您希望注册和使用的自定义 Scope 实现的实际实例。

假设您编写了自定义 Scope 实现,然后按如下方式注册它。

下面的示例使用了 Spring 包含的 SimpleThreadScope ,但默认情况下没有注册它。对于您自己的自定义 Scope 实现,指令是相同的。

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

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

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

使用自定义 Scope 实现,你就不局限于以编程方式注册范围。你也可以使用 CustomScopeConfigurer 类来声明地注册 Scope :

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

以上是关于Spring Bean Scope的主要内容,如果未能解决你的问题,请参考以下文章

spring学习-IOC-scope作用域

SPRING中属性SCOPE的prototype是什么意思

spring bean的作用域之间有啥区别

004bean作用域

Spring中bean的scope详解

Spring常用配置 --- Bean的Scope