4.5 Bean 的范围

当创建 bean 时,也就创建了对通过 bean 定义创建的类真正实例的配方。bean 定义的 配方这个概念是很重要的,因为它就意味着,你可以通过配方来创建一个类的多个对象实例。

你不仅可以控制从特定 bean 定义创建出的对象的各个依赖和要配置到对象中的值,也 可以控制对象的范围。这个方法非常强大并且很灵活,你可以选择通过配置创建的对象的范 围,而不必去处理 Java 类级别对象的范围。Bean 可以被定义部署成一种范围:开箱,Spring

Framework 支持五种范围,里面的三种仅仅通过感知 web 的 ApplicationContext 可用。

下列的范围支持开箱。你也可以创建自定义范围(4.5.5 节)。

表 4.3 Bean 的范围

范围 描述
单例(4.5.1 节) (默认的)为每一个 Spring 的 IoC 容器定义一个 bean 为独立对象的实例。
原型(4.5.2 节) 定义可以有任意个对象实例的 bean。
请求(4.5.4.2 节) 定义生命周期为一个独立 HTTP 请求的 bean;也就是说,每一个 HTTP 请求有一个 bean 的独立的实例而不是一个独立的 bean。仅仅在可感知 Web 的 Spring ApplicationContext 中可用。
会话(4.5.4.3 节) 定义一个生命周期为一个 HTTP Session 的独立的 bean。仅仅在可感知 Web 的 Spring ApplicationContext 中可用。
全局会话(4.5.4.4 节) 定义一个生命周期为一个全局的 HTTP Session 的独立的 bean。典型地是仅仅 使用 portlet 上 下文 时可 用。 仅仅 在可 感知 Web 的 Spring ApplicationContext 中可用。

线程范围的 bean

在 Spring 3.0 当中,线程范围是可用的,但是它默认是不注册的。要获取更多信息,请 参考 SimpleThreadSc ope 的 JavaDoc 文档。对于如何注册这个范围或其它自定义范围的做法,

请参考 4.5.5.2 节,“使用自定义范围”。

4.5.1 单例范围

仅仅共享被管理的 bean 的一个实例,所有使用一个或多个 id 来匹配 bean 的结果对 bean 请求,由 Spring 容器返回指定 bean 的实例。

另外一种方式,当你定义一个范围是单例的 bean 后,Spring 的 IoC 容器通过 bean 的定 义创建了这个对象的一个实例。这个独立的实例存储在单例 bean 的缓存中,所有对这个命 名 bean 的后续的请求和引用都返回缓存的对象。

image

Spring 中对单例 bean 的概念和四人帮(Gang of Four,GoF)设计模式书中定义的单例 模式是不同的。GoF 的单例硬编码了对象的范围,也就是特定的类仅仅能有一个实例被每一 个 ClassLoader 来创建。Spring 中单例 bean 的范围,是对每一个容器和 bean 来说的。这 就意味着在独立的 Spring 容器中,对一个特定的类创建了一个 bean,那么 Spring 容器通过 bean 的定义仅仅创建这个类的一个实例。在 Spring 中,单例范围是默认的范围。要在 XML 中定义单例的 bean,可以按照如下示例来编写:

<bean id="accountService" class="com.foo.DefaultAccountService"/>
<!-- 尽管是冗余的(单例范围是默认的),下面的bean定义和它是等价的 -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>

4.5.2 原型范围

非单例,原型范围的 bean 就是当每次对指定 bean 进行请求时,一个新的 bean 的实例 就会创建。也就是说,bean 被注入到其它 bean 或是在容器中通过 getBean()方法来请求时就 会创建新的 bean 实例。作为一项规则,对所有有状态的 bean 使用原型范围,而对于无状 态的 bean 使用单例范围。

下图讲述了 Spring 的原型范围。数据访问对象(DAO)通常不是配置成原型的,因为典型的 DAO 不会持有任何对话状态;仅仅是对作者简单对单例图的重用。

image

下面的示例在 XML 中定义了原型范围的 bean:

<!-- 使用spring-beans-2.0.dtd -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="prototype"/>

和其它范围相比,Spring 不管理原型 bean 的完整生命周期;容器实例化,配置并装配 原型对象,并把他给客户端,对于原型 bean 的实例来说,就没有进一步的跟踪记录了。因 此,尽管初始化生命周期回调方法可以对所有对象调用而不管范围,那么在原型范围中,配 置销毁生命周期回调是不能被调用的。客户端代码必须清理原型范围的对象并且释放原型 bean 持有的系统资源。要让 Spring 容器来释放原型范围 bean 持有的资源,可以使用自定义 bean 后处理器(4.8.1 节),它持有需要被清理的 bean 的引用。

在某些方面,关于原型范围 bean 的 Spring 容器的角色就是对 Java new 操作符的替代。 所有之后生命周期的管理必须由客户端来处理。(关于 Spring 容器中 bean 的生命周期的详细内容,可以参考 4.6.1 节,“生命周期回调”)

4.5.3 单例 bean 和原型 bean 依赖

当你使用单例范围的 bean 和其依赖是原型范围的 bean 时,要当心依赖实在实例化时 被解析的。因此,如果依赖注入了原型范围的 bean 到单例范围的 bean 中,新的原型 bean 就被实例化并且依赖注入到单例 bean 中。原型实例是唯一的实例并不断提供给单例范围的 bean。

假设你想单例范围的 bean 在运行时可以重复获得新的原型范围 bean 的实例。那么就 不能将原型范围的 bean 注入到单例 bea n 中,因为这个注入仅仅发生一次,就是在 Spring 容器实例化单例 bean 并解析和注入它的依赖时。如果你在运行时需要原型 bean 新的实例 而不是仅仅一次,请参考 4.4.6 节,“方法注入”。

4.5.4 请求,会话和全局会话范围

request,session 和 global session 范围仅仅当你使用感知 Web 的 Spring ApplicationContext 实现类可用(比如 XmlWebApplicationContext)。如果你在 常规的 Spring IoC 容器中使用如 ClassPathXmlApplicationContext 这些范围,那么 就会因为一个未知的 bean 的范围而得到 IllegalStateException 异常。

4.5.4.1 初始化 Web 配置

要支持 request,session 和 global session 级别(Web 范围的 bean)范围的 bean,一些细微的初始化配置就需要在定义 bean 之前来做。(这些初始化的设置对标准的 范围,单例和原型,是不需要的。)

如果来完成这些初始化设置是基于你使用的特定 Servlet 容器的。

如果使用 Spring 的 Web MVC 来访问这些范围的 bean,实际上,是由 Spring 的 DispatcherServlet 或 DispatcherPortlet 来处理请求的,那就没有特殊的设置了: DispatcherServlet 和 DispatcherPortlet 已经可以访问所有相关的状态。

如果你使用支持 Servlet 2.4 规范以上的 Web 容器,在 Spring 的 DispatcherServle(比如,当 使 用 JSF 或 Struts 时 ) 之 外 来 处 理 请 求 , 那 么 就 需 要 添 加 javax.servlet.ServletRequestListener 到 Web 应用的 web.xml 文件的声明中:

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

如果你使用老版的 Web 容器( Serlvet 2.3 ), 那 么 就 使 用 提 供 的 javax.servlet.Filter 实 现 类 。 如 果 你 想 在 Servlet 2.3 容 器 中 , 在 Spring 的 DispatcherServlet 外部的请求中访问 Web 范围的 bean,下面的 XML 配置代码片段就必须包 含在 Web 应用程序的 web.xml 文件中。(过滤器映射基于所处的 Web 应用程序配置,所以 你必须以适当的形式来修改它。)

<web-app>
    ..
    <filter>
        <filter-name>requestContextFilter</filter-name>
        <filter-class>org.springframework.web.filter.RequestCont extFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestContextFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

DispatcherServlet,RequestContextListener 和 RequestContextFilter 所有都可以做相同的事情,即绑定 HTTP 请求对象到服务于请求的 Thread 中。这使得 bean 在请求和会话范围内对后续的调用链可用。

4.5.4.2 请求范围

考虑一下下面的 bean 定义:

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

Spring 容器通过使用 LoginAction bean 的定义来为每个 HTTP 请求创建 LoginAction bean 的新的实例。也就是说,LoginAction bean 是在 HTTP 请求级别的范 围。你可以改变创建的实例内部的状态,怎么改都可以,因为从相同的 LoginAction bean 定义中创建的其它实例在状态上不会看到这些改变;它们对单独的请求都是独立的。当请求 完成处理,bean 在请求范围内被丢弃了。

4.5.4.3 会话范围

考虑一下下面的 bean 定义:

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

Spring 容器通过使用 UserPreferences bean 的定义来为每个 HTTP 会话的生命周期 创建 UserPreferences bean 的新的实例。换句话说,UserPreferences bean 在 HTTP

session 级别的范围内是有效的。正如 request-scoped 的 bean,你可以改变创建的实 例 内 部 的 状 态 ,怎 么 改 都 可以 , 要 知 道其 它 HTTP session 实例也是使用从相同 UserPreferences bean 的定义中创建的实例,它们不会看到状态的改变,因为它们对单 独的 HTTP session 都是独立的。当 HTTP session 最终被丢弃时,那么和这个 HTTP session 相关的 bean 也就被丢弃了。

4.5.4.4 全局会话范围

考虑一下下面的 bean 定义:

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

global session 范围和标准的 HTTP Session 范围(上面 4.5.4.3 节讨论的)类似, 但仅能应用于基于 portlet 上下文的 Web 应用程序。portlet 规范定义了全局的 Session 的 概念,该会话在所有 portlet 之间所共享来构建一个单独的 portlet Web 应用程序。在 global session 范围内定义的 bean 的范围(约束)就会在全局 portlet Session 的生命周期内。

如果你编写了一个标准的基于 Servlet 的 Web 应用,并且定义了一个或多个有 global session 范围的 bean,那么就会使用标准的 HTTP Session 范围,也不会抛出什么错误。

4.5.4.5 各种范围的 bean 作为依赖

Spring 的 IoC 容器不仅仅管理对象(bean)的实例,而且也装配协作者(依赖)。如果 你想注入(比如)一个 HTTP 请求范围的 bean 到另外一个 bean 中,那么就必须在有范围 bean 中注入一个 AOP 代理。也就是说,你需要注入一个代理对象来公开相同的公共接口作为有 范围的对象,这个对象也可以从相关的范围(比如,HTTP 请求范围)中获取真实的目标对 象并且委托方法调用到真正的对象上。

注意

你不需要使用<aop:scoped-proxy/>来结合 singleton 和 prototype 范围的 bean 。 如 果 你 想 为 单 例 bean 尝 试 创 建 有 范 围 的 代 理 , 那 么 就 会 抛 出 BeanCreationException 异常。

在下面的示例中,配置仅仅只有一行,但是这对理解“为什么”和其中的“怎么做”是至关重要的。

<?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-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring -aop-3.0.xsd">
    <!-- HTTP 会话范围的bean作为代理 -->
    <bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
        <!-- 这个元素影响周围bean的代理 -->
        <aop:scoped-proxy/>
    </bean>
    <!-- 单例范围的bean用上面的代理bean来注入 -->
    <bean id="userService" class="com.foo.SimpleUserService">
        <!-- 作为代理的userPreferences bean引用 -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要创建这样的代理,就要插入一个子元素<aop:scoped-proxy/>到有范围的 bean 的 定义中。(如果要选择基于类的代理,那么需要在类路径下放置 CGLIB 的类库,参考下面的 “选择创建代理类型”部分和“附录 C,基于 XML Schema 的配置”)为什么 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 session 范围 的 bean userPreferences 的引用注入的。这里突出的一点就是 userManager bean 是单例的; 那么对于每个容器来说,它只会被实例化一次,并且它的依赖(本例中只有一个依赖,就是 userPreferences bean)也只会被注入一次。这就意味着 userManager bean 只会对同 一个 userPreferences 对象进行操作,也就是说,是最初被注入的那一个对象。

这并不是你想要的行为,即注入生命周期范围短的 bea n 到生命周期范围长的 bea n 中, 比如注入 HTTP session 范围的协作 bean 作为依赖到单例 bean 中。相反,你需要一个单独的 userManager 对 象 , 生 命 周 期 和 HTTP session 一 样 , 你 需 要 一 个 userPreferences 对象来特定于 HTTP session。因此容器创建对象,公开相同的公共 接口,而 UserPreferences 类(理想情况下是 UserPreferences 实例的对象)从范围机制(HTTP 请求,session 等等)来获取真正的 UserPreferences 对象。容器注入这 个代理对象到 userManager bean 中,这个 bean 不能感知 UserPreferences 的引用就 是代理。在这个示例中,当 UserManager 实例调用依赖注入的 UserPreferences 对象 的方法时,那么其实是在代理上调用这个方法。然后代理从(在本例中)HTTP session 中 获取真实的 UserPreferences 对象 ,并且委托方法调用到获取的真实 的 UserPreferences 对象中。

因此,当注入 request 范围,session 范围和 globalSession 范围的 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 的类库。 注意:CGLIB 代理仅仅拦截公有的方法调用!不会调用代理中的非公有方法;它们不会被委托到有范围的目标对象。

另外,你可以配置 Spring 容器有范围的 bean 创建标准的基于 JDK 接口的代理,通过指 定<aop:scoped-proxy/>元素的 proxy-target-class 属性值为 false。使用基于 JDK 接口的代理意味着你不需要在应用程序的类路径下添加额外的类库来使代理生效。然而, 它也意味着有范围 bean 的类必须实现至少一个接口,并且注入到有范围 bean 的所有协作 者必须通过它的一个接口来引用 bean。

<!—DefaultUserPreferences实现了UserPreferences接口 -->
<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>

关于选择基于类或基于接口的代理的更多详细信息,可以参考 8.6 节,“代理机制”。

4.5.5 自定义范围

在 Spring 2.0 当中,bean 的范围机制是可扩展的。你可以定义自己的范围,或者重新定 义已有 的范 围, 尽管 后者 被认 为是 不良 实践 而且 你不能 覆盖 内建 的 singleton 和 prototype 范围。

4.5.5.1 创建自定义范围

要 整 合 自 定 义 范 围 到 Spring 的 容 器 中 , 那 么 需 要 实 现 org.springframework.beans.factory.config.Scope 接口,这会在本节中来介绍。 对于如何实现你自己的范围的想法,可以参考 Spring Framework 本身提供的 Scope 实现和 Scope 的 JavaDoc文档,这里会介绍你需要实现的方法的更多细节。

Scope 接口有四个方法来从作用域范围中获取对象,从作用域范围中移除对象还有允 许它们被销毁。

下面的方法从底层范围中返回对象。比如,会话范围的实现,返回会话范围的 bean(如 果它不存在,在绑定到会话后为将来的引用,这个方法返回 bean 的新的实例)。

Object get(String name, ObjectFactory objectFactory)

下面的方法从底层范围中移除对象。比如会话范围的实现,从底层的会话中移除会话范 围的 bean。对象应该被返回,但是如果特定名字的对象没有被发现的话可以返回 null。

Object remove(String name)

下面的方法注册当被销毁或者指定范围内的对象被销毁时,相关范围应用执行的回调函 数。参考 JavaDoc 文档或者 Spring 范围实现来获取更多关于销毁回调的信息。

void registerDestructionCallback(String name, Runnable destructionCallback)

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

String getConversationId()

4.5.5.2 使用自定义范围

在你编写并测试一个或多个自定义 Scope 实现之后,你需要让 Spring 容器感知你的新 的范围。下面的方法是用 Spring 容器注册新的 Scope 的核心方法。

void registerScope(String scopeName, Scope scope);

这个方法在 ConfigurableBeanFactory 接口中声明,也在 大多数的 ApplicationContext 实现类中可用,通过 BeanFactory 属性跟着 Spring。

registerScope(..)方法的第一个参数是和范围相关的唯一名称;在 Spring 容器本 身这样的名字就是 singleton 和 prototype。registerScope(..)方法的第二个参数 是你想注册并使用的自定义 Scope 实现的真实实例。

假设你编写了自定义的 Scope 实现,之后便如下注册了它。

注意

下面的示例使用了 SimpleThreadScope,这是包含在 Spring 当中的,但模式是没有 注册的。这些指令和你自己自定义的 Scope 的实现是相同的。

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

之后就可以为你的自定义 Scope 创建符合 Spring 规则的 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-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring -aop-3.0.xsd">
    <bean
        class="org.springframework.beans.factory.config.CustomScopeC onfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleT hreadScope"/>
                </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()方法返回的对象。