Bean Scopes
当创建 Bean 定义时,实际上你是在为创建该 Bean 定义所定义的类的实际实例创建步骤。Bean 定义是步骤这一思想很重要,因为这意味着,就像对一个类一样,你可以从一个步骤创建多个对象实例。
你不仅可以控制要插入到从特定 Bean 定义创建的对象中的各种依赖性和配置值,还可以控制从特定 Bean 定义创建的对象的范围。这种方法非常强大且灵活,因为你可以通过配置选择你创建的对象的范围,而不是必须在 Java 类级别设置对象的范围。可以将 Bean 定义为在多个范围之一中进行部署。Spring Framework 支持六个范围,其中四个仅在你使用一个 Web 感知 ApplicationContext
时可用。你还可以创建 a custom scope.
下表描述了受支持的作用域:
Scope | Description |
---|---|
(默认)将一个 Bean 定义限定为每个 Spring IoC 容器的一个对象实例。 |
|
将一个 Bean 定义限定为任意数量的对象实例。 |
|
将一个 Bean 定义限定为一个 HTTP 请求的生命周期。即,每个 HTTP 请求都有一个由一个 Bean 定义创建的 Bean 实例。仅在 web 兼容的 Spring `ApplicationContext`上下文中有效。 |
|
将一个 Bean 定义限定为一个 HTTP `Session`的生命周期。仅在 web 兼容的 Spring `ApplicationContext`上下文中有效。 |
|
将一个 Bean 定义限定为一个 `ServletContext`的生命周期。仅在 web 兼容的 Spring `ApplicationContext`上下文中有效。 |
|
将一个 Bean 定义限定为一个 `WebSocket`的生命周期。仅在 web 兼容的 Spring `ApplicationContext`上下文中有效。 |
提供一个线程范围,但默认情况下不会注册该范围。欲了解更多信息,请参阅https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/support/SimpleThreadScope.html[ |
The Singleton Scope
单例 bean 只有一个共享的实例受到管理,而且对 ID 与该 bean 定义匹配的 bean 的所有请求都会导致 Spring 容器返回那个特定的 bean 实例。
换句话说,当您定义一个 bean 定义且其范围设为单例时,Spring IoC 容器会确切创建一个由该 bean 定义指定的对象实例。此单个实例会存储在这样一个单例 bean 的缓存中,而且对名为 bean 的所有后续请求和引用都会返回缓存的对象。下图展示了单例范围的工作原理:
Spring 的单例 bean 的概念与四人帮 (GoF) 模式手册中定义的单例模式不同。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"/>
The Prototype Scope
bean 部署的非单例原型范围会在每次为特定的 bean 创建一个新的 bean 实例时产生。也就是说,该 bean 会注入到另一 bean 中,或者您可以通过容器上的 getBean()
方法调用请求它。作为规则,您应该将原型范围用于所有有状态的 bean,并将单例范围用于无状态的 bean。
下图展示了 Spring 原型范围:
(数据访问对象 (DAO) 通常不会配置为原型,因为典型的 DAO 不会持有任何会话状态。我们更容易重复使用单例图的核心。)
以下示例在 XML 中将一个 bean 定义为原型:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他范围相反,Spring 不会管理原型 Bean 的完整生命周期。容器实例化、配置和以其他方式组装一个原型对象,并将其传递给客户端,而不进一步记录该原型实例。因此,尽管在所有对象上调用初始化生命周期回调方法,无论范围如何,在原型的情况下,不会调用已配置的销毁生命周期回调。客户端代码必须清理原型作用域的对象并释放原型 Bean 持有的昂贵资源。要让 Spring 容器释放原型作用域 Bean 持有的资源,请尝试使用包含对需要清理的 Bean 的引用的自定义 bean post-processor。
在某些方面,Spring 容器在原型范围 Bean 方面的作用是 Java new
运算符的替代品。此点之后的全部生命周期管理都必须由客户端处理。(有关 Spring 容器中 Bean 的生命周期的详细信息,请参阅 Lifecycle Callbacks。)
Singleton Beans with Prototype-bean Dependencies
当您对单例范围 bean 使用对原型 bean 的依赖项时,请注意在实例化时解决依赖项。因此,如果您将原型范围的 bean 依赖性注入到单例范围的 bean 中,则会实例化一个新的原型 bean,然后将其依赖性注入到单例 bean 中。该原型实例是唯一一直提供给单例范围的 bean 的实例。
但是,假设您想要让单例范围的 bean 在运行时重复获取样例范围 bean 的新实例。您不能将样例范围的 bean 依赖注入到您的单例 bean 中,因为当 Spring 容器实例化单例 bean 并且解决并注入其依赖项时,这种注入仅发生一次。如果您在运行时需要一个样例 bean 的新实例多次,请参阅 Method Injection。
Request, Session, Application, and WebSocket Scopes
request
、session
、application
和 websocket
范围仅在您使用 web 感知的 Spring ApplicationContext
实现(如 XmlWebApplicationContext
)时可用。如果您将这些范围与常规 Spring IoC 容器(如 ClassPathXmlApplicationContext
)一起使用,则会抛出一个 IllegalStateException
,指出未知的 bean 范围。
Initial Web Configuration
为了在 request
、session
、application
和 websocket
级别(基于 web 的 bean)支持 bean 的范围(基于 web 的 bean),在定义 bean 之前进行一些小的初始配置是必需的。(对于标准范围(singleton
和 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 请求对象绑定到为该请求提供服务的 Thread
。这使得在后续的调用链中可使用受 request 和 session 范围约束的 bean。
Request scope
考虑以下 XML 配置,用于 bean 定义:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器对于每一次 HTTP 请求,通过使用 loginAction
bean 定义来创建 LoginAction
bean 的一个新实例。也就是说,loginAction
bean 的作用域为 HTTP 请求级别。您可以根据需要更改实例的内部状态,因为从同一 loginAction
bean 定义创建的其他实例看不到这些状态变化。它是针对个人请求而言的。当请求处理完成后,作用域在请求中的 bean 会被丢弃。
在使用注解驱动的组件或 Java 配置时,可以使用 @RequestScope
注解将一个组件分配给 request
作用域。以下示例说明了如何执行此操作:
-
Java
-
Kotlin
@RequestScope
@Component
public class LoginAction {
// ...
}
@RequestScope
@Component
class LoginAction {
// ...
}
Session Scope
考虑以下 XML 配置,用于 bean 定义:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring 容器对于一个 HTTP Session
的生命周期,通过使用 userPreferences
bean 定义来创建一个 UserPreferences
bean 的新实例。换句话说,userPreferences
bean 的作用域实际上是 HTTP Session
级别。对于采用请求作用域的 bean,您可以根据需要更改实例的内部状态,了解使用相同 userPreferences
bean 定义创建的其他人 HTTP Session
实例不会看到这些状态变化,因为它们针对单个 HTTP Session
而言。当 HTTP Session
最终被丢弃时,作用域在该特定 HTTP Session
中的 bean 也将被丢弃。
在使用注解驱动的组件或 Java 配置时,您可以使用 @SessionScope
注解将一个组件分配给 session
作用域。
-
Java
-
Kotlin
@SessionScope
@Component
public class UserPreferences {
// ...
}
@SessionScope
@Component
class UserPreferences {
// ...
}
Application Scope
考虑以下 XML 配置,用于 bean 定义:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器对于整个 Web 应用程序,通过使用 appPreferences
bean 定义来创建 AppPreferences
bean 的一个新实例。也就是说,appPreferences
bean 的作用域为 ServletContext
级别并存储为常规 ServletContext
属性。这在某种程度上类似于 Spring 单例 bean,但有两种重要的区别:它对于每个 ServletContext
为单例,对于每个 Spring ApplicationContext
而言不是(其中在任何给定 Web 应用程序中可能有多个),而且它实际暴露,因此可见为 ServletContext
属性。
在使用注解驱动的组件或 Java 配置时,您可以使用 @ApplicationScope
注解将一个组件分配给 application
作用域。以下示例说明了如何执行此操作:
-
Java
-
Kotlin
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket Scope
WebSocket 范围与 WebSocket 会话的生命周期关联,并适用于通过 WebSocket 的 STOMP,有关更多详细信息,请参阅“WebSocket scope”。
Scoped Beans as Dependencies
Spring IoC 容器不仅管理您的对象(bean)的实例化,还管理协作者(或依赖项)的连接。如果您想将(例如)HTTP 请求作用域 bean 注入另一个生命周期更长的 bean,您可以选择注入一个 AOP 代理以代替作用域 bean。也就是说,您需要注入一个代理对象,它公开与作用域对象相同的公共接口,而且还可以从相关作用域(例如 HTTP 请求)检索真实目标对象并将方法调用委托到真实对象。
您还可以在作用域为 |
以下示例中的配置只有一行,但理解它的 “为什么” 和 “如何” 非常重要:
<?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/> 1
</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>
1 | 定义代理的行。 |
要创建此代理,你可以将一个子 <aop:scoped-proxy/>
元素插入到作用域 Bean 定义中(请参阅 Choosing the Type of Proxy to Create 和 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 Session
作用域 bean(userPreferences
)的引用。这里的重要点是 userManager
bean 是一个单例:它对于每个容器实例化一次,而且它的依赖项(在此场景中只有一个,即 userPreferences
bean)仅注入一次。这意味着 userManager
bean 仅对完全相同的 userPreferences
对象进行操作(即最初为其注入的对象)。
将作用域较短的 bean 注入到作用域较长的 bean 中时,这不是您想要的(例如,将 HTTP Session
作用域的协作 bean 注入单例 bean 作为依赖项)。相反,您需要一个单独的 userManager
对象,而且对于 HTTP Session
的生命周期,您需要一个特定于 HTTP Session
的 userPreferences
对象。因此,容器会创建一个对象,它公开与 UserPreferences
类完全相同的公共接口(理想情况下是一个 UserPreferences
实例的对象),哪个可以通过作用域机制(HTTP 请求、Session
等)获取真实 UserPreferences
对象。容器将此代理对象注入到 userManager
bean 中,而 userManager
意识不到这个 UserPreferences
引用是代理。在此示例中,当 UserManager
实例在依赖项注入的 UserPreferences
对象上调用方法时,它实际上是在对代理调用方法。然后,代理从(在此场景中)HTTP Session
获取真实的 UserPreferences
对象,并将方法调用委托给检索到的真实 UserPreferences
对象。
因此,在将 request
和 session
作用域的 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>
Choosing the Type of Proxy to Create
默认情况下,当 Spring 容器为标记有 <aop: scoped-proxy/>
元素的 bean 创建代理时,便会创建一个基于 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>
有关选择基于类或基于接口的代理的详细信息,请参阅 Proxying Mechanisms。
Injecting Request/Session References Directly
作为对工厂范围的替代,Spring WebApplicationContext
还支持将 HttpServletRequest
、HttpServletResponse
、HttpSession
、WebRequest
(如果存在 JSF)FacesContext
和 ExternalContext
注入到 Spring 管理的 bean 中,只需在其常规注入点旁边进行基于类型的自动装配即可,针对其他 bean 也是如此。通常,Spring 会为这些请求和会话对象注入代理,这些代理和针对工厂范围的 bean 的范围代理类似,优势在于也能够在单例 bean 和可序列化 bean 中工作。
Custom Scopes
bean 范围机制是可扩展的。您可以定义自己的范围,甚至可以重新定义现有范围,不过后者被认为是不良做法,而且您无法覆盖内置的 singleton
和 prototype
范围。
Creating a Custom Scope
要将自定义作用域集成到 Spring 容器中,您需要实现 org.springframework.beans.factory.config.Scope`接口,本节对此进行了描述。关于如何实现您自己的作用域,请参阅与 Spring 框架本身和 `Scope
javadoc 一起提供的 `Scope`实现,其中更详细地解释了您需要实现的方法。
Scope
接口有四种方法来从该范围内获取对象、将其从该范围移除,并让它们被销毁。
例如,会话范围实现返回会话范围的 bean(如果这个 bean 不存在,则该方法返回该 bean 的一个新实例,同时让它绑定到该会话,以供将来参考)。以下方法从底层范围内返回对象:
-
Java
-
Kotlin
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
例如,会话范围实现从底层会话中移除会话范围的 bean。应当返回对象,但如果未找到指定名称的对象,则您可以返回 null
。以下方法从底层范围内移除对象:
-
Java
-
Kotlin
Object remove(String name)
fun remove(name: String): Any
以下方法注册回调,该回调应当在该范围被销毁或该范围中的指定对象被销毁时被调用:
-
Java
-
Kotlin
void registerDestructionCallback(String name, Runnable destructionCallback)
fun registerDestructionCallback(name: String, destructionCallback: Runnable)
有关销毁回调的详细信息,请参阅 javadoc 或 Spring 作用域实现。
以下方法获取底层范围的对话标识符:
-
Java
-
Kotlin
String getConversationId()
fun getConversationId(): String
此标识符对于每个范围而言都不同。对于会话范围实现,此标识符可以是会话标识符。
Using a Custom Scope
编写并测试了一个或多个自定义 Scope
实现后,您需要让 Spring 容器知道您的新范围。以下方法是向 Spring 容器注册新 Scope
的核心方法:
-
Java
-
Kotlin
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
此方法在 ConfigurableBeanFactory
接口上声明,该接口可通过 Spring 附带的大多数具体 ApplicationContext
实现上的 BeanFactory
属性进行访问。
registerScope(..)
方法的第一个参数是与范围关联的唯一名称。Spring 容器本身的此类名称示例包括 singleton
和 prototype
。registerScope(..)
方法的第二个参数是您希望注册并使用的自定义 Scope
实现的真实实例。
假设您编写了您的自定义 Scope
实现,然后按下一个示例所示进行注册。
下一个示例使用 |
-
Java
-
Kotlin
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)
之后,您可以创建符合您自定义 Scope
范围规则的 bean 定义,如下所示:
<bean id="..." class="..." scope="thread">
使用自定义的 Scope
实现,你不受 Scope
的编程注册的限制。你也可以使用 CustomScopeConfigurer
类声明式地注册 Scope
,如下面的示例所示:
<?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>
当你将 @{1} 放置在 @{2} 为 @{3} 实现的声明中时,实际上范围化的是工厂 bean 本身,而不是 @{4} 返回的对象。 |