|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
Bean 作用域
当你创建一个 bean 定义时,你实际上是在创建一个用于生成该 bean 定义所指定类的实际实例的“配方”。将 bean 定义视为一种配方这一概念非常重要,因为这意味着,就像使用类一样,你可以从单个配方中创建多个对象实例。
你不仅可以控制从特定 bean 定义创建的对象所注入的各种依赖项和配置值,还可以控制从该 bean 定义创建的对象的作用域。这种方法强大而灵活,因为你可以通过配置来选择所创建对象的作用域,而不必在 Java 类级别硬编码对象的作用域。Bean 可以被定义为部署在多种作用域之一。
Spring 框架支持六种作用域,其中四种仅在使用 Web 感知的 ApplicationContext 时才可用。你还可以创建自定义作用域。
下表描述了所支持的作用域:
| 作用域 | <description> </description> |
|---|---|
(默认)将单个 bean 定义的作用域限定为每个 Spring IoC 容器对应一个对象实例。 |
|
将单个 bean 定义的作用域限定为任意数量的对象实例。 |
|
将单个 bean 定义的作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都会基于同一个 bean 定义创建各自独立的 bean 实例。此作用域仅在支持 Web 的 Spring |
|
将单个 bean 定义的作用域限定为 HTTP |
|
将单个 bean 定义的作用域限定为 |
|
将单个 bean 定义的作用域限定为 |
线程作用域可用,但默认未注册。更多信息,
请参阅
SimpleThreadScope的文档。
关于如何注册此作用域或任何其他自定义作用域的说明,请参见
使用自定义作用域。 |
单例作用域
Spring 容器仅管理单例 bean 的一个共享实例,所有请求 ID 或 IDs 与该 bean 定义相匹配的 bean 时,都会返回这同一个特定的 bean 实例。
换句话说,当你定义一个 bean 定义并将其作用域设置为单例(singleton)时,Spring IoC 容器会根据该 bean 定义精确地创建一个对象实例。这个唯一的实例会被存储在单例 bean 的缓存中,此后所有对该命名 bean 的请求和引用都会返回该缓存中的对象。下图展示了单例作用域的工作方式:
Spring 中单例 bean 的概念与《四人帮》(Gang of Four, GoF)设计模式一书中所定义的单例模式有所不同。GoF 单例模式将对象的作用域硬编码,使得每个 ClassLoader 只能创建某个特定类的一个且仅一个实例。而 Spring 单例的作用域最好被描述为“每个容器、每个 bean”。这意味着,如果你在单个 Spring 容器中为某个特定类定义了一个 bean,那么 Spring 容器将根据该 bean 定义创建该类的一个且仅一个实例。单例作用域是 Spring 中的默认作用域。要在 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"/>
原型作用域(Prototype Scope)
Bean 部署的非单例(prototype)作用域会导致每次请求该特定 Bean 时都创建一个新的 Bean 实例。也就是说,当该 Bean 被注入到另一个 Bean 中,或者你通过容器上的 getBean() 方法调用来请求它时,都会创建新实例。通常情况下,你应该对所有有状态的 Bean 使用 prototype 作用域,而对无状态的 Bean 使用 singleton 作用域。
下图展示了 Spring 的原型作用域:
(数据访问对象(DAO)通常不会被配置为原型(prototype),因为典型的 DAO 不保存任何会话状态。对我们来说,复用单例(singleton)图表的核心部分更为简便。)
以下示例在 XML 中将一个 bean 定义为原型(prototype):
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其它作用域不同,Spring 并不管理原型(prototype)Bean 的完整生命周期。容器会实例化、配置并以其他方式组装一个原型对象,然后将其交给客户端,之后便不再保留该原型实例的任何记录。因此,尽管初始化生命周期回调方法会对所有对象(无论其作用域如何)进行调用,但对于原型 Bean 而言,所配置的销毁生命周期回调方法则不会被调用。客户端代码必须自行清理原型作用域的对象,并释放这些原型 Bean 所持有的昂贵资源。若希望 Spring 容器释放原型作用域 Bean 所持有的资源,可以尝试使用一个自定义的Bean 后处理器(bean post-processor),该处理器持有需要清理的 Bean 的引用。
在某些方面,Spring 容器对于原型作用域(prototype-scoped)Bean 的作用相当于 Java 的 new 操作符。在此之后的所有生命周期管理都必须由客户端自行处理。(有关 Spring 容器中 Bean 生命周期的详细信息,请参阅生命周期回调。)
具有原型 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 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-scoped)和会话作用域(session-scoped)的 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 注解将组件指定为 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 容器在整个 Web 应用程序中仅使用 AppPreferences bean 定义创建一次 appPreferences bean 的新实例。也就是说,appPreferences bean 的作用域限定在 ServletContext 级别,并作为常规的 ServletContext 属性进行存储。这在某种程度上类似于 Spring 的单例(singleton)bean,但在两个重要方面有所不同:它是每个 ServletContext 对应一个单例,而不是每个 Spring ApplicationContext(在任意给定的 Web 应用程序中可能存在多个 ServletContext)对应一个单例;并且它实际上是对外暴露的,因此可作为 8 属性被直接访问和查看。
在使用基于注解的组件或 Java 配置时,您可以使用
@ApplicationScope 注解将组件分配给 application 作用域。以下示例展示了如何实现这一点:
-
Java
-
Kotlin
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket 作用域
WebSocket 作用域与 WebSocket 会话的生命周期相关联,适用于基于 WebSocket 的 STOMP 应用程序,更多详情请参见WebSocket 作用域。
作为依赖的作用域 Bean
Spring IoC 容器不仅管理对象(Bean)的实例化,还负责协作对象(或依赖项)的装配。如果你想将一个 HTTP 请求作用域的 Bean 注入到另一个生命周期更长的 Bean 中,可以选择注入一个 AOP 代理来代替该作用域 Bean。也就是说,你需要注入一个代理对象,该代理对象公开与作用域对象相同的公共接口,但还能从相应的作用域(例如 HTTP 请求)中获取真实的目标对象,并将方法调用委托给该真实对象。
|
您也可以在作用域为 当对一个作用域为 此外,作用域代理(scoped proxies)并不是以生命周期安全的方式访问较短作用域中 Bean 的唯一方法。您还可以将注入点(即构造函数或 setter 参数,或自动装配的字段)声明为 作为一种扩展变体,您可以声明 此功能在 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>
| 1 | 定义代理的那行代码。 |
要创建此类代理,需在作用域 bean 的定义中插入一个子元素 <aop:scoped-proxy/>(参见
选择要创建的代理类型
和 基于 XML Schema 的配置)。
为什么在常见场景中,request、session 以及自定义作用域级别的 bean 定义需要使用 <aop:scoped-proxy/> 元素?
请考虑以下单例(singleton)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豆中,而该豆并不知道这个UserPreferences引用是一个代理。在该示例中,当一个UserManager实例调用其通过依赖注入获取的UserPreferences对象的方法时,实际上是在调用代理对象上的方法。该代理随后从(在这种情况下)HTTP 获取实际的
UserPreferences 对象,并将方法调用委托给检索到的实际
UserPreferences 对象。
因此,在将 request- 和 session-scoped 的 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 代理不会拦截私有方法。尝试在这样的代理上调用私有方法时,不会将调用委托给实际的作用域目标对象。 |
或者,你可以通过将 false 元素的 proxy-target-class 属性值设为 <aop:scoped-proxy/>,来配置 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>
有关选择基于类的代理还是基于接口的代理的更多详细信息,请参阅代理机制。
自定义作用域
Bean 的作用域机制是可扩展的。你可以定义自己的作用域,甚至重新定义已有的作用域,尽管后者被认为是不良实践,而且你不能覆盖内置的 singleton(单例)和 prototype(原型)作用域。
创建自定义作用域
要将自定义作用域集成到 Spring 容器中,您需要实现
org.springframework.beans.factory.config.Scope 接口,该接口在本节中进行了描述。关于如何实现您自己的作用域,可以参考 Spring Framework 自身提供的 Scope
实现,以及 Scope Javadoc,
其中更详细地解释了您需要实现的方法。
Scope 接口提供了四种方法,用于从作用域中获取对象、从作用域中移除对象,以及销毁这些对象。
例如,会话作用域(session scope)的实现会返回一个会话作用域的 Bean(如果该 Bean 不存在,则该方法会创建一个新的 Bean 实例,并将其绑定到会话中以供将来引用)。以下方法从底层作用域中返回该对象:
-
Java
-
Kotlin
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
例如,会话作用域(session scope)的实现会从底层会话中移除该会话作用域的 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 容器感知到这些新的作用域。以下方法是向 Spring 容器注册新 Scope 的核心方法:
-
Java
-
Kotlin
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
该方法声明在 ConfigurableBeanFactory 接口中,而大多数 Spring 自带的具体 BeanFactory 实现都通过其 ApplicationContext 属性提供了对该接口的访问。
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 实现,您不仅限于以编程方式注册作用域。您还可以使用 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
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>
当你在 <aop:scoped-proxy/> 实现的 <bean> 声明中放置 FactoryBean 时,被作用域限定的是工厂 Bean 本身,而不是从 getObject() 方法返回的对象。 |