@Scope 详解
定义与作用
@Scope 是 Spring 提供的**作用域声明(Scope Declaration)**注解,用于控制 Bean 实例的创建数量和生命周期可见范围。默认情况下,Spring 容器中的所有 Bean 都是 singleton 作用域——每个容器只创建一个实例,全局共享。但在某些场景下,这种默认行为并不合适:有状态的对象需要每个线程独立一份,Web 应用需要请求级隔离的数据封装,@Scope 让开发者能够精确控制这些行为。
在飞翔科技的学生管理系统中,小崔遇到了一个典型问题:StudentReportBuilder 在生成报表时维护了当前报表的标题、页码等状态。由于默认 singleton 作用域,多个用户同时请求报表时,状态互相覆盖,导致页码错乱、标题串扰。小崔通过在 StudentReportBuilder 上标注 @Scope("prototype"),让每个请求都获得全新的实例,彻底解决了并发状态污染问题。
适用位置与常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
value / scopeName | String | 作用域名称,如 singleton、prototype、request、session、application、websocket |
proxyMode | ScopedProxyMode | 作用域代理模式,用于解决单例 Bean 注入短作用域 Bean 的问题 |
适用位置:类级别(与 @Component 等一起使用)、方法级别(与 @Bean 一起使用)。
核心原理
Spring 容器在注册 BeanDefinition 时,会记录每个 Bean 的 scope 属性。当容器收到 getBean() 请求时,根据作用域决定返回策略:
- singleton:从
singletonObjects缓存池中直接返回已存在的实例,不存在则创建并缓存 - prototype:每次都调用
createBean()创建新实例,不缓存,不管理完整生命周期(不调用销毁回调) - request / session / application / websocket:依赖 Web 环境的
RequestContextListener或RequestContextFilter,将实例绑定到对应的 HTTP 生命周期域
六种作用域详解
| 作用域 | 说明 | 适用场景 | 生命周期管理 |
|---|---|---|---|
| singleton | 默认,每个 Spring 容器一个实例 | 无状态服务、配置类、工具类 | 容器启动时创建(或首次获取时),容器关闭时销毁 |
| prototype | 每次请求都创建新实例 | 有状态对象、多线程独立上下文、不可变构建器 | 容器仅负责创建,销毁由 JVM GC 处理 |
| request | 每个 HTTP 请求一个实例 | 请求级数据封装、表单处理对象 | 请求开始时创建,请求结束时销毁 |
| session | 每个 HTTP Session 一个实例 | 用户登录状态、购物车 | Session 创建时初始化,Session 过期或销毁时清理 |
| application | 每个 ServletContext 一个实例 | 应用级全局配置、缓存管理器 | 应用启动时创建,应用关闭时销毁 |
| websocket | 每个 WebSocket 会话一个实例 | 长连接会话状态、实时消息处理器 | WebSocket 连接建立时创建,断开时销毁 |
注意:
request、session、application、websocket四个作用域需要在 Web-awareApplicationContext(如AnnotationConfigWebApplicationContext)中使用,且需要配置RequestContextListener或RequestContextFilter来绑定线程上下文。
完整示例
场景简述
飞翔科技的学生管理系统中,StudentReportBuilder 用于生成 PDF 学生成绩单。该对象在构建过程中维护了 reportTitle、currentPage、watermarkText 等状态。由于默认 singleton,并发请求时状态互相覆盖,导致 A 同学的成绩单上出现了 B 同学的姓名水印。小崔决定用 @Scope("prototype") 解决。
操作前:默认 singleton 导致的状态污染
@Component
public class StudentReportBuilder {
private String reportTitle;
private int currentPage;
private String watermarkText;
public void setReportTitle(String title) {
this.reportTitle = title;
}
public void setWatermarkText(String text) {
this.watermarkText = text;
}
public byte[] build(Long studentId) {
// 使用 reportTitle、currentPage、watermarkText 生成报表
currentPage++;
return new byte[0]; // 模拟 PDF 数据
}
}
@Service
public class ReportService {
@Autowired
private StudentReportBuilder reportBuilder; // singleton,全局共享
public byte[] generateReport(Long studentId, String studentName) {
reportBuilder.setReportTitle(studentName + " 的成绩单");
reportBuilder.setWatermarkText("飞翔科技版权所有");
return reportBuilder.build(studentId);
}
}
并发问题:
- 线程 A 调用
generateReport(1L, "小崔"),设置了reportTitle = "小崔 的成绩单" - 线程 B 调用
generateReport(2L, "黄俪"),覆盖了reportTitle = "黄俪 的成绩单" - 线程 A 继续执行
build(),发现标题变成了 "黄俪 的成绩单",数据错乱
使用该注解的完整代码
package com.feixiang.student.report;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class StudentReportBuilder {
private String reportTitle;
private int currentPage;
private String watermarkText;
public StudentReportBuilder setReportTitle(String title) {
this.reportTitle = title;
return this;
}
public StudentReportBuilder setWatermarkText(String text) {
this.watermarkText = text;
return this;
}
public byte[] build(Long studentId) {
// 模拟 PDF 生成过程
currentPage = 1;
System.out.println("生成报表:" + reportTitle);
System.out.println("页码:" + currentPage);
System.out.println("水印:" + watermarkText);
System.out.println("实例哈希:" + System.identityHashCode(this));
return new byte[1024];
}
}
package com.feixiang.student.service;
import com.feixiang.student.report.StudentReportBuilder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
@Service
public class ReportService {
private final ObjectProvider<StudentReportBuilder> builderProvider;
// 通过 ObjectProvider 每次获取新的 prototype 实例
public ReportService(ObjectProvider<StudentReportBuilder> builderProvider) {
this.builderProvider = builderProvider;
}
public byte[] generateReport(Long studentId, String studentName) {
StudentReportBuilder builder = builderProvider.getObject();
return builder
.setReportTitle(studentName + " 的成绩单")
.setWatermarkText("飞翔科技版权所有")
.build(studentId);
}
}
package com.feixiang.student;
import com.feixiang.student.config.AppConfig;
import com.feixiang.student.service.ReportService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class StudentApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
ReportService reportService = ctx.getBean(ReportService.class);
// 模拟两次独立的报表生成
reportService.generateReport(1L, "小崔");
reportService.generateReport(2L, "黄俪");
ctx.close();
}
}
操作后运行结果及分析
生成报表:小崔 的成绩单
页码:1
水印:飞翔科技版权所有
实例哈希:12345678
生成报表:黄俪 的成绩单
页码:1
水印:飞翔科技版权所有
实例哈希:87654321
变化分析:
- 两次报表生成的
StudentReportBuilder实例哈希不同,证明确实是两个独立对象 - 每个实例的
currentPage都从 1 开始,不存在状态串扰 ObjectProvider.getObject()是 Spring 5.x 推荐的获取 prototype Bean 的方式,比直接@Autowired注入 prototype 更可靠(见下方易错场景)
Web 作用域示例
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId;
private long startTime;
public void init(String requestId) {
this.requestId = requestId;
this.startTime = System.currentTimeMillis();
}
public String getRequestId() {
return requestId;
}
public long getElapsedTime() {
return System.currentTimeMillis() - startTime;
}
}
@Service
public class StudentService {
@Autowired
private RequestContext requestContext; // singleton 注入 request 作用域 Bean
public Student getStudent(Long id) {
System.out.println("当前请求ID:" + requestContext.getRequestId());
// ... 查询逻辑
return new Student(id, "小崔", 20, "计算机科学");
}
}
proxyMode 的必要性:StudentService 是 singleton,它在容器启动时就需要注入依赖。但 RequestContext 是 request 作用域,在启动时还没有 HTTP 请求。ScopedProxyMode.TARGET_CLASS 会为 RequestContext 生成 CGLIB 代理,实际的目标实例在每次 HTTP 请求时才创建。StudentService 持有的是代理对象,每次调用代理方法时,代理会从当前线程绑定的 Request 域中获取真实实例。
易错场景与面试考点
易错场景一:singleton Bean 直接注入 prototype Bean
@Component
@Scope("prototype")
public class StudentReportBuilder { ... }
@Service
public class ReportService {
@Autowired
private StudentReportBuilder reportBuilder; // 错误!
}
后果:ReportService 是 singleton,在容器启动时注入 StudentReportBuilder。Spring 只会在启动时创建一次 prototype 实例并注入,之后 ReportService 始终持有同一个 StudentReportBuilder,prototype 作用域完全失效。
解决方案:
- 使用
ObjectProvider<T>延迟获取(推荐,Spring 5.x+) - 使用
javax.inject.Provider<T>(JSR-330 标准) - 在
@Scope上设置proxyMode = ScopedProxyMode.TARGET_CLASS,让 Spring 生成代理
// 正确做法一:ObjectProvider
@Service
public class ReportService {
private final ObjectProvider<StudentReportBuilder> builderProvider;
public ReportService(ObjectProvider<StudentReportBuilder> builderProvider) {
this.builderProvider = builderProvider;
}
public byte[] generateReport(...) {
StudentReportBuilder builder = builderProvider.getObject(); // 每次获取新实例
// ...
}
}
// 正确做法二:Scoped Proxy
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StudentReportBuilder { ... }
易错场景二:prototype Bean 的销毁回调不执行
@Component
@Scope("prototype")
public class TemporaryResource implements DisposableBean {
private File tempFile;
public TemporaryResource() {
this.tempFile = new File("/tmp/student_" + System.currentTimeMillis() + ".tmp");
}
@Override
public void destroy() {
tempFile.delete(); // 清理临时文件
}
}
后果:destroy() 永远不会被调用。Spring 容器对 prototype Bean 只负责创建,创建后就不再持有引用,因此无法触发销毁回调。临时文件会残留在磁盘上,造成资源泄漏。
正确做法:在业务代码中手动管理 prototype Bean 的生命周期,或改用 ObjectProvider 并在使用完毕后显式清理:
public void processData() {
TemporaryResource resource = provider.getObject();
try {
// 使用资源
} finally {
resource.destroy(); // 手动调用清理
}
}
面试考点
Q:singleton 是否线程安全?
singleton 本身不保证线程安全。线程安全取决于 Bean 内部状态是否可变。如果 Bean 是无状态的(如 Service 层只调用 DAO,不维护字段状态),则天然线程安全;如果 Bean 是有状态的(如维护了
currentUser、counter等字段),多线程并发访问时需要同步机制,或改用 prototype/request 作用域。
Q:prototype 作用域的 Bean 何时销毁?
Spring 容器不管理 prototype Bean 的销毁。容器创建实例后就不再持有引用,实例的销毁由 JVM 垃圾回收器决定。如果 prototype Bean 持有需要显式释放的资源(如数据库连接、临时文件),必须在业务代码中手动处理,或实现自定义的
BeanPostProcessor来跟踪 prototype 实例。
Q:@Scope 的 proxyMode 有哪些选项?
NO(默认,不生成代理)、INTERFACES(JDK 动态代理,要求目标类实现接口)、TARGET_CLASS(CGLIB 代理,适用于类)。当 singleton Bean 需要注入短作用域(request/session/prototype)Bean 时,必须使用代理模式,否则短作用域会在容器启动时就被"固化"为单例。
Q:Spring Boot 中 request 作用域 Bean 在单元测试中如何获取?
在非 Web 环境的单元测试中,没有真实的 HTTP 请求上下文。可以使用
RequestContextListener或MockHttpServletRequest手动绑定请求上下文,或使用@WebAppConfiguration标注测试类加载 WebApplicationContext。Spring Boot Test 还提供了@AutoConfigureMockMvc来模拟 Web 环境。
Q:application 作用域和 singleton 有什么区别?
在单个 Spring 应用上下文中,两者行为相同(都是一个实例)。区别在于:如果应用中存在多个
ApplicationContext(如 Spring MVC 的 Root Context 和 Servlet Context),singleton是每个 Context 一个实例,而application是整个 ServletContext 一个实例,跨 Context 共享。在典型的 Spring Boot 应用中,两者没有实际差异。