Spring Bean 的作用域详解
在 Spring 里,Bean 的作用域指的是:同一个 BeanDefinition 被容器创建对象时,到底应该创建几个实例,以及这些实例在什么范围内被复用。
很多人刚接触 Spring 时,会默认认为所有 Bean 都是“单例”。这个理解只对了一半。更准确地说,Spring 默认作用域是 singleton,但 singleton 只是 Spring 容器级别的单例,不等于 JVM 进程级单例,更不等于传统设计模式里严格控制构造的 Singleton 模式。把这件事搞清楚,后面理解 Bean 生命周期、线程安全、Web 请求隔离、Session 数据隔离,都会顺很多。
什么是 Bean 作用域
先看一个最核心的问题:当你多次从 Spring 容器中获取同一个 Bean 时,返回的是不是同一个对象?
答案取决于这个 Bean 的作用域。
Spring 常见作用域包括:
singletonprototyperequestsessionapplicationwebsocket
其中前两个属于最基础、最常用的作用域,后四个主要用于 Web 环境。
singleton:默认作用域
singleton 是 Spring 默认作用域。它表示:
- 在 一个 Spring IoC 容器 中
- 同一个 Bean 名称
- 只会创建 一个实例
- 后续注入或获取时,复用这一个对象
示例
@Component
@Scope("singleton")
public class UserService {
}
如果省略 @Scope,默认也是 singleton。
测试代码:
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
UserService bean1 = context.getBean(UserService.class);
UserService bean2 = context.getBean(UserService.class);
System.out.println(bean1 == bean2); // true
输出为 true,说明拿到的是同一个对象。
singleton 的本质
这里要特别注意,Spring 的 singleton 是:
- 每个 IoC 容器一个实例
- 不是整个应用绝对全局唯一
- 如果你创建两个
ApplicationContext,同一个 Bean 会有两份实例
示例:
AnnotationConfigApplicationContext context1 =
new AnnotationConfigApplicationContext(AppConfig.class);
AnnotationConfigApplicationContext context2 =
new AnnotationConfigApplicationContext(AppConfig.class);
UserService bean1 = context1.getBean(UserService.class);
UserService bean2 = context2.getBean(UserService.class);
System.out.println(bean1 == bean2); // false
这也是很多文章容易讲模糊的地方。Spring 单例是容器维度,不是 ClassLoader 维度,不是 JVM 维度。
singleton 的适用场景
适合无状态 Bean,比如:
- Service
- DAO / Repository
- 配置类
- 工具类
- 大多数业务组件
这里的“无状态”指的是:Bean 本身不保存用户请求之间互相影响的数据。
例如下面这种就适合单例:
@Service
public class OrderService {
public void createOrder(Long userId, Long productId) {
// 通过参数执行业务逻辑,不在成员变量里保存用户态数据
}
}
singleton 的线程安全问题
很多人一看到单例就问:线程安全吗?
答案是:Spring 不会自动保证 singleton Bean 的线程安全。
Spring 只负责把它做成一个实例,不负责你在这个实例里怎么写代码。
例如:
@Service
public class CounterService {
private int count = 0;
public int increment() {
return ++count;
}
}
这在并发环境下就是线程不安全的。因为多个线程会同时修改同一个成员变量。
正确理解应该是:
- singleton 只决定实例数量
- 线程安全取决于 Bean 内部是否有共享可变状态
所以实际开发里,单例 Bean 最好遵循一个原则:
尽量设计成无状态 Bean,不在成员变量中保存会变化的业务数据。
prototype:多例作用域
prototype 表示:每次向容器获取 Bean,都会创建一个新的实例。
示例
@Component
@Scope("prototype")
public class TaskProcessor {
}
测试代码:
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
TaskProcessor bean1 = context.getBean(TaskProcessor.class);
TaskProcessor bean2 = context.getBean(TaskProcessor.class);
System.out.println(bean1 == bean2); // false
输出为 false,说明每次获取都是新对象。
prototype 的典型场景
适合有状态对象,例如:
- 临时任务处理器
- 构建器对象
- 每次使用都需要独立上下文的组件
- 非线程安全且必须独占使用的对象
例如:
@Component
@Scope("prototype")
public class UploadContext {
private String fileName;
private long fileSize;
// getter/setter
}
每次拿一个新的 UploadContext,互不影响。
prototype 的一个关键细节:Spring 只负责创建,不负责完整销毁
这是 prototype 最容易被忽略的点。
对于 singleton Bean:
- 容器启动时创建
- 容器关闭时销毁
- 初始化和销毁回调都完整参与
对于 prototype Bean:
- 容器每次请求时创建
- 创建完交给调用方
- 容器不再跟踪其完整生命周期
- 一般不会在容器关闭时帮你执行销毁逻辑
比如:
@Component
@Scope("prototype")
public class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("init");
}
@PreDestroy
public void destroy() {
System.out.println("destroy");
}
}
你获取 Bean 时,init 会执行;但容器关闭时,destroy 通常不会自动执行。
这意味着:如果 prototype Bean 持有文件句柄、Socket、连接等资源,资源清理责任通常在业务代码,不在 Spring 容器。
request:一次 HTTP 请求一个 Bean
request 作用域只在 Web 环境下有效。它表示:
- 一次 HTTP 请求内
- 同一个 Bean 只创建一个实例
- 不同请求之间互不共享
示例
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class RequestTrace {
private String traceId;
public String getTraceId() {
return traceId;
}
public void setTraceId(String traceId) {
this.traceId = traceId;
}
}
如果一个请求链路中多次注入这个 Bean,得到的是同一个对象;但下一个请求会重新创建。
适用场景
适合保存一次请求内的上下文数据,例如:
- 请求追踪 ID
- 当前请求临时缓存
- 表单处理中间状态
- 请求级鉴权信息封装
注意事项
request Bean 不能脱离 Web 请求上下文独立存在。如果你在非 Web 线程、异步线程、启动阶段去访问它,通常会报错,因为当前没有 request 上下文。
session:一次会话一个 Bean
session 作用域同样只在 Web 环境下有效。它表示:
- 同一个 HTTP Session 内共用一个实例
- 不同用户 Session 之间互相隔离
示例
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION)
public class UserCart {
private List<String> items = new ArrayList<>();
public List<String> getItems() {
return items;
}
}
一个用户在自己的会话周期内多次请求,看到的是同一个购物车对象;另一个用户则是另一份实例。
适用场景
常见于:
- 购物车
- 多步骤表单状态
- 会话级用户偏好
- 临时登录态扩展信息
使用上的现实问题
虽然 session 看起来很方便,但现在很多系统都是前后端分离、无状态认证、分布式部署,所以它使用频率比早期 MVC 应用低不少。
原因很直接:
- Session 依赖服务端状态保存
- 集群下要考虑 Session 共享或粘性会话
- 不利于完全无状态扩展
因此在现代架构中,很多业务更倾向于:
- JWT 保存认证信息
- Redis 保存分布式会话数据
- 尽量减少 HttpSession 中的状态
所以 session 不是不能用,而是要结合架构选型。
application:ServletContext 级别共享
application 作用域表示:
- 在一个
ServletContext范围内共享一个 Bean 实例
示例
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION)
public class AppGlobalState {
private String version = "1.0.0";
public String getVersion() {
return version;
}
}
它的可见范围比 session 更大,是整个 Web 应用共享。
与 singleton 的区别
很多人会问:application 和 singleton 看起来都像全局一个,有什么区别?
区别在于它们所属的“管理边界”不同:
singleton:Spring IoC 容器范围application:ServletContext范围
大多数单体 Web 项目里,两者看起来效果接近,但语义不同。只有在更复杂的 Web 应用上下文场景下,这个区别才会更明显。
websocket:WebSocket 会话级作用域
websocket 作用域用于 WebSocket 场景,表示:
- 每个 WebSocket 会话对应一个 Bean 实例
适合保存 WebSocket 连接相关的上下文状态。
例如:
@Component
@Scope("websocket")
public class WebSocketSessionContext {
private String clientId;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
}
这个作用域用得不算多,但在实时通信、在线协作、消息推送等场景里很有用。
各种作用域的对比
下面把几个常见作用域放在一起看,更容易建立整体认识。
| 作用域 | 含义 | 是否默认 | 是否依赖 Web 环境 | 典型场景 |
|---|---|---|---|---|
| singleton | 容器内单例 | 是 | 否 | Service、Repository、工具类 |
| prototype | 每次获取新建 | 否 | 否 | 有状态临时对象 |
| request | 每次 HTTP 请求一个实例 | 否 | 是 | 请求上下文、链路跟踪 |
| session | 每个 Session 一个实例 | 否 | 是 | 购物车、会话状态 |
| application | 整个 Web 应用一个实例 | 否 | 是 | Web 应用级共享状态 |
| websocket | 每个 WebSocket 会话一个实例 | 否 | 是 | WebSocket 连接上下文 |
如何定义 Bean 作用域
Spring 中定义作用域常见有三种方式:
1. 使用 @Scope
@Component
@Scope("prototype")
public class MyBean {
}
或者使用常量:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyBean {
}
Web 环境常量:
@Component
@Scope(WebApplicationContext.SCOPE_REQUEST)
public class MyRequestBean {
}
2. 在 @Bean 方法上指定
@Configuration
public class BeanConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public TaskProcessor taskProcessor() {
return new TaskProcessor();
}
}
3. XML 配置方式
如果项目还在用 XML,也可以这样写:
<bean id="userService" class="com.example.service.UserService" scope="singleton"/>
<bean id="taskProcessor" class="com.example.service.TaskProcessor" scope="prototype"/>
不过现在新项目基本都以注解配置为主。
singleton 注入 prototype,会发生什么
这是 Spring 作用域里最经典、最容易踩坑的问题之一。
假设有两个 Bean:
OrderService是 singletonTaskProcessor是 prototype
代码如下:
@Component
@Scope("prototype")
public class TaskProcessor {
}
@Service
public class OrderService {
@Autowired
private TaskProcessor taskProcessor;
public TaskProcessor getTaskProcessor() {
return taskProcessor;
}
}
测试:
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = context.getBean(OrderService.class);
TaskProcessor p1 = orderService.getTaskProcessor();
TaskProcessor p2 = orderService.getTaskProcessor();
System.out.println(p1 == p2); // true
很多人会本能地以为是 false,但实际上是 true。
原因是什么
因为 OrderService 是单例,它在容器启动时只创建一次。创建它时,Spring 会把 TaskProcessor 注入进去。虽然 TaskProcessor 定义成了 prototype,但注入动作只发生一次,所以最终 OrderService 成员变量里持有的,仍然是那一次创建出来的对象。
也就是说:
- prototype 的语义是“每次向容器请求时新建”
- 不是“每次使用成员变量时自动新建”
这是两个完全不同的概念。
如何在 singleton 中正确使用 prototype
如果你确实需要在 singleton Bean 中每次都拿到新的 prototype 实例,常见做法有几种。
方式一:注入 ObjectProvider
这是最推荐、最常见的方式。
@Service
public class OrderService {
@Autowired
private ObjectProvider<TaskProcessor> taskProcessorProvider;
public TaskProcessor createTaskProcessor() {
return taskProcessorProvider.getObject();
}
}
测试:
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
OrderService orderService = context.getBean(OrderService.class);
TaskProcessor p1 = orderService.createTaskProcessor();
TaskProcessor p2 = orderService.createTaskProcessor();
System.out.println(p1 == p2); // false
每次调用 getObject(),都会向容器重新取一个 prototype Bean。
方式二:注入 ApplicationContext
能用,但耦合更高。
@Service
public class OrderService {
@Autowired
private ApplicationContext applicationContext;
public TaskProcessor createTaskProcessor() {
return applicationContext.getBean(TaskProcessor.class);
}
}
这本质上是主动从容器拉取对象,属于一种 Service Locator 风格,功能上可行,但不如 ObjectProvider 干净。
方式三:使用 @Lookup
@Component
public abstract class OrderService {
public void process() {
TaskProcessor taskProcessor = createTaskProcessor();
}
@Lookup
protected abstract TaskProcessor createTaskProcessor();
}
Spring 会为这个方法生成运行时代码,每次调用时返回新的 prototype Bean。
这个机制可以用,但相对没那么直观,团队里如果对 Spring 底层理解不一致,可读性不如 ObjectProvider。
作用域代理:解决跨作用域注入问题
当 Web 作用域 Bean 被注入到 singleton Bean 时,情况会更复杂。
例如:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
public class RequestUserContext {
private String userId;
}
@Service
public class UserService {
@Autowired
private RequestUserContext requestUserContext;
}
这里 UserService 是单例,RequestUserContext 是 request 级。如果直接注入,单例创建时当前并没有固定请求对象,通常需要借助作用域代理。
写法如下:
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestUserContext {
private String userId;
}
代理做了什么
Spring 注入给 UserService 的不是实际的 RequestUserContext,而是一个代理对象。这个代理对象在每次真正调用方法时,再根据当前请求上下文找到对应的 request Bean。
所以可以把它理解为:
- 注入阶段注入的是“占位代理”
- 运行阶段按当前作用域解析真实对象
这个机制非常重要。没有它,很多跨作用域注入根本没法正常工作。
常见代理模式
proxyMode = ScopedProxyMode.TARGET_CLASS
表示使用基于类的代理,通常最常用。
也可以用:
proxyMode = ScopedProxyMode.INTERFACES
表示基于接口代理,但前提是 Bean 有合适接口。
Bean 作用域和生命周期的关系
作用域和生命周期紧密相关,但不是同一个概念。
- 作用域决定实例创建和共享范围
- 生命周期决定实例从创建到销毁经历哪些阶段
以 singleton 为例:
- 容器启动
- 实例化 Bean
- 属性注入
- 执行初始化回调
- Bean 被使用
- 容器关闭
- 执行销毁回调
而 prototype 只会走到前半段:
- 请求获取 Bean
- 实例化 Bean
- 属性注入
- 执行初始化回调
- 交给调用者使用
- 容器通常不再负责销毁
所以很多生命周期问题,本质上都跟作用域有关。
实际开发中怎么选作用域
如果只给一个非常实用的结论,那就是:
- 绝大多数 Bean 用 singleton
- 确实需要独立状态时才用 prototype
- 和 HTTP 请求、Session 强相关的数据才用 request/session
- 不要因为“担心线程安全”就滥用 prototype
为什么大多数时候用 singleton
因为 Spring 的主流设计本来就是围绕单例组件展开的:
- 创建成本低
- 依赖管理简单
- 容器启动和运行行为更可预测
- 易于统一管理事务、AOP、缓存、监控等横切逻辑
只要你的 Bean 是无状态的,singleton 通常就是最合理的选择。
为什么不要滥用 prototype
有些人觉得单例不安全,就把 Service 全部改成 prototype。这通常不是正确方案。
原因很简单:
- 线程安全问题的根源是共享可变状态,不是 singleton 这个标签本身
- prototype 会增加对象创建开销
- 生命周期更难管理
- 注入到单例中时还容易产生误解
- 容器对其销毁不做完整托管
真正正确的做法,通常是:
- 保持 Service 无状态
- 把状态放到方法参数、局部变量、数据库、缓存、上下文对象里
- 必要时显式创建短生命周期对象
一个容易混淆的问题:局部变量和成员变量
理解线程安全时,这个区分很关键。
看下面的代码:
@Service
public class PriceService {
public BigDecimal calculate(BigDecimal price, BigDecimal discount) {
BigDecimal result = price.subtract(discount);
return result.max(BigDecimal.ZERO);
}
}
这是单例 Bean,但通常没有线程安全问题,因为方法里的 result 是局部变量,每个线程都有自己的调用栈。
再看这个:
@Service
public class PriceService {
private BigDecimal currentResult;
public BigDecimal calculate(BigDecimal price, BigDecimal discount) {
currentResult = price.subtract(discount);
return currentResult.max(BigDecimal.ZERO);
}
}
这里就有问题了,因为多个线程会共享 currentResult 成员变量。
所以讨论 Bean 作用域时,不要只停留在“单例安全吗”这种表层问题,真正该看的是:这个 Bean 是否保存共享可变状态。
自定义作用域
Spring 还支持自定义 Scope,不过这已经不是日常 CRUD 开发的常规需求,而是偏框架扩展场景。
你可以通过实现 org.springframework.beans.factory.config.Scope 接口,定义自己的对象存储和获取规则。比如:
- 线程级作用域
- 租户级作用域
- 流程级作用域
大致步骤包括:
- 实现
Scope接口 - 注册到 BeanFactory
- 在 Bean 上使用自定义作用域名称
简化示意:
public class ThreadLocalScope implements Scope {
private final ThreadLocal<Map<String, Object>> beanMap =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> map = beanMap.get();
return map.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return beanMap.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
注册后即可使用:
@Component
@Scope("threadLocal")
public class ThreadScopeBean {
}
不过这类方案一定要谨慎。只要引入自定义作用域,就意味着你在改变 Spring 默认的对象管理模型,后续维护成本会明显升高。
面试里经常问的几个点
1. Spring Bean 默认是什么作用域
默认是 singleton。
2. Spring 的 singleton 和设计模式里的单例一样吗
不一样。Spring 的 singleton 是 容器级单例,同一个容器内一个 BeanName 对应一个实例;设计模式单例通常强调某个类在整个运行时环境中只存在一个实例,控制方式也不同。
3. prototype Bean 注入 singleton Bean 时,每次使用都会新建吗
不会。默认只在注入时创建一次。要想每次使用都新建,需要 ObjectProvider、ApplicationContext#getBean()、@Lookup 等方式。
4. prototype Bean 会执行销毁方法吗
一般不会由 Spring 容器自动执行完整销毁流程,资源释放通常要由业务代码负责。
5. 单例 Bean 一定线程安全吗
不一定。是否线程安全取决于 Bean 是否包含共享可变状态。
一段完整示例
下面给一个更完整的示例,把 singleton 和 prototype 的行为放在一起。
@Configuration
@ComponentScan("com.example.scope")
public class AppConfig {
}
package com.example.scope;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeBean {
public PrototypeBean() {
System.out.println("PrototypeBean created: " + this);
}
}
package com.example.scope;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
public class SingletonService {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
public SingletonService(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public PrototypeBean getNewPrototypeBean() {
return prototypeBeanProvider.getObject();
}
}
package com.example.scope;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ScopeTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
SingletonService singletonService = context.getBean(SingletonService.class);
PrototypeBean p1 = singletonService.getNewPrototypeBean();
PrototypeBean p2 = singletonService.getNewPrototypeBean();
System.out.println(p1 == p2); // false
context.close();
}
}
运行时你会看到 PrototypeBean 被创建两次,这才是真正按需获取 prototype 的正确方式。
版本说明
Spring Framework 5.x 与 6.x 在 Bean 作用域上的差异
对于本文讨论的这些核心作用域:
singletonprototyperequestsessionapplicationwebsocket
Spring Framework 5.x 与 6.x 的概念和基本行为没有本质差异。
差异更多体现在:
- Spring 6 基于 Jakarta EE 9+,相关包名从
javax.*迁移为jakarta.* - Web 相关整合依赖发生变化
- Boot 3 基于 Spring 6,整体运行环境要求更高
这意味着如果你在 Spring Boot 2.x 和 3.x 之间迁移项目,Bean 作用域本身的使用思路不用重学,但 Web 相关依赖和导包要注意兼容性。
总结
Bean 的作用域,表面看只是一个配置项,实质上决定了 Spring 如何管理对象实例、如何复用对象,以及对象在并发和 Web 场景中的行为边界。
真正需要记住的不是那几个英文单词,而是下面这几条:
- 默认作用域是 singleton,表示容器级单例
- singleton 不等于线程安全,线程安全取决于是否存在共享可变状态
- prototype 表示每次获取新实例,但注入到 singleton 时不会自动“每次使用都新建”
- request、session、application、websocket 都依赖 Web 上下文
- 跨作用域注入时,经常需要作用域代理或延迟获取机制
- 实际开发中,大多数业务 Bean 都应该优先设计为无状态 singleton
如果把作用域理解透了,Spring 里很多看似分散的问题——比如生命周期、线程安全、依赖注入行为、Web 上下文隔离——都会自然串起来。它不是一个孤立知识点,而是理解 Spring 容器运行方式的基础。