图解Spring源码4-Spring Bean的作用域
1. 从一个例子开始
小陈经过开店标准化审计流程后,终于拥有了一家自己的咖啡店,在营业前它向总部的咖啡杯生产工厂订购了一批一次性咖啡杯,每一个用户来到咖啡店可以使用一次性咖啡杯,也可以选择自带最爱的咖啡杯,自带咖啡杯的用户可以享受免费续杯。
这里我们关注三个对象:
总部的咖啡杯生产工厂:目前总部只有一家咖啡杯生产工厂,这是单例作用域,只要总部不倒闭,咖啡杯生产工厂一直会存在
每一个用户来到咖啡店可以使用的一次性咖啡杯:这是会话作用域,在一次会话(也就是一次到店消费)中这个杯子会一直存在
自带最爱的咖啡杯:单例作用域,针对每一个用户来说,它只有一个最爱的咖啡杯。
2. Spring 作用域是什么,有哪些作用域
Spring 作用域是用于定义和管理Bean实例的生命周期范围和共享策略的机制。它决定了Spring容器在何时创建Bean实例、如何共享实例,并在何时销毁实例。通过不同的作用域,Spring能够灵活地适应各种场景需求,例如优化资源使用、隔离用户状态等。
Spring内置了以下常见作用域:
table.feishu-table td:nth-child(0n+1), table.feishu-table th:nth-child(0n+1) { width: 16.27% }
table.feishu-table td:nth-child(0n+2), table.feishu-table th:nth-child(0n+2) { width: 41.87% }
table.feishu-table td:nth-child(0n+3), table.feishu-table th:nth-child(0n+3) { width: 41.87% }
table.feishu-table tr.width-enforcer { height: 0 !important; line-height: 0 !important; padding: 0 !important; visibility: hidden !important; border: none !important }
table.feishu-table tr.width-enforcer td { height: 0 !important; padding: 0 !important; border: none !important }
table.feishu-table { border-collapse: collapse; width: 100%; margin-bottom: 16px; table-layout: fixed }
table.feishu-table, table.feishu-table th, table.feishu-table td { border: 1px solid rgba(221, 221, 221, 1) }
table.feishu-table th, table.feishu-table td { padding: 8px; text-align: left; vertical-align: top; word-wrap: break-word }
table.feishu-table th { font-weight: bold }
table.feishu-table img { max-width: 100%; height: auto; display: block; margin: 0 auto }
作用域 | 描述 | 典型场景 |
---|---|---|
Singleton | 默认作用域,整个容器中仅存在一个Bean实例。 | 数据库连接池、配置类等全局共享组件。 |
Prototype | 每次请求(如通过getBean()或依赖注入)都创建一个新实例。 | 有状态的工具类(如日期格式化工具)。 |
Thread | 每一个线程创建一个新实例 | 多线程环境下的状态隔离和线程安全问题 |
Request | 每个HTTP请求创建一个实例(仅Web应用有效)。 | 用户提交的表单数据、请求级缓存。 |
Session | 每个用户会话(Session)创建一个实例(仅Web应用有效)。 | 用户登录状态、购物车数据。 |
Application | 整个ServletContext生命周期内共享一个实例(类似Singleton,但上下文不同)。 | 全局缓存、应用级配置。 |
3. 如何使用这些作用域
Spring中默认bean都是单例,不需要特殊使用注解,或者xml进行标注
3.1 使用注解配置
@Component
@Scope("prototype") // 或 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyPrototypeBean { ... }
- Web作用域专用注解:
@Component
@RequestScope
// 等效于 @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode =ScopedProxyMode.TARGET_CLASS)
public class MyRequestScopedBean { ... }
- Java配置类:
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public MyBean myBean() {
return new MyBean();
}
}
3.2. XML配置
<bean id="myBean" class="com.example.MyBean" scope="prototype"/>
<!-- Web作用域需配置为 request、session 等 -->
<bean id="requestBean" class="com.example.RequestBean" scope="request"/>
4. Spring是如何实现这些作用域的
4.1 解析XML或者注解将Scope记录到BeanDefinition
BeanDefinition:Bean的元数据信息
如同咖啡店开店规格单,就像表格中每一行配置定义了咖啡店的“基因”(用什么机器、找哪家供应商),BeanDefinition是Spring中描述对象的“元数据”。它不关心咖啡机如何运输,只记录“这个对象该长什么样”(类名、属性值、依赖关系等)。
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
/**
* Override the target scope of this bean, specifying a new scope name.
* @see #SCOPE_SINGLETON
* @see #SCOPE_PROTOTYPE
*/
void setScope(@Nullable String scope);
/**
* Return the name of the current target scope for this bean,
* or {@code null} if not known yet.
*/
@Nullable
String getScope();
}
BeanDefintion 提供记录当前Bean Scope以及获取Scope的能力,生成Bean的时候不同的Scope会引导BeanFactory作出不同行为
4.2 单例是如何实现的
// Create bean instance.
// mbd是BeanDefinition,如果是单例
if (mbd.isSingleton()) {
// 获取单例,第二个参数是一个函数式接口实例,当beanfactory中没有这个单例的时候调用createBean创建
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
// 省略其他
}
如下是getSingleton的逻辑
可以看到本质上单例对象会使用一个Map进行维护,优先从map中获取,如果没有才会创建bean,并且使用了synchronized来保证并发情况下不会创建多次实例
4.3 原型是如何实现的
如果是原型,每次都会新建一个新的bean
// 如果是原型,每次都会新建一个新的bean
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
// 创建实例
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
4.4 其他作用域是如何实现的
else {
// 获取作用域名称
String scopeName = mbd.getScope();
// 获取作用域实例
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
// 调用scope的get方法创建实例
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
其他作用域的bean会调用对于beanDefintion中记录的作用域,然后从scopes中根据scope名称获取对于的Scope对象,然后调用get方法
4.4.1 Scope的接口定义
// 作用域
public interface Scope {
// 根据名称获取bean,如果bean不存在那么调用ObjectFactory(函数式接口)的getObject进行生成
Object get(String name, ObjectFactory<?> objectFactory);
// 删除bean
@Nullable
Object remove(String name);
// 注册销毁的回调方法
void registerDestructionCallback(String name, Runnable callback);
// 省略其他不关键的方法
}
Object get(String name, ObjectFactory<?> objectFactory):根据名称获取bean,如果bean不存在那么调用ObjectFactory(函数式接口)的getObject进行生成
Object remove(String name):删除bean
void registerDestructionCallback(String name, Runnable callback):注册销毁的回调方法
4.4.2 registerDestructionCallback有什么用
用于管理作用域内 Bean 的生命周期,尤其是在作用域结束时(如请求结束、会话过期、自定义作用域销毁)触发清理逻辑。它在作用域的实现中扮演了 资源释放和生命周期控制 的角色。
比如说http请求的session生命周期结束了,这时候会调用Runnable callback对Session作用域的bean进行清理
DisposableBeanAdapter是一个Runable接口实现,run方法会调用对应的销毁方法@PreDestroy标注的方法DisposableBean#destory方法,以及还会触发DestructionAwareBeanPostProcessor#postProcessBeforeDestruction方法
4.4.3 常见Scope实现
thread作用域:实现类SimpleThreadScope,内部使用ThreadLocal来维护bean
session作用域:实现类SessionScope,
如何复用bean:本质上是从HttpServletRequest中获取Session,然后从Session中获取bean
如何销毁bean执行销毁回调:向HttpSession中注册了DestructionCallbackBindingListener(HttpSessionBindingListener)tomcat在session被销毁的时候,会回调valueUnbound方法,在这里会触发bean的销毁
Tomcat Session管理:
- Cookie(默认方式):
检查请求头中的Cookie字段,寻找名为JSESSIONID的Cookie值(可通过context.xml配置自定义名称)。 - URL重写(备用方式):
若Cookie未找到,检查URL中是否包含jsessionid参数(如/path;jsessionid=12345)。
也就是说tomcat会将sessionId写到cookie中,然后浏览器进行存储,并且在服务器本地内存中缓存sessionId对应的HttpSession对象,Spring的Session作用域就是bean存储到HttpSession中
tomcat还会单独启动一个线程,轮训判断session是否过期,如果过期那么调用HttpSessionBindingListener的valueUnbound,从而触发session作用域bean的销毁
request作用域:实现类RequestScope
如何复用bean:spring mvc会把当前请求存储到threadLocal中,对于request作用的bean其实是从threadLocal中获取当前的HttpServletRequest对象,然后调用getAttribute获取bean对象
如何销毁bean执行销毁回调:通过ServletRequestListener的实现类RequestContextListener#requestDestroyed,实现请求完成后Request作用域bean的销毁
application作用域:实现类ServletContextScope
如何复用bean:优先从ServletContext中进行注册bean,ServletContext在web引用中是唯一的
如何销毁bean执行销毁回调:ServletContextScope实现了DisposableBean,spring注册了ContextCleanupListener(ServletContextListener的实现类)在web引用关闭的时候会触发ServletContextListener#contextDestroyed,从而触发ServletContextScope的#destroy,进而触发Applicarion作用域bean的销毁
和单例的区别:单例是先注销单例bean,然后注销Spring的ApplicationContext,Application作用域则是ApplicationContext注销后,再注销Application作用域的bean(取决于ServletContextListener的触发顺序)
5. 为什么说大部分Spring作用域都无用
5.1 单例模式足够高效,适用于99%的场景:
大部分业务逻辑中的 Bean 是无状态的(如 Service、DAO),单例模式可以复用实例、节省资源,天然适合高并发场景。
5.2 存在替代方案
显式使用 ThreadLocal:
需要线程级隔离时,开发者可能直接使用 ThreadLocal 存储数据,而非依赖 Spring 的 Thread 作用域,因为前者更轻量且可控。通过参数传递状态:
在方法调用链中传递上下文(如 RequestContextHolder),比依赖作用域 Bean 更直观。复杂作用域的隐性成本
配置和维护成本高:
如 request 和 session 作用域需搭配代理模式(ScopedProxyMode),且要确保作用域生命周期与线程、请求或会话同步,容易出错。线程池的复用问题:
使用 thread 作用域时,线程池中的线程可能被复用,残留旧 Bean 状态,需手动清理(如实现 DisposableBean)
5.3 分布式系统的挑战:
在微服务架构中,session 作用域依赖本地会话,无法直接扩展到分布式环境,需借助 Redis 等外部存储,导致作用域 Bean 意义下降。
大型微服务架构中,网络请求需要经过:动态DNS → LVS(四层负载均衡) → 反向代理(七层负载均衡) → 网关(API Gateway) → 应用服务器
一个用户的请求很难做到固定的被同一台机器处理,session,request ,application都是在本地web服务内存维度的作用域,因此在分布式常见下,这些作用域基本上无用
基于redis这样的分布式缓存session,也没有必要把bean存储到redis进行反序列化,来实现session,request,这种作用域
分布式环境中我要尽量保证请求的无状态,无状态利于横向扩容,来提高服务的并发度
6. 有哪些巧妙的设计
- 面向接口编程:
Spring将作用域抽象为Scope接口,在接口中定义了作用域的能力,支持开发者根据自己的需要可插拔的加入自己的作用域
- 监听器模式:
对于web服务中的作用域,Spring结合javaee中提供的扩展Listener,实现对应作用域对象销毁的回调逻辑,例如基于HttpSessionBindingListener、RequestContextListener、ServletContextListener,分别实现了session,request,application三种作用域的对象销毁回调
7. 面试题&总结
7.1 Spring中有哪些作用域、各自有哪些应用场景
table.feishu-table td:nth-child(0n+1), table.feishu-table th:nth-child(0n+1) { width: 16.27% }
table.feishu-table td:nth-child(0n+2), table.feishu-table th:nth-child(0n+2) { width: 41.87% }
table.feishu-table td:nth-child(0n+3), table.feishu-table th:nth-child(0n+3) { width: 41.87% }
table.feishu-table tr.width-enforcer { height: 0 !important; line-height: 0 !important; padding: 0 !important; visibility: hidden !important; border: none !important }
table.feishu-table tr.width-enforcer td { height: 0 !important; padding: 0 !important; border: none !important }
table.feishu-table { border-collapse: collapse; width: 100%; margin-bottom: 16px; table-layout: fixed }
table.feishu-table, table.feishu-table th, table.feishu-table td { border: 1px solid rgba(221, 221, 221, 1) }
table.feishu-table th, table.feishu-table td { padding: 8px; text-align: left; vertical-align: top; word-wrap: break-word }
table.feishu-table th { font-weight: bold }
table.feishu-table img { max-width: 100%; height: auto; display: block; margin: 0 auto }
作用域 | 描述 | 典型场景 |
---|---|---|
Singleton | 默认作用域,整个容器中仅存在一个Bean实例。 | 数据库连接池、配置类等全局共享组件。 |
Prototype | 每次请求(如通过getBean()或依赖注入)都创建一个新实例。 | 有状态的工具类(如日期格式化工具)。 |
Thread | 每一个线程创建一个新实例 | 多线程环境下的状态隔离和线程安全问题 |
Request | 每个HTTP请求创建一个实例(仅Web应用有效)。 | 用户提交的表单数据、请求级缓存。 |
Session | 每个用户会话(Session)创建一个实例(仅Web应用有效)。 | 用户登录状态、购物车数据。 |
Application | 整个ServletContext生命周期内共享一个实例(类似Singleton,但上下文不同)。 | 全局缓存、应用级配置。 |
这里需要自主的提出,除了单例和原型基本上都没啥用,单例有极致的性能并且大部分业务代码中的service,dao都是无状态的,并且在分布式环境中网络请求需要经过:动态DNS → LVS(四层负载均衡) → 反向代理(七层负载均衡) → 网关(API Gateway) → 应用服务器
一个用户的请求很难做到固定的被同一台机器处理,session,request ,application都是在本地web服务内存维度的作用域,因此在分布式常见下,这些作用域基本上无用
分布式环境中我要尽量保证请求的无状态,无状态利于横向扩容,来提高服务的并发度
7.2 Spring中的作用域底层原理是什么
见4. Spring是如何实现这些作用域的
伏笔
单例AService 调用原型的BService,每一次是同一个BService,还是不同的BService
注意AService的属性引用了BService的实例,还能做到BService是原型么?
@Component
public class AService {
@Autowired
private BService bService;
// 单例AService 调用原型的BService,每一次是同一个BService,还是不同的BService
public void DoSomething() {
bService.DoSomething();
}
}
@Component
@Scope("prototype")
class BService {
public void DoSomething() {
}
}
。
图解Spring源码4-Spring Bean的作用域的更多相关文章
- Spring 源码分析之 bean 依赖注入原理(注入属性)
最近在研究Spring bean 生命周期相关知识点以及源码,所以打算写一篇 Spring bean生命周期相关的文章,但是整理过程中发现涉及的点太多而且又很复杂,很难在一篇文章中把Spri ...
- Spring源码分析之Bean的创建过程详解
前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...
- Spring源码-IOC部分-Bean实例化过程【5】
实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...
- 【Spring源码分析】Bean加载流程概览
代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. 很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事 ...
- 【Spring源码解读】bean标签中的属性
说明 今天在阅读Spring源码的时候,发现在加载xml中的bean时,解析了很多标签,其中有常用的如:scope.autowire.lazy-init.init-method.destroy-met ...
- Spring 源码分析之 bean 实例化原理
本次主要想写spring bean的实例化相关的内容.创建spring bean 实例是spring bean 生命周期的第一阶段.bean 的生命周期主要有如下几个步骤: 创建bean的实例 给实例 ...
- 【Spring源码分析】Bean加载流程概览(转)
转载自:https://www.cnblogs.com/xrq730/p/6285358.html 代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. ...
- Spring源码分析:Bean加载流程概览及配置文件读取
很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事的都是Java Web的工作,对于程序员来说,一个Web项目用到Spring,只是配置一下配置文件而已 ...
- 初探Spring源码之Spring Bean的生命周期
写在前面的话: 学无止境,写博客纯粹是一种乐趣而已,把自己理解的东西分享出去,不意味全是对的,欢迎指正! Spring 容器初始化过程做了什么? AnnotationConfigApplication ...
- 【转载】Spring 源码分析之 bean 实例化原理
本次主要想写spring bean的实例化相关的内容.创建spring bean 实例是spring bean 生命周期的第一阶段.bean 的生命周期主要有如下几个步骤: 创建bean的实例 给实例 ...
随机推荐
- 浅析Bootstrap中Tab(标签页)的使用方法
Bootstrap 导航元素使用相同的标记和基类,改变修饰的class,可以在不同的样式间进行切换如".nav-pills"(胶囊式导航)与 ".nav-tabs&quo ...
- ServerMmon青蛇探针,一个超好用的服务器状态监控-搭建教程
serverMmon(青蛇探针)是nodeJs开发的一个酷炫高逼格的云探针.云监控.服务器云监控.多服务器探针~. 在线演示:http://106.126.11.114:5880/ 主要功能: 全球服 ...
- 响应式编程之Reactive Streams介绍
Reactive Streams 是一种用于异步流处理的标准化规范,旨在解决传统异步编程中的背压管理.资源消耗及响应速度等问题. 一.核心概念 基本模型 发布者(Publisher):负责 ...
- 浅说树形dp
@ 目录 前言 树形dp的转移方式 树形dp的使用的场景 小结 初步感知--简单的树形dp 例题1 例题2 深入分析--树形dp的经典模型 最大独立集 最小点覆盖 最小支配集 树上直径 前言 因为树的 ...
- Kubernetes v1.16.3版本开启 Job ttlSecondsAfterFinished 自动清理机制
前言 Kubernetes v1.23 之前,Job 在处于 Completed 后,默认是不会被清理的. 完成的 Job 通常不需要留存在系统中.在系统中一直保留它们会给 API 服务器带来额外的压 ...
- Delphi 非主窗体(即子窗体)在任务栏显示按钮
type TForm2 = class(TForm) private { Private declarations } public { Public declarations } procedure ...
- Qt/C++开发经验小技巧311-315
关于流媒体推拉流延时的几点说明. 经常看到一些流媒体相关的程序,号称零延迟,不用怀疑,这肯定吹牛逼的. 搞音视频开发,有个核心的指标就是实时性,也就是延迟多少毫秒,这个问题问的也是最多的. 音视频文件 ...
- ELF-Virus简易病毒程序分析
系统功能概述 ELF-Virus实现了一个简单的病毒程序,能够感染当前目录下的ELF格式的可执行文件.病毒程序通过将自身代码附加到目标文件中,并在文件末尾添加一个特定的签名来标记文件已被感染.感染后的 ...
- Lambda表达式的省略规则、Lambda和匿名内部类的区别--java进阶day03
1.省略规则 2.流程讲解 主方法中调用useStringhandler,该方法的形参是接口,所以我们要给实现类对象,这里我们使用匿名内部类 use...方法进栈,形参也是变量,接收到匿名内部类(如下 ...
- 超简单电脑本地部署deepseek,另附”一键使用脚本“撰写与联网使用方法
在电脑上部署deepseek,总共分三步 1.打开ollama官网点击Download按钮 2.在ollama官网搜索deepseek-r1模型,选择对应规模,并复制ollama命令,比如这里,我的o ...