人人都会AI编程

3.6 Bean 作用域底层实现:单例、多例、请求域、会话域

更新时间:2026-07-11

Bean 的作用域决定了容器如何创建和管理 Bean 实例的生命周期。Spring 提供了多种作用域,其中最常用的是 singleton(单例)、prototype(多例)、request(请求域)和 session(会话域)。表面上看,用户只需在 @Scope 注解中指定作用域名称即可,但底层实现却隐藏着一套精心设计的注册、获取和销毁机制。理解这些机制,有助于在实际开发中避免作用域误用引发的数据串扰或内存泄漏。

3.6.1 单例作用域 —— 容器级别的唯一实例

单例是 Spring 的 默认作用域。当一个 Bean 被声明为单例时,容器在启动过程中会创建该 Bean 的唯一实例,并将其缓存起来。此后所有对该 Bean 的注入请求或 getBean() 调用,都会返回这同一个实例。

底层实现的关键组件

  • DefaultSingletonBeanRegistry:这是单例 Bean 存储的核心类。它内部维护了三个层级的缓存,即常说的“三级缓存”:
  • 一级缓存(singletonObjectsConcurrentHashMap<String, Object>):存放已经完全创建好的单例 Bean。
  • 二级缓存(earlySingletonObjectsHashMap):存放提前暴露的 Bean 实例(尚未完成属性填充),用于解决循环依赖。
  • 三级缓存(singletonFactoriesHashMap):存放 ObjectFactory,用于生成 Bean 的早期引用。
  • AbstractBeanFactory#getBean() 调用链:当容器需要获取一个单例 Bean 时,会先调用 getSingleton(beanName),从一级缓存查找。如果未命中,则进入创建流程 createBean(),并在创建完成后(通过 addSingleton(beanName, singletonObject))将其注册到一级缓存。

单例 Bean 的创建时机

默认情况下,单例 Bean 在容器启动时(refresh() 阶段)就会被 提前实例化(Eager Initialization)。DefaultListableBeanFactory#preInstantiateSingletons() 方法会遍历所有非懒加载的单例 Bean 定义,依次触发实例化。这种策略能够在启动时就发现配置错误或依赖缺失,避免运行时延迟暴露。

如果希望单例 Bean 在第一次被使用时才创建,可以标注 @Lazy 注解或设置 lazy-init=true,容器会在第一次 getBean() 时进行实例化。

单例使用注意事项

单例 Bean 必须是无状态的,或者其状态对所有调用者都是共享且安全的。绝对不能在单例 Bean 中持有线程私有的可变数据(如某个请求的临时计算结果),否则会引发严重的并发数据错乱。

3.6.2 多例作用域 —— 每次获取都创建新实例

将作用域设置为 @Scope("prototype") 后,容器 不再 将实例缓存到单例池中。每次通过 getBean() 或注入时,都会执行完整的 Bean 创建流程,产生一个全新的实例。

底层实现的关键差异

  • AbstractBeanFactory#doGetBean() 中,处理多例的分支会直接跳过 getSingleton() 缓存检查,转而调用 createBean() 的专门逻辑。
  • 多例 Bean 创建后,不会调用 addSingleton() 存入一级缓存。容器只是将创建好的对象返回给调用方,此后不再持有该实例的引用。
  • 容器只负责 Bean 的定义解析和依赖注入,不管理多例 Bean 的完整生命周期。换句话说,Spring 不会对多例 Bean 执行销毁回调(除非它实现了 DisposableBean 接口并且被显式注册为“需销毁”的 Bean,但常规多例不会这样处理)。如果多例 Bean 持有需要释放的资源(如文件句柄、连接),开发人员必须手动在适当的时机关闭。

多例的典型适用场景

  • 有状态的对象,例如某个领域模型对象,每次使用都要求独立的数据副本。
  • 某种不可共享的短生命周期工具对象,比如线程不安全的格式化器(但通常可以用 ThreadLocal 替代,多例并非解决线程安全的首选方案)。

注意:当多例 Bean 被注入到单例 Bean 中时,需要注意 依赖查找 vs 依赖注入 的时机问题。如果在单例 Bean 中通过 @Autowired 注入一个多例 Bean,由于单例 Bean 只初始化一次,注入的多例实例也只会被创建一次,结果就是多例实际上退化成了单例。解决这个问题的正确方式是使用 @Lookup 注解或 ApplicationContext.getBean(),让每次调用都重新获取新实例。Spring 底层通过 CGLIB 动态代理为 @Lookup 方法生成重写字节码,在方法内部实现实时 getBean() 调用。

3.6.3 请求域与会话域 —— 与 Web 容器生命周期的绑定

请求域(request)和会话域(session)只在 Spring Web 应用 中有效。它们允许 Bean 的生命周期绑定到 HTTP 请求或 HTTP 会话上。

实现基础:Scope 接口

Spring 定义了一个 org.springframework.beans.factory.config.Scope 接口,包含两个核心方法:

  • Object get(String name, ObjectFactory<?> objectFactory):从当前作用域获取对象,若不存在则通过 ObjectFactory 创建并放入作用域。
  • Object remove(String name):从作用域移除对象,通常伴随销毁回调。

RequestScopeSessionScope 分别是该接口的实现类,它们都将实际的存储委托给 RequestAttributesHttpSession

请求域(request)底层流程

  1. RequestScopeget() 方法内部通过 RequestContextHolder.currentRequestAttributes() 获取当前请求的 ServletRequestAttributes
  2. 将 Bean 实例存储到 request.setAttribute(beanName, instance) 中(实际上是 RequestAttributes 提供的 setAttribute 封装)。
  3. 后续在同一请求中对该 Bean 的任何调用,都直接从 Request 属性中获取,保证同一请求内单例。
  4. 请求结束时,Spring 的 RequestContextListenerDispatcherServlet 会调用 ServletRequestAttributes.requestCompleted(),继而触发 RequestScoperemove(),执行 Bean 的销毁回调。

会话域(session)底层流程

  • SessionScope 将 Bean 实例存储在 HttpSession 的属性中(session.setAttribute(beanName, instance))。
  • 整个会话生命周期内,同一个 Bean 名称只会存在一个实例,不同用户拥有独立的会话空间,互不干扰。
  • 当会话过期或被显式销毁时,Servlet 容器会通知 Spring 的 HttpSessionDestructionListener,触发 SessionScope 的清理,并执行 Bean 的 @PreDestroy 方法。

作用域代理的必要性

请求域和会话域的 Bean 生命周期都比单例 Bean 短。当它们需要注入到单例 Bean(如 Controller)中时,同样面临注入时机问题——Controller 是单例,在容器初始化时创建,而请求域对象此时还不存在。Spring 通过 作用域代理 解决这一矛盾。当声明 @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) 时:

  • 容器不直接注入真实 Bean,而是注入一个 CGLIB 代理对象
  • 代理对象持有一个对 ScopedObjectFactory 的引用,每次被调用时,代理都会实时通过 RequestContextHolder 查找当前请求上下文,从中获取目标实例。
  • 这样,单例 Bean 看似持有一个固定的依赖,实际上每次方法调用都会路由到当前请求的正确实例。

这一机制完全透明,开发者感受到的就是“每个请求都有自己的 Bean 实例”。

全局域(application)和 WebSocket 域

除了上述四种,Spring 还支持 application 作用域(绑定到 ServletContext,所有请求共享)和 websocket 作用域(绑定到 WebSocket 会话)。它们的实现原理与 request/session 类似,都基于 Scope 接口和对应的存储委托对象。

3.6.4 自定义作用域

Spring 的作用域体系是可扩展的。如果业务需要特定的生命周期(如“线程作用域”或“批处理作业作用域”),可以实现 Scope 接口并注册到容器中:

public class ThreadScope implements Scope {
    private final ThreadLocal<Map<String, Object>> scope = 
            ThreadLocal.withInitial(HashMap::new);

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> map = scope.get();
        return map.computeIfAbsent(name, k -> objectFactory.getObject());
    }

    @Override
    public Object remove(String name) {
        return scope.get().remove(name);
    }

    @Override
    public String getConversationId() { return null; }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {}
}

然后通过 ConfigurableBeanFactory.registerScope("thread", new ThreadScope()) 注册,就可以在 @Scope("thread") 中使用。这种灵活性使得 Spring 在管理对象生命周期方面几乎不受限制。

3.6.5 实践建议

  • 默认使用单例,这是性能最优、模型最简单的选择。
  • 仅在确实需要独立状态时才使用多例,并注意避免将多例直接注入单例。
  • Web 作用域 Bean 务必配合代理使用,否则启动阶段就会报错。通常 Spring Boot 的 Web 应用自动启用 RequestContextListener,并通过 @SessionScope 等注解自动设置代理模式。
  • 理解作用域的生命周期边界,有助于提前规划资源释放策略,防止内存泄漏(例如请求域对象持有阻塞队列引用未清理)。

透过作用域的底层实现,我们可以看到 Spring 容器对“对象管理”这一命题的深度思考:它不仅将实例化动作从业务代码中抽出,而且为不同的生命周期场景提供了风格一致的编程模型。开发者只需声明意图,复杂的存储、回调和代理逻辑全部由框架在幕后完成。