2.5 Spring Framework 5.x 之Bean的范围

Bean 范围

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

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

下表描述了支持的范围:
表3. Bean范围

范围 描述
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或ID的bean的所有请求都会导致Spring容器返回一个特定的bean实例。

换句话说,当您定义一个bean定义并且它的范围是一个单例时,Spring IoC容器只创建该bean定义定义的对象的一个…实例。此单个实例存储在此类单例bean的缓存中,并且该命名Bean的所有后续请求和引用都将返回缓存对象。下图显示了单例范围的工作原理:
在这里插入图片描述

Spring的单例bean概念不同于Gang of Four(GoF)模式书中定义的单例模式。GoF单例对一个对象的范围进行硬编码,使得每个ClassLoader创建一个且只有一个特定类的实例。Spring单例的范围最好描述为每容器和每个bean。这意味着,如果在单个Spring容器中为特定类定义一个bean,则Spring容器将创建该bean定义所定义的类的唯一一个实例。单例范围是Spring中的默认范围。要将bean定义为XML中的单例,您可以定义bean,如以下示例所示:

<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的请求时都创建新的bean实例。也就是说,将bean注入另一个bean,或者通过getBean()对容器的方法调用来请求它。通常,您应该对所有有状态bean使用原型范围,对无状态bean使用单例范围。
下图说明了Spring原型范围:

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

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

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

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

在某些方面,Spring容器关于原型范围bean的角色是Java new运算符的替代品。超过该点的所有生命周期管理必须由客户端处理。(有关Spring容器中bean的生命周期的详细信息,请参阅Lifecycle Callbacks。)

1.5.3 具有Prototype-bean依赖关系的单例Bean

当您使用具有依赖于Prototype-bean的单例作用域bean时,请注意在实例化时解析依赖项。因此,如果依赖项将原型范围的bean注入到单例范围的bean中,则会实例化一个新的原型bean,然后将依赖注入到单例bean中。原型实例是唯一提供给单例范围bean的实例。

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

1.5.4 Request, Session, Application, and WebSocket 范围

在request,session,application,和websocket范围只有当你使用一个基于web的Spring可ApplicationContext实现(例如 XmlWebApplicationContext)。如果将这些范围与常规的Spring IoC容器一起使用,例如ClassPathXmlApplicationContext,IllegalStateException则会引发抱怨未知Bean范围的问题。

初始Web配置

为了支持Bean的范围界定在request,session,application,和 websocket(即具有web作用域bean),需要做少量的初始配置定义你的Bean之前。(标准范围不需要此初始设置:singleton和prototype。)

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

如果您在Spring Web MVC中访问scoped 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请求对象绑定到Thread为该请求提供服务的对象。这使得请求和会话范围的bean可以在调用链的下游进一步使用。

Request 范围

考虑bean定义的以下XML配置

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

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

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

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

session 范围

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

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

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

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

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

适用范围

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

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

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

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

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

作为依赖关系的Scoped Bean

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

  • 您还可以aop:scoped-proxy/在作用域的bean之间使用singleton,然后通过引用然后通过可序列化的中间代理,从而能够在反序列化时重新获取目标单例bean。

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

  • 此外,范围代理不是以生命周期安全的方式从较短范围访问bean的唯一方法。您还可以将您的注入点(即构造函数或setter参数或autowired字段)声明为ObjectFactory允许getObject()调用,以便在每次需要时按需检索当前实例 - 无需保留实例或单独存储它。

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

  • 调用它的JSR-330变体,Provider并与每次检索尝试的Provider 声明和相应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.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架构的配置)。豆类的定义为何作用域的request,session和自定义范围水平要求aop:scoped-proxy/元素?考虑以下单例bean定义,并将其与您需要为上述范围定义的内容进行对比(请注意,以下 userPreferencesbean定义不完整):

<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 Session-scoped bean(userPreferences)的引用。这里的重点是 userManagerbean是一个单例:它每个容器只实例化一次,它的依赖项(在这种情况下只有一个,userPreferencesbean)也只注入一次。这意味着userManagerbean只在完全相同的userPreferences对象(即最初注入它的对象)上运行。

当将一个寿命较短的scoped bean注入一个寿命较长的scoped bean时,这不是你想要的行为(例如,将一个HTTP Session-scoped协作bean作为依赖注入到singleton bean中)。相反,您需要一个userManager 对象,并且在HTTP的生命周期中Session,您需要一个userPreferences特定于HTTP 的对象Session。因此,容器创建一个对象,该对象公开与UserPreferences该类完全相同的公共接口(理想情况下是一个UserPreferences实例的对象),该UserPreferences对象可以从作用域机制(HTTP请求Session等)中获取真实 对象。容器将此代理对象注入到userManagerbean中,该bean不知道此UserPreferences引用是代理。在这个例子中,当一个 UserManagerinstance在依赖注入的UserPreferences 对象上调用一个方法,它实际上是在代理上调用一个方法。然后,代理 UserPreferences从(在这种情况下)HTTP中Session获取真实UserPreferences对象,并将方法调用委托给检索到的真实对象。

因此,在将bean request-和session-scopedbean注入协作对象时,您需要以下(正确和完整)配置 ,如以下示例所示:

<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/元素标记的bean创建代理时,将创建基于CGLIB的类代理。

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

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

<!-- 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和prototype范围。

创建自定义范围

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

该Scope接口有四种方法可以从作用域中获取对象,将其从作用域中删除,然后将其销毁。

例如,会话范围实现返回会话范围的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容器知道您的新范围。以下方法是Scope使用Spring容器注册new的核心方法:

void registerScope(String scopeName, Scope scope);

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

该registerScope(…)方法的第一个参数是与范围关联的唯一名称。Spring容器本身中的这些名称的示例是singleton和 prototype。该registerScope(…)方法的第二个参数是Scope您希望注册和使用的自定义实现的实际实例。

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

下一个示例使用SimpleThreadScope,它包含在Spring中,但默认情况下未注册。您自己的自定义Scope 实现的说明是相同的。

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

然后,您可以创建符合自定义的作用域规则的bean定义, Scope如下所示:

<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.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="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>

放置aop:scoped-proxy/在FactoryBean实现中时,工厂bean本身是作用域的,而不是从中返回的对象getObject()。

猜你喜欢

转载自blog.csdn.net/hadues/article/details/86301754