Bean 作用域
Bean 作用域(Scope)决定 Spring 容器何时创建、创建几个、生命周期持续多久。不同作用域适用于不同场景。
作用域速查
| 作用域 | 实例数 | 生命周期 | 适用环境 |
|---|---|---|---|
singleton(默认) | 每容器 1 个 | 容器启动 → 容器关闭 | 通用 |
prototype | 每次注入/获取新建 1 个 | 使用方负责管理 | 有状态对象 |
request | 每个 HTTP 请求 1 个 | 请求开始 → 请求结束 | Web |
session | 每个 HTTP Session 1 个 | Session 创建 → Session 过期 | Web |
application | 每个 ServletContext 1 个 | 应用启动 → 应用关闭 | Web |
websocket | 每个 WebSocket 连接 1 个 | 连接建立 → 连接关闭 | WebSocket |
Singleton(单例,默认)
容器中只存在一个共享实例,所有注入点拿到的是同一个对象:
// 无需任何注解,默认即为 singleton
@Service
public class UserService {
// 整个应用生命周期内只有一个实例
}
// 显式声明(冗余,仅作说明用)
@Bean
@Scope("singleton")
public OrderService orderService() {
return new OrderService();
}适用场景:无状态服务、工具类、配置类。
注意:单例 Bean 中不应保存请求级别的状态(如当前用户信息),线程不安全的字段会引发并发问题。
Prototype(原型)
每次注入或调用 getBean() 时新建一个实例:
@Component
@Scope("prototype")
public class ReportTask {
private String reportId;
private List<String> results = new ArrayList<>();
public void setReportId(String id) { this.reportId = id; }
public void addResult(String result) { results.add(result); }
public List<String> getResults() { return results; }
}@Service
@RequiredArgsConstructor
public class ReportService {
private final ApplicationContext context;
public void generateReport(String reportId) {
// 每次获取全新实例,避免状态污染
ReportTask task = context.getBean(ReportTask.class);
task.setReportId(reportId);
task.addResult("数据加载完成");
// ...
}
}Spring 不管理 Prototype Bean 的销毁:
@PreDestroy和DisposableBean.destroy()不会被调用。若持有资源(连接、线程),需由使用方手动释放。
单例 Bean 注入 Prototype Bean 的陷阱
单例 Bean 只初始化一次,其中注入的 Prototype Bean 也只创建一次,后续不再新建,失去 Prototype 意义:
// ❌ 错误:每次调用 getTask() 返回的是同一个 prototype 实例
@Service
public class BadService {
@Autowired
private ReportTask task; // 只在 BadService 初始化时注入一次
public ReportTask getTask() { return task; }
}解决方案一:注入 ApplicationContext,每次手动 getBean(见上例)
解决方案二:@Lookup 方法注入(Spring 通过 CGLIB 覆盖该方法)
@Service
public abstract class ReportService {
// Spring 自动覆盖此方法,每次调用返回新的 Prototype 实例
@Lookup
public abstract ReportTask createTask();
public void run(String reportId) {
ReportTask task = createTask(); // 每次调用都是新实例
task.setReportId(reportId);
}
}解决方案三:ObjectProvider(推荐,类型安全)
@Service
@RequiredArgsConstructor
public class ReportService {
private final ObjectProvider<ReportTask> taskProvider;
public void run(String reportId) {
ReportTask task = taskProvider.getObject(); // 每次获取新实例
task.setReportId(reportId);
}
}Web 作用域
Web 作用域仅在 Web 应用(Servlet 容器)中可用,需要 spring-webmvc 或 spring-webflux。
Request(请求作用域)
每个 HTTP 请求独立一个实例,请求结束后销毁:
@Component
@RequestScope // 等价于 @Scope("request")
// 或 @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String traceId;
private String userId;
private long startTime = System.currentTimeMillis();
// getter / setter
}@RestController
@RequiredArgsConstructor
public class OrderController {
private final RequestContext requestContext; // 注入的是代理对象
@PostMapping("/orders")
public Order createOrder(@RequestBody CreateOrderRequest req) {
requestContext.setUserId(SecurityContextHolder.getContext()
.getAuthentication().getName());
// 在同一请求的任何地方都能获取到相同的 RequestContext 实例
return orderService.create(req, requestContext);
}
}Session(会话作用域)
每个 HTTP Session 独立一个实例,Session 过期后销毁:
@Component
@SessionScope // 等价于 @Scope("session")
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
public void addItem(CartItem item) { items.add(item); }
public void removeItem(Long productId) {
items.removeIf(i -> i.getProductId().equals(productId));
}
public List<CartItem> getItems() { return Collections.unmodifiableList(items); }
public BigDecimal getTotal() {
return items.stream()
.map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}Application(应用作用域)
整个 ServletContext 共享一个实例(与 Singleton 类似,但绑定的是 ServletContext 而非 Spring 容器):
@Component
@ApplicationScope
public class GlobalConfig {
private volatile String maintenanceMessage = "";
// ...
}作用域代理(Scoped Proxy)
核心问题:单例 Bean 依赖短生命周期 Bean(如 Request/Session/Prototype)时,注入时机不匹配。
单例 Bean 只初始化一次,而 Request Bean 每个请求都是新实例。直接注入会拿到一个过期的固定实例。
解决方案:作用域代理。Spring 注入的是一个 CGLIB 代理,每次方法调用时代理自动路由到当前作用域的真实实例:
// 显式指定代理模式
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String userId;
// ...
}
// @RequestScope / @SessionScope 已内置了 ScopedProxyMode.TARGET_CLASS
// 直接使用这两个注解无需额外配置// 单例 Bean 安全注入 Request 作用域 Bean
@Service
@RequiredArgsConstructor
public class AuditService {
// requestContext 是代理对象,每次方法调用自动委托给当前请求的真实实例
private final RequestContext requestContext;
public void audit(String action) {
// 正确:每次调用都拿到当前请求的 userId
log.info("用户 {} 执行了: {}", requestContext.getUserId(), action);
}
}| 代理模式 | 说明 | 适用场景 |
|---|---|---|
ScopedProxyMode.NO | 不使用代理(默认) | 仅在同作用域内注入 |
ScopedProxyMode.INTERFACES | JDK 动态代理(目标需实现接口) | 有接口的 Bean |
ScopedProxyMode.TARGET_CLASS | CGLIB 代理 | 无接口的普通类(推荐) |
自定义作用域
实现 Scope 接口并注册,可创建任意生命周期的作用域:
// 示例:线程作用域(每个线程一个实例)
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadScope =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadScope.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return threadScope.get().remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 线程结束时调用(可通过 ThreadLocal 的 remove 扩展)
}
@Override
public Object resolveContextualObject(String key) { return null; }
@Override
public String getConversationId() {
return String.valueOf(Thread.currentThread().getId());
}
}
// 注册自定义作用域
@Configuration
public class ScopeConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new ThreadScope());
return configurer;
}
}
// 使用自定义作用域
@Component
@Scope("thread")
public class ThreadLocalContext {
private String operatorId;
// ...
}Spring 内置 SimpleThreadScope,可以直接注册使用:
@Bean
public static CustomScopeConfigurer threadScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("thread", new SimpleThreadScope());
return configurer;
}作用域选型指南
Bean 有状态?
├── 否 → Singleton(默认,无需配置)
└── 是 → 状态绑定到哪里?
├── 方法/任务调用 → Prototype(配合 @Lookup 或 ObjectProvider)
├── HTTP 请求 → @RequestScope
├── 用户会话 → @SessionScope
├── 当前线程 → thread scope(SimpleThreadScope)
└── 全局共享状态 → Singleton(加锁保护并发写入)
与生命周期的关系
| 作用域 | 初始化时机 | 销毁时机 | @PreDestroy 是否生效 |
|---|---|---|---|
| Singleton | 容器启动时(默认)/ 首次使用(@Lazy) | 容器关闭 | ✅ |
| Prototype | 每次请求时 | 使用方负责 | ❌ |
| Request | 每个 HTTP 请求 | 请求结束 | ✅ |
| Session | Session 创建 | Session 过期 | ✅ |
| Application | 容器启动 | 容器关闭 | ✅ |
Singleton 的延迟初始化(@Lazy)详见 延迟加载。Bean 销毁回调的完整机制详见 Bean生命周期。