Spring Framework 6.0.3 Bean Scopes(Bean范围) 翻译

48 阅读15分钟

在创建bean定义时,您创建了用于创建由该bean定义定义的类的实际实例的配方。bean定义是一个配方的想法很重要,因为这意味着,与类一样,您可以从一个配方创建许多对象实例。

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

支持的范围如下表所示:

ScopeDescription
singleton默认)将单个bean定义作用于每个Spring IoC容器的单个对象实例。
prototype将单个bean定义作用于任意数量的对象实例。
request将单个bean定义限定在单个HTTP请求的生命周期内。也就是说,每个HTTP请求都有自己的bean实例,该实例是在单个bean定义的基础上创建的。仅在web感知Spring ' ApplicationContext '的上下文中有效。
session将单个bean定义限定在HTTP“Session”的生命周期内。仅在web感知Spring ' ApplicationContext '的上下文中有效.
application将单个bean定义限定在' ServletContext '的生命周期内。仅在web感知Spring ' ApplicationContext '的上下文中有效。.
websocket将单个bean定义限定在' WebSocket '的生命周期内。仅在web感知Spring ' ApplicationContext '的上下文中有效。

从Spring 3.0开始,线程作用域是可用的,但默认情况下没有注册。有关更多信息,请参阅SimpleThreadScope的文档。有关如何注册此或任何其他自定义范围的说明,请参见使用自定义范围

1.5.1. 单例作用域/singleton

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

换句话说,当您定义一个bean定义并且它的作用域是单例时,Spring IoC容器只创建该bean定义的对象的一个实例。此单个实例存储在此类单例bean的缓存中,对该命名bean的所有后续请求和引用都返回缓存的对象。下图显示了单例作用域是如何工作的:

image.png

Spring的单例bean概念不同于四人组(GoF)模式书中定义的单例模式。GoF单例硬编码对象的作用域,以便每个ClassLoader创建一个且只有一个特定类的实例。Spring单例的作用域最好描述为每个容器和每个bean。这意味着,如果您为单个Spring容器中的特定类定义了一个bean,那么Spring容器将创建该bean定义的类的一个且仅一个实例。单例作用域是Spring中的默认作用域。要在XML中将bean定义为单例,可以如下所示定义bean:

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

<!-- 下面是等效的,尽管是冗余的(单例作用域是默认的) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

1.5.2. 原型范围/Prototype

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

下图说明了Spring Prototype范围:

image.png

数据访问对象(DAO)通常不配置为Prototype,因为典型的DAO不保存任何会话状态。对我们来说重用单例图的核心更容易。)

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

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

与其他作用域相比,Spring不管理Prototype bean的完整生命周期。容器实例化、配置并以其他方式组装一个Prototype对象,并将其交给客户端,不再记录该Prototype实例。因此,尽管初始化生命周期回调方法在所有对象上都被调用,无论其作用域如何,但在Prototype的情况下,配置的销毁生命周期回调不会被调用。客户端代码必须清理Prototype作用域的对象,并释放Prototypebean所拥有的昂贵资源。要让Spring容器释放Prototype作用域bean所拥有的资源,请尝试使用自定义bean后处理器,它包含需要清理的bean的引用。

在某些方面,Spring容器在Prototype作用域bean方面的角色是Java new操作符的替代品。超过这一点的所有生命周期管理都必须由客户端处理。(有关Spring容器中bean的生命周期的详细信息,请参见生命周期回调。)

1.5.3. 带有Prototype bean依赖关系的单例bean

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

但是,假设您希望单例作用域bean在运行时反复获取Prototype作用域bean的新实例。您不能将Prototype作用域bean的依赖项注入到单例bean中,因为这种注入只发生一次,即当Spring容器实例化单例bean并解析并注入其依赖项时。如果在运行时不止一次需要Pro totypebean的新实例,请参阅方法注入。

1.5.4.Request, Session, Application, and WebSocket作用域

请求、会话、应用程序和websocket作用域只有在使用感知web的Spring ApplicationContext实现(比如XmlWebApplicationContext)时才可用。如果您将这些作用域与常规的Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,则会抛出一个IllegalStateException异常,它抱怨未知的bean作用域。

Web初始配置

为了在请求、会话、应用程序和websocket级别(web作用域bean)支持bean的作用域,在定义bean之前需要进行一些次要的初始配置。(这个初始设置对于标准作用域来说是不需要的:单例和Prototype。)

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

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

如果你使用Servlet web容器,在Spring的DispatcherServlet外部处理请求(例如,当使用JSF时),你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。这可以通过使用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在调用链的更下方可用。

请求范围

考虑以下bean定义的XML配置:

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

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

当使用注释驱动的组件或Java配置时,可以使用@RequestScope注释将组件分配到请求范围。下面的例子展示了如何这样做:

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

会话范围

考虑以下bean定义的XML配置:

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

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

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

应用范围

考虑以下bean定义的XML配置:

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

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

当使用注释驱动的组件或Java配置时,您可以使用@ApplicationScope注释将组件分配给应用程序作用域。下面的例子展示了如何这样做:

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

WebSocket范围

WebSocket作用域与WebSocket会话的生命周期相关联,适用于WebSocket应用程序上的STOMP,详情请参阅WebSocket作用域

将bean限定为依赖项

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

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

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

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

作为一个扩展变体,您可以声明ObjectProvider<MyTargetBean>,它提供了几个额外的访问变体,包括getIfAvailable和getIfUnique。

它的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
        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:scope -proxy/>插入到有作用域的bean定义中(请参阅选择要创建的代理类型基于XML模式的配置)。为什么在请求、会话和自定义作用域级别定义的bean需要<aop:scope -proxy/>元素?考虑以下单例bean定义,并将其与您需要为前面提到的作用域定义的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>

在前面的示例中,单个bean (userManager)被注入了对HTTP会话范围bean (userPreferences)的引用。这里的重点是userManager bean是一个单例:它在每个容器中只实例化一次,并且它的依赖项(在本例中只有一个,即userPreferences bean)也只注入一次。这意味着userManager bean只对完全相同的userPreferences对象(即最初注入它的对象)进行操作。

当将一个短寿命的作用域bean注入到一个长寿命的作用域bean中时(例如,将一个HTTP会话作用域协作bean作为依赖项注入到单例bean中),这不是您想要的行为。相反,您需要一个单独的userManager对象,并且对于HTTP会话的生命周期,您需要一个特定于HTTP会话的userPreferences对象。因此,容器创建了一个对象,该对象公开了与UserPreferences类完全相同的公共接口(理想情况下是一个UserPreferences实例对象),该对象可以从作用域机制(HTTP请求、会话等)获取真正的UserPreferences对象。容器将这个代理对象注入到userManager bean中,该bean不知道这个UserPreferences引用是一个代理。在本例中,当UserManager实例调用依赖注入的UserPreferences对象上的方法时,它实际上是在调用代理上的方法。代理然后从HTTP Session(在本例中)获取真实的UserPreferences对象,并将方法调用委托给检索到的真实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:scope -proxy/>元素标记的bean创建代理时,将创建基于cglib的类代理。

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

或者,您可以配置Spring容器,为这种限定作用域的bean创建标准的基于JDK接口的代理,方法是将<aop:scope -proxy -proxy/>元素的proxy-target-class属性的值指定为false。使用基于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作用域机制是可扩展的。您可以定义自己的作用域,甚至可以重新定义现有的作用域,不过后者被认为是不好的做法,您不能覆盖内置的singleton and prototype作用域。

创建自定义作用域

为了将您的自定义作用域集成到Spring容器中,您需要实现org.springframework.beans.factory.config.Scope接口,本节将对此进行描述。要了解如何实现自己的作用域,请参阅随Spring框架本身和Scope javadoc提供的作用域实现,其中详细解释了需要实现的方法。

Scope接口有四个方法用于从作用域获取对象、从作用域删除对象和销毁对象

例如,会话范围实现返回会话范围的bean(如果它不存在,则在将bean绑定到会话以供将来引用之后,该方法返回该bean的一个新实例)。下面的方法从底层作用域返回对象:

Object get(String name, ObjectFactory<?> objectFactory)

例如,会话范围实现从底层会话中删除会话范围bean。应该返回该对象,但如果没有找到具有指定名称的对象,则可以返回null。下面的方法从底层作用域移除对象:

Object remove(String name)

下面的方法注册一个回调,当它被销毁时,或者当作用域中指定的对象被销毁时,作用域应该调用这个回调:

void registerDestructionCallback(String name, Runnable destructionCallback)


有关破坏回调的更多信息,请参阅javadoc或Spring作用域实现。

下面的方法获取底层作用域的会话标识符:

String getConversationId()

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

使用自定义作用域

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

void registerScope(String scopeName, Scope scope);

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

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

假设您编写了自定义Scope实现,然后按照下一个示例所示注册它。

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

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

然后,您可以创建遵循自定义Scope范围规则的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实现的<bean>声明中放置<aop:scope -proxy/>时,确定作用域的是工厂bean本身,而不是从getObject()返回的对象。