>>>点击去看B站配套视频<<<

系列文章目录和关于我

1. 从一个例子开始

小陈经过开店标准化审计流程后,终于拥有了一家自己的咖啡店,在营业前它向总部的咖啡杯生产工厂订购了一批一次性咖啡杯,每一个用户来到咖啡店可以使用一次性咖啡杯,也可以选择自带最爱的咖啡杯,自带咖啡杯的用户可以享受免费续杯。

这里我们关注三个对象:

  1. 总部的咖啡杯生产工厂:目前总部只有一家咖啡杯生产工厂,这是单例作用域,只要总部不倒闭,咖啡杯生产工厂一直会存在

  2. 每一个用户来到咖啡店可以使用的一次性咖啡杯:这是会话作用域,在一次会话(也就是一次到店消费)中这个杯子会一直存在

  3. 自带最爱的咖啡杯:单例作用域,针对每一个用户来说,它只有一个最爱的咖啡杯。

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); // 省略其他不关键的方法
}
  1. Object get(String name, ObjectFactory<?> objectFactory):根据名称获取bean,如果bean不存在那么调用ObjectFactory(函数式接口)的getObject进行生成

  2. Object remove(String name):删除bean

  3. 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实现

  1. thread作用域:实现类SimpleThreadScope,内部使用ThreadLocal来维护bean

  2. session作用域:实现类SessionScope,

  3. 如何复用bean:本质上是从HttpServletRequest中获取Session,然后从Session中获取bean

  4. 如何销毁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的销毁
  1. request作用域:实现类RequestScope

  2. 如何复用bean:spring mvc会把当前请求存储到threadLocal中,对于request作用的bean其实是从threadLocal中获取当前的HttpServletRequest对象,然后调用getAttribute获取bean对象

  3. 如何销毁bean执行销毁回调:通过ServletRequestListener的实现类RequestContextListener#requestDestroyed,实现请求完成后Request作用域bean的销毁

  4. application作用域:实现类ServletContextScope

  5. 如何复用bean:优先从ServletContext中进行注册bean,ServletContext在web引用中是唯一的

  6. 如何销毁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 存在替代方案

  1. 显式使用 ThreadLocal​:

    需要线程级隔离时,开发者可能直接使用 ThreadLocal 存储数据,而非依赖 Spring 的 Thread 作用域,因为前者更轻量且可控。

  2. 通过参数传递状态​:

    在方法调用链中传递上下文(如 RequestContextHolder),比依赖作用域 Bean 更直观。

  3. 复杂作用域的隐性成本

  4. 配置和维护成本高​:

    如 request 和 session 作用域需搭配代理模式(ScopedProxyMode),且要确保作用域生命周期与线程、请求或会话同步,容易出错。

  5. 线程池的复用问题​:

    使用 thread 作用域时,线程池中的线程可能被复用,残留旧 Bean 状态,需手动清理(如实现 DisposableBean)

5.3 分布式系统的挑战​:

在微服务架构中,session 作用域依赖本地会话,无法直接扩展到分布式环境,需借助 Redis 等外部存储,导致作用域 Bean 意义下降。

大型微服务架构中,网络请求需要经过:动态DNS → LVS(四层负载均衡)​ → 反向代理(七层负载均衡)​ → 网关(API Gateway)​ → 应用服务器

一个用户的请求很难做到固定的被同一台机器处理,session,request ,application都是在本地web服务内存维度的作用域,因此在分布式常见下,这些作用域基本上无用

基于redis这样的分布式缓存session,也没有必要把bean存储到redis进行反序列化,来实现session,request,这种作用域

分布式环境中我要尽量保证请求的无状态,无状态利于横向扩容,来提高服务的并发度

6. 有哪些巧妙的设计

  1. 面向接口编程:

Spring将作用域抽象为Scope接口,在接口中定义了作用域的能力,支持开发者根据自己的需要可插拔的加入自己的作用域

  1. 监听器模式:

    对于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的作用域的更多相关文章

  1. Spring 源码分析之 bean 依赖注入原理(注入属性)

         最近在研究Spring bean 生命周期相关知识点以及源码,所以打算写一篇 Spring bean生命周期相关的文章,但是整理过程中发现涉及的点太多而且又很复杂,很难在一篇文章中把Spri ...

  2. Spring源码分析之Bean的创建过程详解

    前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...

  3. Spring源码-IOC部分-Bean实例化过程【5】

    实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...

  4. 【Spring源码分析】Bean加载流程概览

    代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. 很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事 ...

  5. 【Spring源码解读】bean标签中的属性

    说明 今天在阅读Spring源码的时候,发现在加载xml中的bean时,解析了很多标签,其中有常用的如:scope.autowire.lazy-init.init-method.destroy-met ...

  6. Spring 源码分析之 bean 实例化原理

    本次主要想写spring bean的实例化相关的内容.创建spring bean 实例是spring bean 生命周期的第一阶段.bean 的生命周期主要有如下几个步骤: 创建bean的实例 给实例 ...

  7. 【Spring源码分析】Bean加载流程概览(转)

    转载自:https://www.cnblogs.com/xrq730/p/6285358.html 代码入口 之前写文章都会啰啰嗦嗦一大堆再开始,进入[Spring源码分析]这个板块就直接切入正题了. ...

  8. Spring源码分析:Bean加载流程概览及配置文件读取

    很多朋友可能想看Spring源码,但是不知道应当如何入手去看,这个可以理解:Java开发者通常从事的都是Java Web的工作,对于程序员来说,一个Web项目用到Spring,只是配置一下配置文件而已 ...

  9. 初探Spring源码之Spring Bean的生命周期

    写在前面的话: 学无止境,写博客纯粹是一种乐趣而已,把自己理解的东西分享出去,不意味全是对的,欢迎指正! Spring 容器初始化过程做了什么? AnnotationConfigApplication ...

  10. 【转载】Spring 源码分析之 bean 实例化原理

    本次主要想写spring bean的实例化相关的内容.创建spring bean 实例是spring bean 生命周期的第一阶段.bean 的生命周期主要有如下几个步骤: 创建bean的实例 给实例 ...

随机推荐

  1. [BZOJ3451] Normal 题解

    这题分三步:葺网(期望).淀粉质(点分治).蓉翅(容斥),再佐以芬芳团(FFT),一道巨难无比的 luogu 黑题就诞生了. 期望 先考虑在淀粉树上,\(i\) 点在 \(j\) 点的子树里的概率.实 ...

  2. Kettle - 使用案例

    原文链接:https://blog.csdn.net/gdkyxy2013/article/details/117106691 案例一:把seaking的数据按id同步到seaking2,seakin ...

  3. 【BUUCTF】Easy MD5

    [BUUCTF]Easy MD5 (SQL注入.PHP代码审计) 题目来源 收录于:BUUCTF BJDCTF2020 题目描述 抓包得到提示 select * from 'admin' where ...

  4. 机器学习 | 强化学习(2) | 动态规划求解(Planning by Dynamic Programming)

    动态规划求解(Planning by Dynamic Programming) 动态规划概论 动态(Dynamic):序列性又或是时序性的问题部分 规划(Programming):最优化一个程序(Pr ...

  5. ModuleNotFoundError: No module named '_ctypes' when Python3

    前言 运行 python 报错:ModuleNotFoundError: No module named '_ctypes' when using Value from module multipro ...

  6. [Vue warn]: Unknown custom element: did you register the component correctly?

    前言 [Vue warn]: Unknown custom element: did you register the component correctly? For recursive compo ...

  7. NumPy学习3

    继续学习NumPy,今天学习以下3个章节: 7,NumPy高级索引 8,NumPy广播机制 9,NumPy遍历数组 numpy_test3.py : import numpy as np ''' 7, ...

  8. celery 启动显示警告信息“...whether broker connection retries are made during startup in Celery 6.0 and above...”

    博客地址:https://www.cnblogs.com/zylyehuo/ # celery作为一个单独项目运行,在settings文件中设置 broker_connection_retry_on_ ...

  9. AI Agent爆火后,MCP协议为什么如此重要!

    什么是MCP? 模型上下文协议(Model Context Protocol, MCP)是一种专为机器学习模型服务设计的通信协议,旨在高效管理模型推理过程中的上下文信息(如会话状态.环境变量.动态配置 ...

  10. 如何每5分钟、10分钟或15分钟运行一次Cron计划任务

    一个cron job是一个在指定时间段执行的任务.这些任务可以按分钟.小时.月.日.周.日或这些的任何组合来安排运行. Cron作业一般用于自动化系统维护或管理,例如备份数据库或数据.用最新的安全补丁 ...