Bean 作用域 (Bean Scopes)
当你创建一个 Bean 定义时,你实际上是创建了一个用于创建该 Bean 定义所定义的类的实际实例的“蓝图”。Bean 定义是一个蓝图这一理念非常重要,因为它意味着与类一样,你可以从一个蓝图中创建多个对象实例。
你不仅可以控制要插入到从特定 Bean 定义创建的对象中的各种依赖项和配置值,还可以控制从特定 Bean 定义创建的对象的作用域(Scope)。这种方法非常强大且灵活,因为你可以通过配置来选择创建对象的作用域,而不需要在 Java 类级别硬编码对象的作用域。Bean 可以被定义为部署在多种作用域之一中。
Spring 框架支持六种作用域,其中四种只有在使用 Web 感知的 ApplicationContext 时才可用。你也可以创建自定义作用域。
下表描述了支持的作用域:
| 作用域 | 说明 |
|---|---|
| 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)也是可用的,但默认情况下并不注册。更多信息请参阅 SimpleThreadScope 的文档。
单例作用域 (Singleton Scope)
容器只管理一个单例 Bean 的共享实例,所有对该 Bean 的请求(通过 ID 或 ID 匹配)都会导致 Spring 容器返回该特定的 Bean 实例。
换句话说,当你定义一个 Bean 且其作用域为单例时,Spring IoC 容器会为该 Bean 定义所定义的对象创建恰好一个实例。这个单一实例存储在此类单例 Bean 的缓存中,所有后续对该命名 Bean 的请求和引用都会返回该缓存对象。

Spring 的单例 Bean 概念不同于设计模式中定义的单例模式。设计模式中的单例硬编码了对象的作用域,使得每个 ClassLoader 只能创建一个特定类的实例。Spring 单例的作用域最准确的描述是按容器、按 Bean。这意味着,如果你在单个 Spring 容器中为特定类定义了一个 Bean,则 Spring 容器将为该 Bean 定义所定义的类创建且仅创建一个实例。单例作用域是 Spring 中的默认作用域。
在 XML 中定义单例 Bean:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- 以下内容是等效的,尽管是冗余的(单例是默认值) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>原型作用域 (Prototype Scope)
非单例的原型作用域会在每次对该特定 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 时的角色是 Java new 运算符的替代品。此后的所有生命周期管理必须由客户端处理。
具有原型 Bean 依赖项的单例 Bean
当你使用依赖于原型 Bean 的单例作用域 Bean 时,请注意依赖关系在实例化时解析。因此,如果你将原型作用域的 Bean 依赖注入到单例作用域的 Bean 中,则会实例化一个新的原型 Bean,然后将其注入到该单例 Bean 中。该原型实例是提供给该单例作用域 Bean 的唯一实例。
但是,假设你想让单例作用域的 Bean 在运行时反复获取原型作用域 Bean 的新实例。你不能直接注入,因为注入只发生一次。如果你需要在运行时多次获取原型 Bean 的新实例,请参阅方法注入。
Request, Session, Application, and WebSocket 作用域
request、session、application 和 websocket 作用域只有在你使用 Web 感知的 Spring ApplicationContext 实现(如 XmlWebApplicationContext)时才可用。如果在普通的 Spring IoC 容器(如 ClassPathXmlApplicationContext)中使用这些作用域,会抛出 IllegalStateException。
初始 Web 配置
为了支持 request、session、application 和 websocket 级别的 Bean 作用域(Web 作用域 Bean),在定义 Bean 之前需要进行一些简单的初始配置。(标准作用域 singleton 和 prototype 不需要此初始设置。)
如果你在 Spring Web MVC 中访问作用域 Bean,实际上是在由 Spring DispatcherServlet 处理的请求中,则无需特别设置。DispatcherServlet 已经公开了所有相关状态。
如果你使用 Servlet Web 容器,且请求在 Spring 的 DispatcherServlet 之外处理(例如使用 JSF),则需要注册 org.springframework.web.context.request.RequestContextListener。
在 web.xml 中配置:
<web-app>
...
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
...
</web-app>或者使用 RequestContextFilter:
<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>这些组件的作用都是将 HTTP 请求对象绑定到为该请求提供服务的 Thread,从而使得请求和会话作用域的 Bean 在调用链下游可用。
Request 作用域
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>对于每个 HTTP 请求,Spring 容器都会创建一个全新的 LoginAction 实例。也就是说,loginAction Bean 的作用域限定在 HTTP 请求级别。你可以随意更改实例的内部状态,因为由于是从同一个 Bean 定义创建的其他实例,它们看不到这些状态更改。当请求处理完成时,该 Bean 会被丢弃。
使用注解时:
@RequestScope
@Component
public class LoginAction {
// ...
}@RequestScope
@Component
class LoginAction {
// ...
}Session 作用域
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>Spring 容器在单个 HTTP Session 的生命周期内创建一个全新的 UserPreferences 实例。当 HTTP Session 最终失效时,限定在该特定 Session 作用域内的 Bean 也会被丢弃。
使用注解时:
@SessionScope
@Component
public class UserPreferences {
// ...
}@SessionScope
@Component
class UserPreferences {
// ...
}Application 作用域
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>对于整个 Web 应用程序,Spring 容器只为 appPreferences 定义创建一个实例。也就是说,Bean 的作用域限定在 ServletContext 级别,并存储为常规的 ServletContext 属性。这与 Spring 单例 Bean 有些相似,但有两个重要的区别:
- 它在每个
ServletContext中是单例的,而不是每个 SpringApplicationContext(一个 Web 应用中可能有多个子系统容器)。 - 它实际上作为
ServletContext属性公开,因此是可见的。
使用注解时:
@ApplicationScope
@Component
public class AppPreferences { }WebSocket 作用域
WebSocket 作用域与 WebSocket 会话的生命周期相关联,适用于 STOMP over WebSocket 应用程序。更多细节请参阅 WebSocket 作用域。
作为依赖项的作用域 Bean
如果你想将(例如)HTTP request 作用域的 Bean 注入到另一个具有更长生命周期的作用域的 Bean 中,你需要注入一个 AOP 代理。即,你需要注入一个代理对象,它公开与作用域对象相同的公共接口,但也可以从相关作用域(如 HTTP 请求)中检索真正的目标对象,并将方法调用委托给真正的对象。
INFO
- 你也可以在
singleton作用域的 Bean 之间使用<aop:scoped-proxy/>。 - 对
prototype作用域的 Bean 使用<aop:scoped-proxy/>时,代理上的每次方法调用都会导致创建一个新的目标实例。 - ObjectFactory / ObjectProvider:除了代理,你也可以声明注入点为
ObjectFactory<MyTargetBean>,通过getObject()调用按需检索实例,这在生命周期安全方面也是一种好方法。
在 XML 中配置作用域代理:
<!-- 一个 HTTP Session 作用域的 Bean,作为代理公开 -->
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
<!-- 指示容器代理周围的 Bean -->
<aop:scoped-proxy/>
</bean>
<!-- 一个单例作用域的 Bean,注入上述 Bean 的代理 -->
<bean id="userService" class="com.something.SimpleUserService">
<property name="userPreferences" ref="userPreferences"/>
</bean>为什么需要代理?
考虑一个单例 UserManager 依赖一个 Session 作用域的 UserPreferences。 由于 UserManager 是单例,它只会被实例化一次,依赖项也只会被注入一次。如果没有代理,UserManager 将永远持有它在启动时拿到的那个首选对象。 通过使用代理,Spring 注入一个“虚”的对象。每当 UserManager 调用该虚对象的方法时,代理会拦截调用,跑到当前的 HTTP Session 里找到“真”的那个 UserPreferences 实例,然后把活儿转给它。
选择创建代理的类型
默认情况下,Spring 使用 CGLIB 创建类代理。
INFO
CGLIB 代理不会拦截 private 方法。
或者,通过设置 proxy-target-class="false" 使用标准 JDK 接口代理。这要求 Bean 必须实现至少一个接口,并且注入点必须引用接口类型。
自定义作用域 (Custom Scopes)
Bean 作用域机制是可扩展的。你可以定义自己的作用域,甚至重新定义现有的作用域(但不能覆盖 singleton 和 prototype)。
创建自定义作用域
你需要实现 org.springframework.beans.factory.config.Scope 接口。核心方法包括:
get(String name, ObjectFactory<?> objectFactory):从底层作用域获取对象。remove(String name):从底层作用域移除对象。registerDestructionCallback(String name, Runnable callback):注册销毁回调。getConversationId():获取会话标识符。
使用自定义作用域
注册自定义作用域:
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);然后在 XML 中使用:
<bean id="..." class="..." scope="thread">或者使用 CustomScopeConfigurer 进行声明式注册:
<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 的“生存空间”
1. Singleton vs Prototype:分清“共享”与“私有”
- Singleton (单例):全家共用一双筷子。虽然省钱(内存),但如果筷子上有脏东西(状态污染),全家都会受影响。所以单例 Bean 必须是 线程安全 的。
- Prototype (多例):每人发一双新筷子。用完得自己扔(由于 Spring 不管销毁),虽然费钱,但安全。
2. 为什么 Spring 默认选单例?
- 性能:Bean 的创建、依赖注入、生命周期回调都是有开销的。
- 解耦:大多数业务逻辑对象(Service, DAO)本身就是无状态的。
3. Prototype 的“烂摊子”问题 这是个大坑:Spring 只管原型的“生”,不管原型的“死”。 如果你在原型 Bean 里打开了数据库连接或文件流,Spring 的销毁回调(如 @PreDestroy)对原型 Bean 是无效的。如果你疯狂创建原型对象而不手动清理,会导致内存泄漏。
4. 作用域代理(Scoped Proxy)的直白理解 想象你去酒店前台(Singleton 经理)反馈房间问题。无论谁去,经理都是那一个。 但经理处理问题时,会根据你的“房间号”(当前 Session)去查你的“住房记录”(Session Bean)。 代理对象就像是经理桌上的一个拨号盘:你一拨号,它就带你去对应的房间。没有它,经理就记不清谁是谁了。
5. 这种模式在 Spring Boot 中的现状 在现代微服务(Stateless)架构中,request 和 session 作用域的使用频率在下降,因为我们倾向于把状态保存在 Redis 或 JWT 中,而不是 Web 容器的内存里。但在传统的单体应用或复杂的 Web Portal 中,它们依然非常有用。