Spring 中 Bean 的作用域有哪些?
结论先行:Spring 内置六种作用域:singleton(默认,容器内唯一)、prototype(每次获取新建)、以及仅在 Web 环境生效的 request、session、application、websocket。作用域决定"实例的数量与生命周期归属",不决定线程安全;单例注入短生命周期 Bean 时必须配 proxyMode 生成作用域代理,否则短生命周期对象会被"钉"在单例里。
一、六种作用域
| 作用域 | 实例数量 | 生命周期 | 生效条件 | 典型用途 |
|---|---|---|---|---|
| singleton | 每容器一个 | 随容器创建与销毁 | 默认 | 无状态 Service、DAO、配置 |
| prototype | 每次 getBean/注入一个 | 容器不管理销毁 | 默认 | 有状态、短命对象 |
| request | 每 HTTP 请求一个 | 请求结束销毁 | Web 环境 | 请求级上下文、参数聚合 |
| session | 每会话一个 | 会话过期销毁 | Web 环境 | 登录用户信息(不推荐存大对象) |
| application | 每 ServletContext 一个 | 应用生命周期 | Web 环境 | 全局缓存、共享配置 |
| websocket | 每 WebSocket 会话一个 | 连接关闭销毁 | WebSocket 环境 | 长连接会话状态 |
二、声明方式
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestHolder {
private String traceId;
// getter / setter
}
<!-- XML 写法,proxy-mode 等价于注解的 proxyMode -->
<bean id="holder" class="com.demo.RequestHolder" scope="request" proxy-mode="targetClass"/>
三、关键坑:单例注入短生命周期
单例 OrderService ──持有──▶ RequestHolder(request 作用域)
▲ ▲
│ 容器启动时注入一次 │ 若没有代理,这个引用永远是"第一个请求"的对象
└──────────────────────────────┘
容器启动时单例初始化,此时并没有 HTTP 请求,注入只能得到"某个代理"或直接报错。proxyMode 的解法是注入一个 CGLIB/JDK 代理,代理在每次方法调用时从 RequestAttributes 中取当前请求对应的真实实例。
@Service
public class OrderService {
// 注入的是代理对象,调用时才解析到当前请求的 RequestHolder
private final RequestHolder holder;
public OrderService(RequestHolder holder) {
this.holder = holder;
}
public void handle() {
System.out.println(holder.getTraceId()); // 代理转发到当前请求实例
}
}
四、两个易错点
- prototype 的销毁不由容器负责:容器只负责实例化与初始化,
@PreDestroy不会回调,需要调用方自己释放资源(或实现DisposableBean后手工调用)。 - singleton 与 GoF 单例不是一回事:Spring 的 singleton 是"每个容器每个 Bean 名一个实例",同一个类可以在不同容器、不同 Bean 名(甚至同容器不同 id)下存在多个实例;GoF 单例是 JVM 级、类加载器级唯一。
// 第二种解法:ObjectProvider 延迟查找,不依赖作用域代理
@Service
public class ReportService {
private final ObjectProvider<RequestHolder> holderProvider;
public ReportService(ObjectProvider<RequestHolder> holderProvider) {
this.holderProvider = holderProvider;
}
public String traceId() {
// 每次调用时才向容器要实例,request 作用域下即当前请求对应的对象
return holderProvider.getIfAvailable(RequestHolder::new).getTraceId();
}
}
ObjectProvider 适合"用一次取一次"的场景;proxyMode 适合"注入后到处当普通字段用"的场景,二者不要叠加使用。
五、常见追问
- 问:
request作用域在非 Web 环境会怎样? 抛IllegalStateException: No thread-bound request found,因为拿不到RequestAttributes。 - 问:作用域代理的底层是什么?
ScopedProxyFactoryBean+SimpleBeanTargetSource,本质仍是 AOP 代理:每次调用从RequestContextHolder或会话中查找目标对象。 - 问:
@Scope与proxyMode能用在@Bean方法上吗? 可以,例如@Bean @Scope(value="request", proxyMode=TARGET_CLASS)。但@Bean方法本身在配置类中被调用时仍返回新对象,需注意代理语义。 - 问:线程作用域存在吗? Spring 提供
SimpleThreadScope,但不会默认注册,需要CustomScopeConfigurer手动加,且线程池场景几乎不可用。
小结
作用域回答"这个 Bean 有几个、活多久、谁来销毁"。面试时先报六种,再重点讲清两件事:prototype 不回调销毁、单例注入 request/session 必须 proxyMode。这两点几乎覆盖了作用域相关的全部线上事故。