Spring Framework Core(1)-The Ioc Container(5) Bean 作用域

1 IoC 容器

1.5 Bean 作用域

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

您不仅可以控制要插入到由特定bean定义创建的对象中的各种依赖项和配置值,还可以控制由特定bean定义创建的对象的范围。这种方法强大而灵活,因为您可以选择通过配置创建的对象的范围,而不必在Java类级别上考虑对象的范围。可以将bean定义为部署在多种作用域中的一种。Spring框架支持6种作用域,其中4种只有在使用web感知的ApplicationContext时才可用。您还可以创建a custom scope。
下表描述了支持的范围:

Scope Description
singleton (默认)为每个Spring IoC容器将单个bean定义作用於单个对象实例
prototype 将单个bean定义作用于任意数量的对象实例。
request 将单个bean定义的范围限定为单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的bean实例,这些实例是在单个bean定义的基础上创建的。仅在可感知web的Spring应用程序上下文中有效。
session 将单个bean定义的范围限定为HTTP会话的生命周期。仅在可感知web的Spring应用程序上下文中有效。
application 将单个bean定义作用于ServletContext的生命周期。仅在可感知web的Spring应用程序上下文中有效。
websocket 将单个bean定义作用于WebSocket的生命周期。仅在可感知web的Spring应用程序上下文中有效。

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

1.5.1 The Singleton Scope

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

换句话说,当您定义一个bean定义并将其定义为一个单例对象时,Spring IoC容器只创建该bean定义定义的对象的一个实例。此单一实例存储在此类单例bean的缓存中,该指定bean的所有后续请求和引用都将返回缓存的对象。下图显示了单例范围的工作方式:
在这里插入图片描述Spring的单例bean概念与《设计模式:可复用面向对象软件的基础》(四人组(GoF)模式)书中定义的单例模式不同。例对象对对象的作用域进行硬编码,这样每个类装入器只能创建一个特定类的实例。Spring单例的范围最好描述为每个容器和每个bean。这意味着,如果您在单个Spring容器中为特定类定义一个bean,那么Spring容器将创建由该bean定义定义的类的一个且仅一个实例。单例范围是Spring的默认范围。要在XML中将bean定义为单例,您可以定义如下例所示的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 The Prototype Scope

bean部署的非单例原型范围导致在每次发出对特定bean的请求时创建一个新的bean实例。也就是说,bean被注入到另一个bean中,或者您通过容器上的getBean()方法调用请求它。通常,您应该为所有有状态bean使用 Prototype Scope,为无状态bean使用单例范围
下图说明了Spring prototype作用域
在这里插入图片描述
数据访问对象(DAO)通常不配置为原型,因为典型的DAO不包含任何会话状态。我们可以更容易地重用单例图的核心:
下面的示例将bean定义为XML中的prototype

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

与其他作用域不同,Spring不管理prototype bean的完整生命周期。容器实例化、配置或以其他方式组装prototype对象并将其交给客户机,而不需要该prototype实例的进一步记录。因此,尽管初始化生命周期回调方法在所有对象上都被调用,而与范围无关,但是在prototypes的情况下,配置的销毁生命周期回调不会被调用。客户端代码必须清理prototype-scoped的对象,并释放 prototype bean持有的宝贵资源。尝试使用自定义bean post-processor,它包含需要清理的bean的引用。

在某些方面,Spring容器在 prototype-scoped bean方面的角色可以替代Java new操作符,所有超过那个点的生命周期管理都必须由客户端处理。(有关Spring容器中bean生命周期的详细信息,请参阅Lifecycle Callbacks。)

1.5.3 具有Prototype-bean依赖项的Singleton Beans

当您使用依赖于prototype bean的singleton-scoped bean时,请注意依赖项是在实例化时解析的。
因此,如果您依赖地将一个prototype-scoped bean注入到一个singleton-scoped bean中,一个新的prototype bean将被实例化,然后依赖地注入到singleton bean中,prototype实例是惟一提供给单例作用域bean的实例。

但是,假设您希望singleton-scoped bean在运行时重复获取prototype-scoped bean的新实例。您不能依赖地将一个prototype-scoped bean注入到您的singleton bean中,因为当Spring容器实例化singleton bean并解析和注入它的依赖项时,这种注入只发生一次。如果在运行时需要原型bean的新实例超过一次,请参考 Method Injection

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

只有在使用web感知的Spring ApplicationContext实现(如XmlWebApplicationContext)时,Request, Session, Application 和 WebSocket 作用域才可用。如果您将这些作用域与常规的Spring IoC容器(如ClassPathXmlApplicationContext)一起使用,则会抛出一个IllegalStateException,它会报错一个未知的bean作用域。

初始化web配置

为了在请求、会话、应用程序和websocket级别(web范围的bean)上支持bean的作用域,需要在定义bean之前进行少许初始配置。(标准作用域单例和原型不需要这个初始设置。)

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

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

如果您使用Servlet 2.5 web容器,并在Spring的DispatcherServlet之外处理请求(例如,在使用JSF或Struts时),您需要注册org.springframe .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 作用域

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

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

在这里插入图片描述

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

Session 作用域

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

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

Spring容器通过为单个HTTP会话的生命周期使用UserPreferences bean定义来创建UserPreferences bean的新实例。换句话说,userPreferences bean有效地限定在HTTP会话级别。与使用请求作用域bean一样,您可以根据需要更改所创建实例的内部状态,因为您知道其他使用相同userPreferences bean定义创建的实例的HTTP会话实例不会在状态中看到这些更改,因为他们各自在独立的HTTP会话。当HTTP会话最终被丢弃时,作用域为该特定HTTP会话的bean也被丢弃。
在使用注解驱动的组件或Java配置时,可以使用@SessionScope注解将组件分配给Session 作用域。

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

Application 作用域

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

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

通过为整个web应用程序使用一次AppPreferences bean定义,Spring容器创建了AppPreferences bean的一个新实例。也就是说,appPreferences bean的作用域在ServletContext级别,并存储为一个常规的ServletContext属性。
这有点类似于Spring单例bean,但在两个重要方面有所不同:它是每个ServletContext的单例对象,而不是每个Spring 'ApplicationContext’的单例对象(在任何给定的web应用程序中可能有多个); 它实际上是公开的,因此作为ServletContext属性可见。
在使用注解驱动的组件或Java配置时,可以使用@ApplicationScope注解将组件分配给Application作用域:

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

设置了作用域的Bean作为依赖

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

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

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

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

作为扩展的变体,您可以声明ObjectProvider<MyTargetBean>,它提供了几个额外的访问变体,包括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
        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定义中. (see Choosing the Type of Proxy to Create and XML Schema-based configuration)。为什么在request、session和自定义范围级别定义作用域的bean需要<aop:scoped-proxy/>元素。考虑下面的单例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会话中获取真实的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:scoped-proxy/>元素标记的bean创建代理时,将创建一个基于cglib的类代理。

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

或者,您可以配置Spring容器,为这种作用域bean创建标准的基于JDK接口的代理,方法是为<aop:scoped-proxy/>元素的代理目标类属性的值指定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>

有关选择基于类或基于接口的代理的详细信息,请看这里

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章