1.5 Bean作用域

当你创建一个bean定义时,你将创建一个recipe用来创建由该bean定义的类的实际实例。bean定义是一个recipe的想法很重要,因为这意味着,和类一样,你可以从一个recipe创建多个对象实例。

你不仅可以控制要插入到从特定bean定义创建的对象中的各种依赖项和配置值,还可以控制从特定bean定义创建的对象的范围。这种方法是强大和灵活的,因为你可以通过配置来选择你创建的对象的作用域,而不必在Java类级别对对象作用域进行设置。bean可以选择被部署在任何一个作用域中。

Spring框架支持六个作用域,其中四个作用域仅在使用web-aware的ApplicationContext时可用。当然,你可以创建自定义作用域。

下表介绍了支持的作用域:

Table 3. Bean Scope

作用域描述

singleton

(默认)Spring IOC容器将单个bean定义绑定到单个对象实例。

prototype

单个bean定义可以被绑定到多个对象实例

request

单个bean定义被绑定到单个的Http请求生命周期。就是每一次HTTP请求都有属于他自己的单独实例创建,这个只在web-aware的Spring ApplicationContext中有效。

session

单个bean的定义会被绑定到Http Session的生命周期中,这个只在web-aware的Spring ApplicationContext中有效。

application

单个bean的定义会被绑定到ServletContext的生命周期中,这个只在web-aware的Spring ApplicationContext中有效。

websocket

单个bean的定义会被绑定到WebSocket的生命周期中,这个只在web-aware的Spring ApplicationContext中有效。

从Spring3.0开始,添加了thread作用域,但默认情况下不被注册:请参阅SimpleThreadScope。 从Spring4.2开始,添加了transaction作用域:SimpleTransactionScope。有关如何注册这些或任何其他自定义范围的说明,请参阅使用自定义范围。

1.5.1 Singleton作用域

只会创建一个singleton bean的共享实例,所有与该bean ID相匹配的bean的请求都会导致Spring容器返回这个特定的bean实例。

换句话说,当你定义一个bean定义并且它的作用域是Singleton时,Spring IOC容器只创建由该bean定义的对象的一个实例。这个单个实例存储在这样的单实例bean的缓存中,所有随后的请求和对这个名为bean的请求都返回缓存的对象。下图显示了Singleton作用域的工作方式:

Spring对单例bean的概念不同于《Gang of Four (GoF) 》一书中定义的单例模式。gof singleton硬编码对象的作用域,这样每个类加载器只能创建一个特定类的实例。Spring Singleton的范围是针对于每个容器和每个bean的。这意味着,如果在单个Spring容器中为特定类定义一个bean,那么Spring容器将创建由该bean定义的类的一个且仅一个实例。单例作用域是Spring中的默认作用域。要在XML中将bean定义为singleton,如下例所示:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

1.5.2 Prototype作用域

每次请求特定bean时,bean如果是Prototype作用域,当bean被注入到另一个bean中,或者通过对容器的getBean()方法调用请求时,都会创建一个新的bean实例。通常来说,你应该为所有有状态bean使用Prototype作用域,为无状态bean使用Singleton作用域。

下图说明了prototype作用域:

(数据访问对象(DAO)通常不配置为原型,因为典型的DAO不具有任何会话状态。使用单例模式将会更方便。)

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

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

与其他生命周期不同,Spring不负责prototype bean的完整生命周期。容器实例化、配置或以其他方式组装原型对象,然后将其交给客户端,之后就不再记录该原型实例。

因此,尽管初始化生命周期回调方法会在所有对象上被调用,不管生命周期如何,但在原型的情况下,不会调用配置的销毁生命周期回调。客户端代码必须自行清理原型作用域内的对象,并释放原型bean所拥有的昂贵资源。

要让Spring容器释放原型作用域bean所拥有的资源,请尝试使用自定义bean post-processor,该处理器包含对需要清理的bean的引用。

从某些方面来讲,Spring容器在prototype-scoped bean中的作用类似于Java new操作符。这之后的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean生命周期的详细信息,请参阅生命周期回调。)

1.5.3 Singleton Beans依赖于 Prototype-bean

当你使用依赖于原型bean的单例bean时,请注意依赖项是在实例化时解析的。因此,如果你将一个原型作用域的bean注入到一个单例bean中,那么将实例化一个新的原型bean,然后将依赖项注入到单例bean中。这个原型实例是提供给单例bean的唯一实例。

但是,假设你希望单例bean在运行时重复获取原型bean的新实例。则不能将原型bean注入到singleton bean中,因为当Spring容器实例化singleton bean并解析和注入其依赖项时,该注入只发生一次。如果在运行时需要多个原型bean的新实例,请参照方法注入。

1.5.4 Request, Session, Application, 和WebSocket作用域

Request, Session, Application, 和WebSocket作用域仅在使用web-aware的Spring ApplicationContext实现(如XMLWebApplicationContext)时可用。如果将这些作用域与常规的Spring IOC容器(如ClassPathXmlApplicationContext)一起使用,则会引发一个IllegalstateException,它会抱怨未知的bean作用域。

初始Web配置

为了支持Request, Session, Application, 和WebSocket作用域,在定义bean之前需要一些小的初始配置。(对于标准范围:singleton和prototype,不需要此初始设置。)

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

如果你在SpringWebMVC中访问作用域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应用程序的筛选部分:

<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 scope

考虑下面的定义:

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

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

当使用注解驱动的组件或Java配置时,可以使用@RequestScope来将组件分配给request作用域。以下示例显示了如何执行此操作:

@RequestScope
@Component
public class LoginAction {
    // ...
}

Session Scope 考虑下面的定义:

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

Spring容器通过在单个HTTP Session的生命周期中使用UserPreferences bean定义创建一个新的UserPreferences bean实例。换句话说,UserPreferences bean在HTTP Session级别有效。与request scope的bean一样,你可以根据需要更改创建的实例的内部状态,因为其他也使用从相同的用户首选项bean定义创建的实例的HTTP session实例在状态中看不到这些更改,因为它们是特定于单个HTTP session的。当最终丢弃HTTP session时,也会丢弃作用于该特定HTTP session的bean。

当使用注解驱动的组件或Java配置时,可以使用@SessionScope将组件分配给session 作用域。

@SessionScope
@Component
public class UserPreferences {
    // ...
}

Application Scope

考虑下面的定义:

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

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

当使用注解驱动的组件或Java配置时,可以使用 @ApplicationScope注解来将组件分配给application scope。以下示例显示了如何执行此操作:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}

作用域Bean作为依赖

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

你还可以在作用域为singleton的bean之间使用 <aop:scoped-proxy/>来引用beans,然后通过一个可序列化的中间代理,从而能够在反序列化时重新获取目标singleton bean。

对prototype bean声明 <aop:scoped-proxy/>时,共享代理上的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该实例。

此外,作用域代理并不是从较短的作用域获取bean的唯一方法。你还可以将注入点(即构造函数、setter参数或autowired字段)声明为ObjectFactory<MyTargetBean>,允许每次需要时getObject()调用根据需要检索当前实例-而不保留该实例或将其单独存储。

作为扩展变量,你可以声明ObjectFactory,它提供了几个额外的访问变量,包括getIfAvailable和getIfUnique。

它的JSR-330变种称为Provider,和Provider<MyTargetBean>一起使用,从而在每次查询时都会调用get()方法。

下面的配置文件只有一行,但是我们需要弄清楚其背后的原因。

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.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.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

要创建这样的代理,可以将一个子<aop:scoped-proxy/>元素插入到作用域bean定义中(请参见选择要创建的代理类型和基于XML模式的配置)。为什么在请求、会话和自定义范围的bean需要<aop:scoped-proxy/>元素?考虑下面的singleton bean定义,并将其与你需要为上述范围定义的内容进行对比(请注意,下面的userPreferences bean定义并不完整):

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

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

在前面的示例中,singleton bean(UserManager)注入了一个HTTP会话范围的bean(UserPreferences)的引用。这里的问题是,UserManager bean是一个单例:每个容器只实例化一次,它的依赖项(在本例中只有一个,即UserPreferences bean)也只注入一次。这意味着UserManager bean只在完全相同的UserPreferences对象(即最初注入它的对象)上操作。

这不是将寿命较短的作用域bean注入寿命较长的作用域bean时需要的行为(例如,将HTTP会话作用域协作bean作为依赖项注入到singleton bean中)。相反,你需要一个UserManager对象,并且在HTTP会话的生命周期中,你需要一个特定于HTTP会话的UserPreferences对象。因此,容器创建一个对象,该对象与UserPreferences类公开完全相同的公共接口(理想情况下是一个UserPreferences实例的对象),该对象可以从作用域机制(HTTP请求、会话等)获取实际的UserPreferences对象。

容器将这个代理对象注入到UserManager bean中,后者不知道这个用户首选项引用是代理。在本例中,当一个UserManager实例在依赖项注入的UserPreferences对象上调用一个方法时,它实际上是在代理上调用一个方法。然后,代理从HTTP会话(在本例中)中获取real userPreferences对象,并将方法调用委托给检索到的real userPreferences对象。

因此,在将请求和会话范围的bean注入到协作对象时,你需要以下(正确和完整)配置,如下示例所示:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

选择创建的代理类型

一般来说当Spring使用<aop:scoped-proxy/>来创建代理时,会使用CGLIB。

CGLIB代理只拦截公共方法调用!不要在此类代理上调用非公共方法。它们不会委托给实际作用域目标对象。

或者,可以通过为<aop:scoped-proxy/>元素的proxy-target-class属性的值指定false,将spring容器配置为为此类作用域bean创建标准的基于JDK接口的代理。使用基于JDK接口的代理意味着你不需要在应用程序类路径中加入其他库。但是,它意味着作用域bean的类必须实现至少一个接口,并且所有注入作用域bean的合作者必须通过其接口之一引用bean。以下示例显示了基于接口的代理:

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

1.5.5 自定义作用域

Bean的作用域范围是可以扩展的,你可以定义自己的作用域,甚至重新定义现有作用域,尽管后者被认为是不好的做法,并且不能覆盖内置的单例和原型作用域。

创建自定义作用域

要将自定义作用域集成到Spring容器中,需要实现本节中描述的org.springframework.beans.factory.config.Scope接口。有关如何实现自己的作用域的概念,请参阅Spring框架本身提供的作用域实现和作用域JavaDoc,后者更详细地解释了需要实现的方法。

作用域接口有四种方法来从作用域中获取对象,将它们从作用域中移除,并让它们被销毁。 例如,会话作用域实现返回会话作用域bean(如果它不存在,则在将其绑定到会话以供将来使用,该方法返回bean的新实例)。以下方法从所在作用域返回对象:

Object get(String name, ObjectFactory objectFactory)

例如,会话作用域实现从基础会话中删除会话作用域bean。应返回对象,但如果找不到具有指定名称的对象,则可以返回空值。以下方法将对象从基础作用域中移除:

Object remove(String name)

以下方法注册当作用域被销毁或作用域中的指定对象被销毁时应执行的回调:

void registerDestructionCallback(String name, Runnable destructionCallback)

以下方法获取基础作用域的会话标识符:

String getConversationId()

对于每个作用域,这个标识符是不同的。对于会话范围的实现,此标识符可以是会话标识符。

使用自定义作用域

当你写好一个Scope的实现之后,需要通知Spring容器,下面是注册到Spring容器的核心方法:

void registerScope(String scopeName, Scope scope);

此方法在ConfigurableBeanFactory接口中,该接口可通过Spring附带的大多数具体应用程序上下文实现的BeanFactory属性获得。

registerScope(..)方法的第一个参数是与作用域关联的唯一名称。在Spring容器本身中,此类名称的示例是singleton和prototype。registerScope(..)方法的第二个参数是要注册和使用的自定义作用域实现的实际实例。

假设你编写自定义作用域实现,然后按照下一个示例所示注册它。

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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://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="thing2" class="x.y.Thing2" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="thing1" class="x.y.Thing1">
        <property name="thing2" ref="thing2"/>
    </bean>

</beans>

当你在FactoryBean的实现中使用<aop:scoped-proxy/>时,是这个factory bean本身被代理,而不是他返回的对象被代理getObject()。

最后更新于