Bean 范围
当你创建一个 bean 定义时,你是在为创建该 bean 定义所定义的类的实际实例创建一个食谱。将 bean 定义视为食谱的想法很重要,因为这意味着,像类一样,你可以从一个单一的食谱中创建多个对象实例。
您不仅可以控制要插入到从特定 bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 bean 定义创建的对象的作用域。这种方法强大而灵活,因为您可以通过配置选择您创建的对象的作用域,而不必在 Java 类级别上固定对象的作用域。可以将 bean 定义为在多种作用域之一中部署。Spring 框架支持六种作用域,其中四种仅在您使用 web-aware ApplicationContext
时可用。您还可以创建 自定义作用域。
下表描述了支持的范围:
表 1. 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 的上下文中有效。 |
线程作用域是可用的,但默认情况下未注册。有关更多信息,请参见 SimpleThreadScope 的文档。有关如何注册此或任何其他自定义作用域的说明,请参见 使用自定义作用域。
单例作用域
只有一个共享的单例 bean 实例被管理,所有对与该 bean 定义匹配的 ID 或 IDs 的 bean 请求都将由 Spring 容器返回该一个特定的 bean 实例。
换句话说,当你定义一个 bean 定义并将其作用域设置为单例时,Spring IoC 容器会创建该 bean 定义所定义的对象的确切一个实例。这个单一实例被存储在一个单例 bean 的缓存中,所有后续对该命名 bean 的请求和引用都会返回缓存的对象。以下图像展示了单例作用域的工作原理:
Spring 的单例 bean 概念与《设计模式:可复用面向对象软件的基础》一书中定义的单例模式有所不同。GoF 单例模式硬编码了对象的作用域,使得每个 ClassLoader 只能创建一个特定类的实例。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"/>
原型范围
非单例原型作用域的 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 的引用。
在某些方面,Spring 容器在原型作用域 bean 中的角色是 Java new
操作符的替代。此后所有的生命周期管理必须由客户端处理。(有关 Spring 容器中 bean 生命周期的详细信息,请参见 Lifecycle Callbacks。)
单例 Bean 与原型 Bean 依赖关系
当你在使用具有原型 bean 依赖的单例作用域 bean 时,要注意依赖关系是在实例化时解析的。因此,如果你将一个原型作用域的 bean 注入到一个单例作用域的 bean 中,那么一个新的原型 bean 将被实例化,并随后被注入到单例 bean 中。原型实例是唯一提供给单例作用域 bean 的实例。
然而,假设您希望单例作用域的 bean 在运行时重复获取原型作用域的 bean 的新实例。您不能将原型作用域的 bean 依赖注入到您的单例 bean 中,因为该注入仅在 Spring 容器实例化单例 bean 并解析和注入其依赖项时发生一次。如果您在运行时需要多次获取原型 bean 的新实例,请参见 方法注入。
请求、会话、应用程序和 WebSocket 范围
request
、session
、application
和 websocket
范围仅在您使用支持 Web 的 Spring ApplicationContext
实现(例如 XmlWebApplicationContext
)时可用。如果您在常规的 Spring IoC 容器中使用这些范围,例如 ClassPathXmlApplicationContext
,则会抛出一个 IllegalStateException
,抱怨未知的 bean 范围。
初始 Web 配置
为了支持在 request
、session
、application
和 websocket
级别(Web 范围的 bean)对 bean 的作用域进行配置,在定义 bean 之前需要进行一些小的初始配置。(对于标准作用域:singleton
和 prototype
,不需要此初始设置。)
您完成此初始设置的方式取决于您的特定 Servlet 环境。
如果您在 Spring Web MVC 中访问作用域 bean,实际上是在由 Spring DispatcherServlet
处理的请求中,则无需进行特殊设置。DispatcherServlet
已经暴露了所有相关状态。
如果您使用 Servlet 网络容器,并且请求在 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
。这使得请求范围和会话范围的 bean 可以在调用链的更深处使用。
请求范围
考虑以下用于 bean 定义的 XML 配置:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器通过使用 loginAction
bean 定义为每一个 HTTP 请求创建一个新的 LoginAction
bean 实例。也就是说,loginAction
bean 的作用域是 HTTP 请求级别。你可以随意更改所创建实例的内部状态,因为从同一 loginAction
bean 定义创建的其他实例不会看到这些状态的变化。这些变化是特定于单个请求的。当请求处理完成后,作用于该请求的 bean 会被丢弃。
当使用基于注解的组件或 Java 配置时,@RequestScope
注解可以用于将一个组件分配到 request
范围。以下示例演示了如何做到这一点:
- Java
- Kotlin
@RequestScope
@Component
public class LoginAction {
// ...
}
@RequestScope
@Component
class LoginAction {
// ...
}
会话范围
考虑以下用于 bean 定义的 XML 配置:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring 容器通过使用 userPreferences
bean 定义为单个 HTTP Session
的生命周期创建 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 {
// ...
}
应用范围
考虑以下用于 bean 定义的 XML 配置:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器通过使用 appPreferences
bean 定义为整个 web 应用程序创建 AppPreferences
bean 的新实例。这意味着 appPreferences
bean 的作用域是在 ServletContext
级别,并作为常规的 ServletContext
属性存储。这在某种程度上类似于 Spring 单例 bean,但在两个重要方面有所不同:它是每个 ServletContext
的单例,而不是每个 Spring ApplicationContext
的单例(在任何给定的 web 应用程序中可能有多个 ApplicationContext
),并且它实际上被暴露,因此作为 ServletContext
属性可见。
当使用基于注解的组件或 Java 配置时,您可以使用 @ApplicationScope
注解将组件分配给 application
范围。以下示例展示了如何做到这一点:
- Java
- Kotlin
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket 范围
WebSocket 范围与 WebSocket 会话的生命周期相关,并适用于 STOMP over WebSocket 应用程序,详细信息请参见 WebSocket scope。
作用域 Bean 作为依赖
Spring IoC 容器不仅管理对象(bean)的实例化,还管理协作对象(或依赖)的连接。如果你想将一个 HTTP 请求作用域的 bean 注入到另一个生命周期更长的 bean 中,你可以选择注入一个 AOP 代理来替代作用域 bean。也就是说,你需要注入一个代理对象,该对象暴露与作用域对象相同的公共接口,但也可以从相关作用域(例如 HTTP 请求)中检索真实的目标对象,并将方法调用委托给真实对象。
您还可以在作用域为 singleton
的 bean 之间使用 <aop:scoped-proxy/>
,此时引用将通过一个可序列化的中间代理进行,从而能够在反序列化时重新获取目标单例 bean。
当针对作用域为 prototype
的 bean 声明 <aop:scoped-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/> // <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>
定义代理的行。
要创建这样的代理,您需要在作用域 bean 定义中插入一个子 <aop:scoped-proxy/>
元素(请参见 选择要创建的代理类型 和 基于 XML 的配置)。
为什么在常见场景中,作用域为 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
实例),该对象可以从作用域机制中获取真实的 UserPreferences
对象(HTTP 请求、Session
等等)。容器将这个代理对象注入到 userManager
bean 中,而 userManager
并不知道这个 UserPreferences
引用是一个代理。在这个例子中,当一个 UserManager
实例调用依赖注入的 UserPreferences
对象上的方法时,它实际上是在调用代理上的方法。然后,代理从(在这种情况下)HTTP Session
中获取真实的 UserPreferences
对象,并将方法调用委托给检索到的真实 UserPreferences
对象。
因此,当将 request-
和 session-scoped
beans 注入到协作对象中时,您需要以下(正确且完整的)配置,如以下示例所示:
<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 代理不会拦截私有方法。尝试在这样的代理上调用私有方法将不会委托给实际的作用域目标对象。
或者,您可以通过为 <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。
直接注入请求/会话引用
作为工厂作用域的替代,Spring WebApplicationContext
还支持通过基于类型的自动装配,将 HttpServletRequest
、HttpServletResponse
、HttpSession
、WebRequest
以及(如果存在 JSF)FacesContext
和 ExternalContext
注入到 Spring 管理的 bean 中,这与其他 bean 的常规注入点并排进行。Spring 通常为这些请求和会话对象注入代理,这具有在单例 bean 和可序列化 bean 中工作的优势,类似于工厂作用域 bean 的作用域代理。
自定义作用域
bean 作用域机制是可扩展的。您可以定义自己的作用域,甚至重新定义现有的作用域,尽管后者被认为是不好的实践,并且您不能覆盖内置的 singleton
和 prototype
作用域。
创建自定义作用域
要将自定义作用域集成到 Spring 容器中,您需要实现 org.springframework.beans.factory.config.Scope
接口,本节将对此进行描述。有关如何实现您自己的作用域的想法,请参阅 Spring 框架本身提供的 Scope
实现以及 Scope javadoc,其中更详细地解释了您需要实现的方法。
Scope
接口有四个方法,用于从作用域中获取对象、从作用域中移除对象以及允许对象被销毁。
会话范围的实现,例如,返回会话范围的 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
这个标识符在每个作用域中都是不同的。对于会话作用域的实现,这个标识符可以是会话标识符。
使用自定义作用域
在您编写并测试一个或多个自定义 Scope
实现后,您需要让 Spring 容器意识到您的新作用域。以下方法是将新 Scope
注册到 Spring 容器的核心方法:
- 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
实现,然后按照下一个示例中的方式注册它。
下一个示例使用 SimpleThreadScope
,该组件包含在 Spring 中,但默认情况下未注册。对于您自己自定义的 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
实现,您不仅可以通过编程方式注册作用域。您还可以通过使用 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>
当你在 <bean>
声明中为 FactoryBean
实现放置 <aop:scoped-proxy/>
时,作用域是针对工厂 bean 本身,而不是从 getObject()
返回的对象。