Bean 的作用域决定了容器如何创建和管理 Bean 实例的生命周期。Spring 提供了多种作用域,其中最常用的是 singleton(单例)、prototype(多例)、request(请求域)和 session(会话域)。表面上看,用户只需在 @Scope 注解中指定作用域名称即可,但底层实现却隐藏着一套精心设计的注册、获取和销毁机制。理解这些机制,有助于在实际开发中避免作用域误用引发的数据串扰或内存泄漏。
3.6.1 单例作用域 —— 容器级别的唯一实例
单例是 Spring 的 默认作用域。当一个 Bean 被声明为单例时,容器在启动过程中会创建该 Bean 的唯一实例,并将其缓存起来。此后所有对该 Bean 的注入请求或 getBean() 调用,都会返回这同一个实例。
底层实现的关键组件:
DefaultSingletonBeanRegistry:这是单例 Bean 存储的核心类。它内部维护了三个层级的缓存,即常说的“三级缓存”:- 一级缓存(
singletonObjects,ConcurrentHashMap<String, Object>):存放已经完全创建好的单例 Bean。 - 二级缓存(
earlySingletonObjects,HashMap):存放提前暴露的 Bean 实例(尚未完成属性填充),用于解决循环依赖。 - 三级缓存(
singletonFactories,HashMap):存放 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):从作用域移除对象,通常伴随销毁回调。
RequestScope 和 SessionScope 分别是该接口的实现类,它们都将实际的存储委托给 RequestAttributes 或 HttpSession。
请求域(request)底层流程:
RequestScope的get()方法内部通过RequestContextHolder.currentRequestAttributes()获取当前请求的ServletRequestAttributes。- 将 Bean 实例存储到
request.setAttribute(beanName, instance)中(实际上是RequestAttributes提供的setAttribute封装)。 - 后续在同一请求中对该 Bean 的任何调用,都直接从 Request 属性中获取,保证同一请求内单例。
- 请求结束时,Spring 的
RequestContextListener或DispatcherServlet会调用ServletRequestAttributes.requestCompleted(),继而触发RequestScope的remove(),执行 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 容器对“对象管理”这一命题的深度思考:它不仅将实例化动作从业务代码中抽出,而且为不同的生命周期场景提供了风格一致的编程模型。开发者只需声明意图,复杂的存储、回调和代理逻辑全部由框架在幕后完成。