人人都会AI编程

8.5 Bean 作用域选型与使用场景

更新时间:2026-07-11

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 接口并注册。但绝大多数场景用现有作用域结合外部存储即可。

选型决策规则

  1. 无状态可共享 → singleton(默认首选)。
  2. 每次使用都需独立状态 → prototype。
  3. 状态仅在 HTTP 请求内共享 → request。
  4. 状态需跨请求但仅限同一用户会话 → session。
  5. 整个 Web 应用仅需一份状态 → application。
  6. 需要注入到单例 Bean 的短生命周期 Scoped Bean → 设置 proxyMode 为 TARGET_CLASS(或 INTERFACES)。

图省事将所有组件都设为 singleton 是危险的,尤其是当类中包含可变状态时;过度使用 prototype 又会带来不必要的性能开销。根据状态的边界选择作用域,是一个成熟 Spring 开发者的基本素养。