Spring 容器管理的每个 Bean 都有一个作用域(Scope),它决定了 Bean 实例在容器中的生命周期和可见范围。正确理解并选择作用域,是构建高效、安全应用的必备技能。很多诡异的 Bug(例如成员变量“串数据”、状态莫名其妙被共享)最终都追溯到作用域选型的偏差。
8.5.1 五大核心作用域一览
Spring 提供了五种常见作用域,其中两种通用(singleton、prototype),三种专用于 Web 环境(request、session、application)。
| 作用域 | 描述 |
|--------|------|
| singleton | 整个 IoC 容器中仅存在一个 Bean 实例,所有请求共享同一实例。 |
| prototype | 每次注入或通过容器获取时,都重新创建一个新的实例。 |
| request | 每个 HTTP 请求拥有独立的实例,请求结束即销毁。 |
| session | 每个 HTTP 会话拥有独立的实例,会话结束即销毁。 |
| application | 整个 ServletContext 级别单例,与 Web 应用生命周期绑定。 |
实际开发中还有 websocket 作用域(为 WebSocket 会话级),但不属于常规高频场景。
声明方式:与 @Component、@Bean 配合使用 @Scope 注解。例如:
@Component
@Scope("prototype")
public class ShoppingCart { ... }
// 或使用常量字符串更安全
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart { ... }
// 在 @Bean 方法上声明
@Bean
@Scope("session")
public UserPreferences userPreferences() {
return new UserPreferences();
}
8.5.2 singleton —— 默认主角,覆盖 90% 场景
特点:容器启动时创建单例 Bean 的实例(默认非延迟),随后在整个容器生命周期内复用同一个实例。任何地方注入该 Bean,得到的都是同一个对象。
适用场景:
- 无状态的服务类:典型的 Service、Controller、Repository,它们不持有请求相关的私有成员变量,只使用方法参数和局部变量执行业务逻辑。
- 基础设施组件:数据源(DataSource)、连接池、线程池、缓存管理器、Jackson ObjectMapper、事务管理器等,创建成本高且必须全局唯一。
- 配置映射对象:绑定
application.properties的@ConfigurationProperties类。
实践要点:
- 必须保证线程安全。因为单例是共享的,如果有可变成员变量,就会出现并发问题。如果确实需要状态,改用
prototype或将状态移到外部存储(如 Redis、数据库)。 - 延迟初始化:默认单例 Bean 在容器刷新时提前创建(除非标记
@Lazy),有助于启动时发现配置错误。如果该 Bean 启动很慢又不常用,加上@Lazy延迟到首次调用时才创建。
@Service
public class ProductService {
// 无状态,只使用方法参数和 DAO
private final ProductDao dao; // 同样是单例
public ProductService(ProductDao dao) {
this.dao = dao;
}
public Product findById(Long id) {
return dao.findById(id);
}
}
8.5.3 prototype —— 给需要“新”对象的场景
特点:每次请求(注入或 getBean())都生成全新实例。容器只负责创建,不负责销毁(不调用 @PreDestroy,需调用方自行释放)。
适用场景:
- 有状态的 POJO:比如购物车对象、订单构造器、动态表单数据持有者,这些对象必须隔离,不能共享成员变量。
- 短生命周期的工具类:每次使用都需要不同的配置状态,如某个复杂查询的查询对象构建器。
- 需要多次使用的线程不安全组件:比如 DateFormat(众所周知非线程安全)。但应优先用 ThreadLocal 或改用线程安全实现,而不是大量创建 prototype 实例。
典型代码:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart {
private List<CartItem> items = new ArrayList<>();
public void addItem(CartItem item) {
items.add(item);
}
// ...
}
// 使用:每次注入获得全新 cart
@Service
public class OrderService {
@Autowired
private ObjectProvider<ShoppingCart> cartProvider;
public void process() {
ShoppingCart cart = cartProvider.getObject(); // 每次调用得到新实例
// ...
}
}
为何不能直接用 new 而是依赖容器? 因为 prototype Bean 可以享受依赖注入、AOP 增强(如事务代理)等功能。直接 new 则脱离容器管理。
关键陷阱:
- 在单例 Bean 中直接注入 prototype Bean,prototype 将只被创建一次(因为单例初始化时注入的那个实例会被固定)。解决方法:使用
ObjectProvider或@Lookup方法每次动态获取新实例。 - prototype Bean 的销毁不受容器管理,若持有资源(如文件流),需自行释放。
8.5.4 request / session / application —— Web 环境专属
这三个作用域只在 Web 应用上下文中生效(需引入 spring-web 模块,Spring Boot 会默认启用)。
request 作用域
- 特点:每个 HTTP 请求创建新实例,请求结束实例销毁。
- 适用场景:
- 请求级上下文信息:如记录当前请求的用户 IP、请求 ID、鉴权后的用户详情等,在整个请求处理链中共享。
- 避免参数层层传递:可以在拦截器或过滤器中设置 request 作用域 Bean 的属性,后续 Controller 和 Service 直接读取。
- 注意:异步请求可能传播失效(需显式配置
RequestContextFilter并选择合适模式)。
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedContext {
private String userId;
private String traceId;
// getters & setters
}
session 作用域
- 特点:每个 HTTP 会话创建独立实例,会话失效时销毁。
- 适用场景:
- 用户个人数据:如购物车、用户偏好设置、登录令牌、多步骤表单中间数据。
- 跨请求保持状态:比使用 HttpSession 更类型安全,通过注入即可读写。
- 注意事项:
- 若系统采用无状态 JWT 认证,session 作用域变相成为“按 token 解析出的用户标识来缓存”的定制作用域,需人工处理失效。
- 注意内存占用,若 Bean 体积较大且活跃用户多,可配合 Redis 存储 Spring Session 解决水平扩展问题。
application 作用域
- 特点:与 ServletContext 绑定,整个 Web 应用仅此一个实例,可视为 Web 环境下的单例增强版。
- 适用场景:
- 应用级全局缓存或注册表:如全局配置、数据字典缓存、在线用户列表(需并发控制)。
- 与 singleton 的区别:singleton 是 IoC 容器内的单例,application 是 Servlet 容器范围。在单一 Web 应用下几乎等价,但 application 能以更“语义化”的方式表达它是 Web 全局的。
避免 Scoped Bean 注入到短生命周期对象中的问题:当把 request/session 作用域 Bean 注入到 singleton 作用域 Bean 时,需使用代理模式(设置 proxyMode = ScopedProxyMode.TARGET_CLASS),让单例 Bean 持有代理,运行时动态路由到当前请求的 Bean。
8.5.5 自定义作用域与选型决策树
自定义作用域:偶尔需要对作用域做定制(如基于租户、基于线程),可实现 org.springframework.beans.factory.config.Scope 接口并注册。但绝大多数场景用现有作用域结合外部存储即可。
选型决策规则:
- 无状态可共享 → singleton(默认首选)。
- 每次使用都需独立状态 → prototype。
- 状态仅在 HTTP 请求内共享 → request。
- 状态需跨请求但仅限同一用户会话 → session。
- 整个 Web 应用仅需一份状态 → application。
- 需要注入到单例 Bean 的短生命周期 Scoped Bean → 设置
proxyMode为 TARGET_CLASS(或 INTERFACES)。
图省事将所有组件都设为 singleton 是危险的,尤其是当类中包含可变状态时;过度使用 prototype 又会带来不必要的性能开销。根据状态的边界选择作用域,是一个成熟 Spring 开发者的基本素养。