Spring 中 Bean 的作用域有哪些?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
基础掌握度:面试官不仅仅是想知道你能背出几种作用域,更是想看你能否准确描述每种作用域的生命周期和适用场景,能不能说出
singleton和prototype的核心区别。 -
实践经验:考察你是否在真实项目中用过非 singleton 的作用域,比如 Web 环境下的
request、session作用域,有没有踩过坑。 -
原理理解:是否理解
singleton下注入prototypeBean 的 "陷阱",以及解决方案,这才是拉开差距的地方。
核心答案
Spring 框架支持 6 种 Bean 作用域,其中前 2 种是所有环境都可用的,后 4 种只在 Web 环境下生效——
| 作用域 | 说明 | 适用环境 |
|---|---|---|
singleton |
默认值。整个 IoC 容器中只有一个 Bean 实例 | 所有环境 |
prototype |
每次获取 Bean 时都创建一个新实例 | 所有环境 |
request |
每个 HTTP 请求创建一个实例,请求结束即销毁 | 仅 Web |
session |
每个 HTTP Session 创建一个实例,Session 结束即销毁 | 仅 Web |
application |
每个 ServletContext 对应一个实例 |
仅 Web |
websocket |
每个 WebSocket 会话对应一个实例 | 仅 Web |
深度解析
一、singleton — 默认但别无脑用
singleton 是 Spring 的默认作用域,意思是 Spring 容器启动时(严格来说是首次被请求时)创建一个实例,之后所有地方拿到的都是同一个对象。
// 默认就是 singleton,写不写都一样
@Component
public class UserService {
// ...
}
// 显式指定
@Component
@Scope("singleton")
public class UserService {
// ...
}
大多数业务场景用 singleton 没问题,因为 Spring Bean 通常是无状态的(不保存会话数据),靠方法参数和 ThreadLocal 来处理并发。但如果你在 singleton Bean 里存了可变状态,那就是给自己埋雷——多线程并发访问会出问题。
二、prototype — 用得少但得懂
prototype 作用域下,每次从容器中获取 Bean(通过 getBean() 或依赖注入),都会拿到一个全新的实例。Spring 只负责创建,不负责销毁,生命周期管理交给了使用者。
@Component
@Scope("prototype")
public class PaymentTask {
private String orderId;
public void setOrderId(String orderId) {
this.orderId = orderId;
}
}
适合那些有状态的、需要独立副本的对象,比如每次请求都要携带不同参数的任务对象。
三、Web 环境的四种作用域
这四个只在 Web 环境下才有意义,底层依赖 ServletRequest、HttpSession、ServletContext 等 Web 对象。
上图展示了四种 Web 作用域的生命周期,核心区别在于 "绑定的生命周期对象" 不同:
request:绑定一次 HTTP 请求。请求进来时创建,响应返回时销毁。适合存放请求级别的数据,比如当前请求的用户信息。session:绑定一次 HTTP 会话。用户首次访问创建,Session 过期或手动销毁时结束。适合存放会话级别的数据,比如购物车。application:绑定ServletContext,和 Web 应用同生命周期。效果类似singleton,但singleton是每个 Spring 容器一个,application是每个 ServletContext 一个(理论上一个 Web 应用可能有多个 Spring 容器)。websocket:Spring 4.2 新增,绑定 WebSocket 会话。适合需要在 WebSocket 连接期间保持状态的场景。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String userId;
// 每个请求独立的上下文
}
注意那个 proxyMode,下面细说为什么需要它。
四、singleton 里注入 prototype 的 "陷阱"
这个是面试官最爱追问的点。假设你有一个 singleton 的 Service,里面注入了一个 prototype 的 Bean——
@Component // 默认 singleton
public class OrderService {
@Autowired
private PaymentTask paymentTask; // prototype
public void process(String orderId) {
paymentTask.setOrderId(orderId);
paymentTask.execute();
}
}
你以为每次调用 process() 都会用新的 PaymentTask?错了。因为 OrderService 是 singleton,Spring 只会在创建它的时候注入一次 PaymentTask,之后用的永远是同一个实例。prototype 根本没起作用。
三种解决方案:
方案一:使用 @Lookup 方法注入(推荐)
@Component
public abstract class OrderService {
@Lookup // Spring 会通过 CGLIB 生成子类重写这个方法
public abstract PaymentTask getPaymentTask();
public void process(String orderId) {
PaymentTask task = getPaymentTask(); // 每次拿到新实例
task.setOrderId(orderId);
task.execute();
}
}
方案二:使用 ObjectFactory / Provider
@Component
public class OrderService {
@Autowired
private ObjectFactory<PaymentTask> paymentTaskFactory;
public void process(String orderId) {
PaymentTask task = paymentTaskFactory.getObject(); // 每次拿到新实例
task.setOrderId(orderId);
task.execute();
}
}
方案三:使用 Scoped Proxy(代理模式)
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PaymentTask {
// ...
}
加了 proxyMode 后,Spring 注入的其实是一个代理对象,每次调用方法时代理会去容器拿真正的 prototype 实例。Web 作用域(request、session)也需要用这种方式,因为 singleton Bean 创建时可能还没有 Request/Session 上下文。
五、如何指定作用域
// 方式一:注解
@Component
@Scope("prototype")
public class MyBean {}
// 方式二:Java Config
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public MyBean myBean() {
return new MyBean();
}
}
// 方式三:XML(现在很少用了)
<bean id="myBean" class="com.example.MyBean" scope="prototype" />
面试高频追问
-
追问一:singleton Bean 是线程安全的吗?
不是。
singleton只保证容器中只有一个实例,但如果 Bean 内部有可变状态,多线程并发修改就会出问题。通常的做法是:Bean 设计成无状态的,或者用ThreadLocal,或者通过prototype作用域来避免共享。 -
追问二:Spring 的 singleton 和 GOF 设计模式中的单例模式有什么区别?
GOF 单例是类加载级别的,一个 ClassLoader 只有一个实例,通过私有构造函数实现。Spring 的
singleton是容器级别的,一个 Spring IoC 容器中只有一个实例,但你可以自己new一个同类型的对象,不冲突。Spring 单例的实现靠的是容器管理,而不是私有构造函数。 -
追问三:
request作用域的 Bean 在非 Web 环境下能用吗?不能。会抛出
IllegalStateException,因为这些作用域依赖 Web 环境的RequestContextHolder。如果你需要在非 Web 环境下实现类似效果,可以考虑自定义作用域(实现Scope接口)。
常见面试变体
- "Spring Bean 默认是什么作用域?能改成其他的吗?"
- "singleton 里注入 prototype 会怎样?怎么解决?"
- "
request和session作用域有什么区别?"
记忆口诀
六种作用域:单(singleton)、多(prototype)、请(request)、会(session)、应(application)、ws(websocket)。前两个通用,后四个限定 Web。
singleton 注 prototype 的坑:单例只注入一次,原型变 "假原型"。解决靠 @Lookup、ObjectFactory、代理三把斧。
总结
Spring 的 6 种 Bean 作用域,核心掌握 singleton 和 prototype 的区别就够了。生产中 99% 的场景用的都是 singleton,但面试官真正想考察的是你知不知道 "singleton 注入 prototype 的陷阱" 以及对应的解决方案。把这个点讲清楚,这道题基本满分。
