一、作用域的本质与价值
在Spring框架中,Bean作用域定义了Bean实例的生命周期边界 和可见性范围。它决定了:
- 何时创建Bean实例
- 谁能访问这个实例
- 何时销毁这个实例
- 实例在应用中的共享程度
选择正确的作用域,是构建高效、安全Spring应用的基础。
二、六大作用域全景概览
| 作用域 | 注解声明 | 生命周期 | 适用场景 | 关键特性 |
|---|---|---|---|---|
| singleton | @Scope("singleton") 或默认 |
容器启动到关闭 | 无状态服务、工具类 | 默认作用域,线程安全需自行保证 |
| prototype | @Scope("prototype") |
每次获取时创建,Spring不销毁 | 有状态对象、昂贵资源 | 每次getBean()都返回新实例 |
| request | @Scope(value="request", proxyMode=...) |
HTTP请求开始到结束 | 请求级数据、表单对象 | 仅Web环境,自动绑定请求周期 |
| session | @Scope(value="session", proxyMode=...) |
HTTP会话期间 | 用户会话数据、购物车 | 用户隔离,会话超时自动销毁 |
| application | @Scope("application") |
ServletContext生命周期 | 应用级全局缓存 | 类似单例,但作用域为ServletContext |
| websocket | @Scope("websocket") |
WebSocket连接期间 | WebSocket会话状态 | 实时通信场景专用 |
三、各作用域深度解析
3.1 singleton(单例作用域)- 默认选择
生命周期特性:
- 唯一实例:每个Spring IoC容器中只有一个该Bean的实例
- 启动创建 :默认在容器启动时创建(可通过
@Lazy延迟) - 全局共享:所有依赖注入请求都返回同一实例
- 容器管理销毁:容器关闭时调用销毁方法
java
@Component // 等价于 @Scope("singleton")
public class PaymentService {
// 必须设计为无状态!
public PaymentResult process(PaymentRequest request) {
// 使用局部变量而非成员变量保证线程安全
String transactionId = generateId();
return processPayment(transactionId, request);
}
// ❌ 危险:多线程并发访问会导致数据混乱
// private String currentTransactionId;
}
最佳实践:
- 确保单例Bean是无状态的(不保存客户端特定状态)
- 对于线程共享资源(如缓存),使用线程安全数据结构
- 需要全局配置或服务时首选单例
3.2 prototype(原型作用域)- 需要谨慎使用
核心行为 :每次获取都是全新实例
java
@Component
@Scope("prototype")
public class ReportGenerator {
private final String reportId;
private List<Data> reportData = new ArrayList<>();
public ReportGenerator() {
this.reportId = "REPORT_" + UUID.randomUUID().toString();
System.out.println("创建报告实例: " + reportId);
}
public void addData(Data data) {
reportData.add(data); // 每个实例维护独立状态
}
// ⚠️ @PreDestroy 方法可能不会被调用!
@PreDestroy
public void cleanup() {
System.out.println("清理报告: " + reportId);
}
}
生命周期关键点:
- 创建时机 :调用
getBean()或依赖注入发生时 - 无销毁管理:Spring创建实例后即移交控制权
- 内存管理责任:由使用者负责资源释放
正确获取方式对比:
java
// ❌ 错误方式:直接字段注入
@Autowired
private ReportGenerator generator; // 启动时固定一个实例
// ✅ 正确方式1:通过ApplicationContext
@Autowired
private ApplicationContext context;
public void generateReport() {
ReportGenerator generator = context.getBean(ReportGenerator.class);
// 使用后需注意资源清理
}
// ✅ 正确方式2:通过ObjectProvider(推荐)
@Autowired
private ObjectProvider<ReportGenerator> generatorProvider;
public void generateReport() {
ReportGenerator generator = generatorProvider.getObject();
// 使用后需注意资源清理
}
// ✅ 正确方式3:使用方法注入
public abstract class ReportService {
@Lookup
protected abstract ReportGenerator createGenerator();
}
适用场景:
- 每个请求需要独立状态的计算器
- 需要手动管理生命周期的昂贵资源
- 对象需要频繁创建和丢弃的临时处理器
3.3 request(请求作用域)- Web专用
生命周期绑定 :HTTP请求(从接收到响应)
java
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestTracker {
private final String requestId;
private final LocalDateTime startTime;
private Map<String, String> requestAttributes = new HashMap<>();
public RequestTracker() {
this.requestId = UUID.randomUUID().toString();
this.startTime = LocalDateTime.now();
System.out.println("创建请求追踪器: " + requestId);
}
@PreDestroy
public void cleanup() {
long duration = Duration.between(startTime, LocalDateTime.now()).toMillis();
System.out.println("请求 " + requestId + " 完成,耗时: " + duration + "ms");
}
}
配置要求:
- 必须设置代理 :
proxyMode = ScopedProxyMode.TARGET_CLASS - 仅限Web环境:需要Spring Web支持
- 自动生命周期管理:请求结束自动销毁
工作原理:
HTTP请求到达 → Spring创建Request实例 → 注入到单例Controller
↓
请求处理中 → Controller通过代理访问真实Request实例
↓
请求完成 → Spring销毁Request实例 → 触发@PreDestroy
3.4 session(会话作用域)- 用户级隔离
生命周期绑定 :用户会话(从登录到退出/超时)
java
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserShoppingCart {
private List<CartItem> items = new ArrayList<>();
private User user;
public void addItem(Product product, int quantity) {
items.add(new CartItem(product, quantity));
}
public BigDecimal getTotal() {
return items.stream()
.map(item -> item.getProduct().getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
@PreDestroy
public void saveCart() {
// 会话结束时保存购物车到数据库
System.out.println("保存用户 " + user.getUsername() + " 的购物车");
}
}
多用户隔离示例:
java
@RestController
public class CartController {
@Autowired
private UserShoppingCart cart; // 代理对象
@GetMapping("/cart")
public CartView getCart() {
// 用户A访问:返回用户A的购物车
// 用户B访问:返回用户B的购物车
return new CartView(cart.getItems(), cart.getTotal());
}
}
3.5 application作用域 - ServletContext级单例
与singleton的区别:
- singleton:每个Spring容器一个实例
- application:每个ServletContext一个实例(跨多个Spring容器)
java
@Component
@Scope(value = "application")
public class GlobalAppConfig {
private final Properties appProperties = new Properties();
private final AtomicInteger totalRequests = new AtomicInteger(0);
public void incrementRequestCount() {
totalRequests.incrementAndGet();
}
// 在整个Web应用中共享
}
3.6 websocket作用域 - 实时通信专用
java
@Component
@Scope("websocket")
public class WebSocketSessionState {
private String sessionId;
private LocalDateTime connectedAt;
private Queue<Message> pendingMessages = new ConcurrentLinkedQueue<>();
@PreDestroy
public void cleanup() {
// WebSocket连接关闭时清理
}
}
四、核心对比:prototype vs request
虽然两者都产生多个实例,但本质完全不同:
| 维度 | prototype | request |
|---|---|---|
| 触发条件 | 显式调用getBean() |
HTTP请求到达 |
| 生命周期管理 | 不管理销毁,可能泄漏 | 请求结束自动销毁 |
| 适用环境 | 任何Spring应用 | 仅Web应用 |
| 与单例Bean协作 | 需特殊处理(Provider/Context) | 通过代理自动处理 |
| 典型获取方式 | context.getBean() |
直接@Autowired(代理) |
| 设计目的 | 对象复用控制 | HTTP请求状态管理 |
五、作用域选择决策指南
5.1 决策流程
是否需要对象隔离?
├─ 是 → 是否绑定Web请求?
│ ├─ 是 → 用 request
│ └─ 否 → 是否绑定用户会话?
│ ├─ 是 → 用 session
│ └─ 否 → 用 prototype(需谨慎)
│
└─ 否 → 是否需要在多个Spring容器间共享?
├─ 是 → 用 application
└─ 否 → 用 singleton(默认)
5.2 场景化选择矩阵
| 应用场景 | 推荐作用域 | 理由 |
|---|---|---|
| 用户登录信息 | session | 用户会话期间需要保持状态 |
| API请求追踪 | request | 请求级别数据,请求结束即可丢弃 |
| 数据库连接池 | singleton | 无状态,需要全局共享 |
| PDF报告生成器 | prototype | 每个报告独立,生成后即丢弃 |
| 全局配置参数 | application | 整个Web应用共享 |
| 聊天消息状态 | websocket | 绑定到WebSocket连接生命周期 |
六、常见陷阱与解决方案
陷阱1:单例Bean中直接注入prototype
java
// ❌ 问题代码
@Service
public class OrderService {
@Autowired
private PaymentProcessor processor; // prototype
public void processOrder() {
// 永远使用同一个processor实例
}
}
// ✅ 解决方案:使用Provider
@Service
public class OrderService {
@Autowired
private ObjectProvider<PaymentProcessor> processorProvider;
public void processOrder() {
PaymentProcessor processor = processorProvider.getObject();
// 每次都是新实例
}
}
陷阱2:忘记request/session作用域的代理配置
java
// ❌ 缺少proxyMode
@Scope("request")
// ✅ 正确配置
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
陷阱3:prototype作用域的资源泄漏
java
@Component
@Scope("prototype")
public class FileHandler {
private FileInputStream stream;
public void processFile(String path) throws IOException {
stream = new FileInputStream(path);
// 处理文件...
}
// ❌ 没有close方法,文件句柄泄漏!
}
// ✅ 正确做法:实现DisposableBean或提供清理方法
@Component
@Scope("prototype")
public class FileHandler implements DisposableBean {
private FileInputStream stream;
@Override
public void destroy() throws IOException {
if (stream != null) {
stream.close();
}
}
}
八、性能考虑
- singleton:内存效率最高,启动时间可能受影响
- prototype:创建开销大,适合轻量对象
- request/session:HTTP容器有额外开销,需合理使用
黄金法则:默认使用singleton,只在确有必要时使用其他作用域。