>>>点击去看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. 浅析Bootstrap中Tab(标签页)的使用方法

    Bootstrap 导航元素使用相同的标记和基类,改变修饰的class,可以在不同的样式间进行切换如".nav-pills"(胶囊式导航)与 ".nav-tabs&quo ...

  2. ServerMmon青蛇探针,一个超好用的服务器状态监控-搭建教程

    serverMmon(青蛇探针)是nodeJs开发的一个酷炫高逼格的云探针.云监控.服务器云监控.多服务器探针~. 在线演示:http://106.126.11.114:5880/ 主要功能: 全球服 ...

  3. 响应式编程之Reactive Streams介绍

    Reactive Streams 是一种用于‌异步流处理的标准化规范,旨在解决传统异步编程中的背压管理.资源消耗及响应速度等问题‌. 一.核心概念 ‌基本模型‌ ‌发布者(Publisher)‌:负责 ...

  4. 浅说树形dp

    @ 目录 前言 树形dp的转移方式 树形dp的使用的场景 小结 初步感知--简单的树形dp 例题1 例题2 深入分析--树形dp的经典模型 最大独立集 最小点覆盖 最小支配集 树上直径 前言 因为树的 ...

  5. Kubernetes v1.16.3版本开启 Job ttlSecondsAfterFinished 自动清理机制

    前言 Kubernetes v1.23 之前,Job 在处于 Completed 后,默认是不会被清理的. 完成的 Job 通常不需要留存在系统中.在系统中一直保留它们会给 API 服务器带来额外的压 ...

  6. Delphi 非主窗体(即子窗体)在任务栏显示按钮

    type TForm2 = class(TForm) private { Private declarations } public { Public declarations } procedure ...

  7. Qt/C++开发经验小技巧311-315

    关于流媒体推拉流延时的几点说明. 经常看到一些流媒体相关的程序,号称零延迟,不用怀疑,这肯定吹牛逼的. 搞音视频开发,有个核心的指标就是实时性,也就是延迟多少毫秒,这个问题问的也是最多的. 音视频文件 ...

  8. ELF-Virus简易病毒程序分析

    系统功能概述 ELF-Virus实现了一个简单的病毒程序,能够感染当前目录下的ELF格式的可执行文件.病毒程序通过将自身代码附加到目标文件中,并在文件末尾添加一个特定的签名来标记文件已被感染.感染后的 ...

  9. Lambda表达式的省略规则、Lambda和匿名内部类的区别--java进阶day03

    1.省略规则 2.流程讲解 主方法中调用useStringhandler,该方法的形参是接口,所以我们要给实现类对象,这里我们使用匿名内部类 use...方法进栈,形参也是变量,接收到匿名内部类(如下 ...

  10. 超简单电脑本地部署deepseek,另附”一键使用脚本“撰写与联网使用方法

    在电脑上部署deepseek,总共分三步 1.打开ollama官网点击Download按钮 2.在ollama官网搜索deepseek-r1模型,选择对应规模,并复制ollama命令,比如这里,我的o ...