spring高级篇(九)
boot的执行流程分为构造SpringApplication对象、调用run方法两部分
1、Spring Boot 执行流程-构造
通常我们会在SpringBoot的主启动类中写以下的代码:
参数一是当前类的字节码,参数二是main的args参数。
public class StartApplication {public static void main(String[] args) {SpringApplication.run(SpringApplication.class,args);}
}
在SpringApplication.run的内部,会做两件事:

创建SpringApplication对象:

以及调用SpringApplication对象的run方法:

在创建SpringApplication对象时,通常又会做下面几件事:
- 获取bean definition源
- 获取推断应用类型
- ApplicationContext初始化器
- 监听器与事件
- 主类推断
1.1、获取bean definition源
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
将传入的 primarySources数组转换为 LinkedHashSet,并赋值给当前对象的 primarySources成员变量。这个集合用于存储应用程序的主要源,通常是启动类(例如,@SpringBootApplication注解标记的类)。
1.2、获取推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
通过deduceFromClasspath() 方法进行应用类型推论:
deduceFromClasspath() 是WebApplicationType的方法,WebApplicationType是一个枚举类:

简单说明一下这段代码:第一个if判断,如果是reactive.DispatcherHandler,并且不是servlet.DispatcherServlet和servlet.ServletContainer,就推断类型为REACTIVE并返回。
如果上面的条件不成立,也就是应用类型没有被推断为REACTIVE,就会循环成员变量的SERVLET_INDICATOR_CLASSES数组,该数组中有两个元素:
- javax.servlet.Servlet
- org.springframework.web.context.ConfigurableWebApplicationContext
如果任意一个元素不存在就推断类型为NONE并返回。
最后推论类型为SERVLET并返回

1.3、ApplicationContext初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
获取所有注册的 ApplicationContextInitializer实例,并将其设置为应用程序的初始化器。
通过SpringApplication实例的.addInitializers()方法注册初始化器
springApplication.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {if (applicationContext instanceof GenericApplicationContext){((GenericApplicationContext) applicationContext).registerBean("bean3",Bean3.class);}}});
1.4、监听器与事件
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
获取所有注册的 ApplicationListener实例,并将其设置为应用程序的监听器。
通过SpringApplication实例的.addListeners()方法注册监听器与事件
springApplication.addListeners(new ApplicationListener<ApplicationEvent>() {@Overridepublic void onApplicationEvent(ApplicationEvent event) {System.out.println("事件:"+event.getClass());}});
1.5、主类推断
this.mainApplicationClass = deduceMainApplicationClass();
调用deduceMainApplicationClass() 方法进行主类推论:
- 通过创建一个新的 RuntimeException对象来获取当前线程的堆栈轨迹(stack trace),即调用堆栈信息。
- 遍历堆栈信息,找到方法名为"main"的函数,并返回类的class对象
- 没有找到就抛出异常

2、Spring Boot 执行流程-run
2.1、事件发布器

在run方法中,首先会通过getRunListeners(args);得到 SpringApplicationRunListeners。
SpringApplicationRunListeners是一个事件发布器,会在run方法的执行不同阶段中发布对应的事件:
- 得到事件发布器后,会发布listeners.starting(); 事件,代表spring boot 开始启动。
- prepareEnvironment(listeners, applicationArguments);方法执行时,会发布listeners.environmentPrepared(environment); 事件,代表环境信息准备完成
- prepareContext(context, environment, listeners, applicationArguments, printedBanner); 方法执行时,会发布listeners.contextPrepared(context);事件,代表spring容器创建,但未调用初始化器。listeners.contextLoaded(context);事件,代表所有bean definition加载完毕。(然后会调用context.refresh();方法)
- 在调用context.refresh();方法后,会发布listeners.started(context);事件
- 如果在这个过程中发生了异常,会发布listeners.failed(context, exception); 事件
- 最后会发布listeners.running(context);事件,代表spring boot 启动完成
在第六点中,如果在发布listeners.running(context);事件的过程中出现异常,不会发布listeners.failed(context, exception); 事件:

我们也可以模拟一下上述过程:
事件发布器SpringApplicationRunListener是一个接口,其与子类的对应关系是放在spring.factories的配置文件中:

public class A34 {public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {SpringApplication springApplication = new SpringApplication(A34.class);springApplication.addListeners(new ApplicationListener<ApplicationEvent>() {@Overridepublic void onApplicationEvent(ApplicationEvent event) {System.out.println(event.getClass());}});//演示事件发送器,获取事件发送器类名//事件发送器是一个接口,和子类的对应关系存放在spring.factories配置文件中List<String> factoryNames = SpringFactoriesLoader.loadFactoryNames(SpringApplicationRunListener.class, A34.class.getClassLoader());for (String name : factoryNames) {System.out.println(name);//创建事件发布器实现类Class<?> clazz = Class.forName(name);Constructor<?> constructor = clazz.getConstructor(SpringApplication.class, String[].class);EventPublishingRunListener publishingRunListener = (org.springframework.boot.context.event.EventPublishingRunListener) constructor.newInstance(springApplication, args);//模拟SpringApplication源码public ConfigurableApplicationContext run(String... args) 方法中的七个事件//spring boot 开始启动publishingRunListener.starting();//环境信息准备完成publishingRunListener.environmentPrepared(new StandardEnvironment());//在spring容器创建,并调用初始化器之前,发送此事件GenericApplicationContext context = new GenericApplicationContext();publishingRunListener.contextPrepared(context);//所有bean definition加载完毕publishingRunListener.contextLoaded(context);context.refresh();//spring 容器初始化完成 refresh方法调用完成,加载了所有后处理器,初始化所有单例publishingRunListener.started(context);//spring boot 启动完毕publishingRunListener.running(context);//过程中发生错误publishingRunListener.failed(context,new Exception("报错"));}}
}
2.2、封装args参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
用于将args参数进行封装,便于最后一步执行实现了ApplicationRunner接口的方法。(实现了CommandLineRunner或ApplicationRunner接口的方法会在boot启动时运行其中的逻辑)

2.3、创建环境对象
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); 用于创建环境对象:
getOrCreateEnvironment()方法用于根据不同的webApplication类型,去创建对应的环境对象。(webApplication类型是在构造SpringApplication时推论出的)

configureEnvironment(ConfigurableEnvironment environment, String[] args) 方法用于配置应用程序的环境:
environment.setConversionService(new ApplicationConversionService()); 会先添加转换器:

其中又有configureProfiles(environment, args);方法来配置环境的配置文件:

configurePropertySources(environment, args) 方法配置环境的属性源(重点:在这一步只加入命令行参数,没有加入application.properties):
首先从环境中获取systemProperties和systemEnvironment


2.4、配置文件读取统一命名处理
prepareEnvironment中的 ConfigurationPropertySources.attach(standardEnvironment); 方法用于统一命名处理:

因为配置文件在读取的过程中可能会存在这样的问题:
我们定义了一个配置文件,每个键的格式都不一样:

当我们不做任何处理,在读取时键名统一使用"-"分隔时,只能读取到第一个的值,因为命名和文件中不匹配。ConfigurationPropertySources.attach() 方法可以解决这样的问题。
public class Step4 {public static void main(String[] args) throws IOException {StandardEnvironment standardEnvironment = new StandardEnvironment();standardEnvironment.getPropertySources().addLast(new ResourcePropertySource("step4",new ClassPathResource("step4.properties")));// run 源码中做了统一命名ConfigurationPropertySources.attach(environment);ConfigurationPropertySources.attach(standardEnvironment);//不做任何处理 只能读取第一个 因为命名和文件中的不匹配System.out.println(standardEnvironment.getProperty("user.first-name"));System.out.println(standardEnvironment.getProperty("user.middle-name"));System.out.println(standardEnvironment.getProperty("user.last-name"));}
}
在attach方法内部,最后会将configurationProperties加入environment的头部,以便优先被访问

2.5、EnvironmentPostProcessor功能扩展
EnvironmentPostProcessor相当于环境对象的后处理器,可以增加一些扩展。
prepareEnvironment 中的listeners.environmentPrepared(bootstrapContext, environment);
使用事件发布器去触发监听器,调用其中的后处理器方法:(监听器是在SpringApplication构造时加入的,但是需要等到环境对象创建后才应该触发其中的那些后处理器)
通过debug发现,在构造SpringApplication时已经加入了对于环境对象后处理器的监听器

关于后处理器的实现关系也是定义在spring.factories文件中的:

EnvironmentPostProcessorApplicationListener就是去读取EnvironmentPostProcessor中的后处理器



2.6、将环境中的键值和SpringApplication中的属性匹配
prepareEnvironment 中的bindToSpringApplication(environment); 用于将环境中的键值和SpringApplication中的属性匹配:

读取配置文件中以spring.main开头的键,并且与SpringApplication中的成员变量绑定:


上面配置文件中的键都是SpringApplication中的成员变量:


2.7、输出Banner图标
run()方法中的printBanner(environment); 用于在控制台或日志中输入图标:


2.8、创建容器
在环境准备完成并发布listeners.environmentPrepared(environment); 事件后,会创建容器:

createApplicationContext() 方法根据构造SpringApplication推断的不同的类型有不同的实现:

2.9、准备容器
prepareContext(context, environment, listeners, applicationArguments, printedBanner); 方法用于准备容器:

在准备容器时又会回调构造SpringApplication时编写的初始化器中的增强逻辑:


2.10、加载Bean定义
prepareContext方法中的load(context, sources.toArray(new Object[0]));

这段代码大致的意思是,加载应用程序的配置源,并根据需要设置相关的配置,然后执行加载操作。
根据不同的配置类型去分派对应的加载方法:

2.11、refresh
执行run中的refreshContext()方法:

调用refresh方法,加载配置、初始化所有单例 、重新启动应用程序上下文。
在这之前还会判断,如果this.registerShutdownHook为true,那么会通过 shutdownHook.registerApplicationContext(context);方法为当前的应用程序上下文注册一个关机钩子(shutdown hook)。这个钩子用于在应用程序关闭时执行一些清理操作或释放资源。

2.12、执行初始化方法
run方法中的callRunners 用于执行初始化方法中编写的逻辑:

“2.2、封装args参数”的作用就体现在此:
ApplicationRunner需要将args参数包装成为ApplicationArguments类型:
虽然无论是ApplicationRunner还是CommandLineRunner,调用callRunner() 方法时传递的args参数类型都是ApplicationArguments。但是不代表CommandLineRunner需要ApplicationArguments参数类型:
会取出原本的String args 参数

相关文章:
spring高级篇(九)
boot的执行流程分为构造SpringApplication对象、调用run方法两部分 1、Spring Boot 执行流程-构造 通常我们会在SpringBoot的主启动类中写以下的代码: 参数一是当前类的字节码,参数二是main的args参数。 public class StartApplication {public static…...
用wordpress建跨境电商独立站的5大优势
免费和开源 WordPress是一个免费的开源内容管理系统,用户可以自由下载、安装和使用,无需支付版权费用或订阅费用。开源特性也意味着用户可以根据自己的需求修改和定制代码,或者使用其他开发者提供的插件和主题来扩展和美化网站。 易用和灵活…...
Windows中安装的PostgreSQL 数据库如何重启
1. 使用Windows服务管理器 打开“运行”对话框(按WinR键)。输入services.msc并按回车,这将打开服务列表。在服务列表中找到PostgreSQL服务。它通常命名为“PostgreSQL”后面跟着版本号和实例名称,例如“PostgreSQL 13 - mydb”。…...
Remix框架实现 SSR
SSR SSR是一种网页渲染方式,它与传统的客户端渲染(CSR)相对,在日常的项目中我们更多是使用 CSR 的方式进行前端分离开发,渲染会在浏览器端进行。然而在SSR中,当用户请求一个网页时,服务器将生成…...
如何快速开发项目,提高开发效率
文章目录 一、问题描述二、问题解决1.需求分析2.架构设计3.技术选型4.正式开发 一、问题描述 有很多小伙伴在开发一个项目的时候,总是需要很长时间,效率很低,其实本质是没有掌握开发项目的关键和技巧 我下面列举一些问题,不知道…...
面试笔记——多线程使用场景
线程池使用场景(CountDownLatch, Future) CountDownLatch CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件…...
02.0 基于Verilog控制LED灯每秒钟闪烁一次
本段代码是为Verilog初学者提供的一个名为led_blink简单实例Verilog模块,其功能是控制6个LED灯同步闪烁,每秒钟闪烁一次。 本例代码用于理解时序逻辑的概念,理解多个always模块完全并行执行的概念,讲授时可以与C语言的执行过程进行…...
C语言创建文件夹和多级目录
C调用系统命令创建多级目录 #include <stdio.h> #include <stdlib.h>int main() {const char *path "a/b/c";// 创建目录命令的字符串char mkdir_command[100];sprintf(mkdir_command, "mkdir %s", path);// 调用系统命令system(mkdir_comma…...
2024.5.6
#include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {//设置窗口大小this->resize(1000,740);//设置窗口图标this->setWindowIcon(QIcon("C:\\Users\\Administrator\\Desktop\\pictrue\\Plants.png"));//设置窗口标题this-…...
mybatis配置获取自增主键
mybatis配置获取自增主键 【/n】 01 使用场景 当需要刚刚插入数据库的数据对应的新增主键时,通过配置xml文件,使数据库返回新增主键id,并把主键id与类参数对应 02 涉及配置 注解TableId(type IdType.AUTO):在类主键id通过配…...
完整、免费的把pdf转word文档
在线工具网 https://www.orcc.online 支持pdf转word,免费、完整、快捷 登录网站 https://orcc.online 选择需要转换的pdf文件: 等待转换完成 点击蓝色文件即可下载 无限制,完整转换。...
使用 Lua 协程模拟 Golang 的 go defer 编程模式
封装 go 函数 在 使用 Lua 协程处理异步回调函数 中已经介绍 这里简要列下: 封装 go 函数---go 函数创建并启动一个协程 ---param _co_task function 函数原型 fun(_co:thread) function go(_co_task)local co coroutine.create(_co_task) -- 创建的协程是暂停的…...
网络通信协议,UDP和TCP,初步了解
UDP(User Datagram Protocol)和TCP(Transmission Control Protocol)是两种常见的网络通信协议,用于在计算机网络中进行数据传输。 1. TCP:Transmission Control Protocol(传输控制协议…...
Golang | Leetcode Golang题解之第61题旋转链表
题目: 题解: func rotateRight(head *ListNode, k int) *ListNode {if k 0 || head nil || head.Next nil {return head}n : 1iter : headfor iter.Next ! nil {iter iter.Nextn}add : n - k%nif add n {return head}iter.Next headfor add > …...
美业SaaS系统多门店收银系统源码-【分润常见问题】讲解(一)
美业管理系统源码 博弈美业SaaS系统 连锁多门店美业收银系统源码 多门店管理 / 会员管理 / 预约管理 / 排班管理 / 商品管理 / 促销活动 PC管理后台、手机APP、iPad APP、微信小程序 ▶ 分润常见问题: 1、分润金额基数 所有分润计算的基数均为平台订单中各个商…...
Chatbot 在教育中的应用
Chatbot 在教育中的应用 基本信息 这篇博客主要介绍几篇Chatbot在教育领域中应用的文章,根据文章的侧重点不同,分为介绍教育理论,与介绍系统设计两类。从问题定义、技术方法、教育学理论、实验设计、结论证据几个方面概括各篇文章。 博…...
Apache和Nginx的区别以及如何选择
近来遇到一些客户需要lnmp环境的虚拟主机,但是Hostease这边的虚拟主机都是基于Apache的,尽管二者是不同的服务器软件,但是大多数情况下,通过适当的配置和调整两者程序也是可以兼容的。 目前市面上有许多Web服务器软件,…...
深入探索Element-UI:构建高效Web前端的利器
深入探索Element-UI:构建高效Web前端的利器 引言:前端框架的选择与Element-UI的定位一、Element-UI初探二、快速上手:安装与配置三、核心组件深度解析四、实用功能与进阶技巧五、性能优化与最佳实践六、实战案例分析七、与其他技术栈的集成 安…...
在Ubuntu 24.04 LTS (Noble Numbat)上安装nfs server以及nfs client
在Ubuntu 24.04 LTS (Noble Numbat)上,我使用的是最小化安装, 当然server版本的Ubuntu在安装的时候可能会有网络不通的问题,解决办法见如下文章: ubuntu 24.04 server 仅NAT模式上网设置静态IP设置-CSDN博客文章浏览阅读489次,点赞9次,收藏3次。在Ubuntu 24.04 上设置网…...
供应链|经典论文解读:(s,S) 策略在动态库存下的最优性
文章考虑了具有订购成本(由单位成本加上重新订购成本组成)的动态库存问题。具体而言,对于每个时期,系统在中期开始是做出一系列采购决策——这些采购有助于库存的积累,并在随后的周期被需求所消耗。每时期系统会产生各…...
手搓LabVIEW声音采集系统——从调参到装X全攻略
Labview程序开发——声音采集系统 基于 Labview软件,以声卡为数据采集设备开发数据采集处理系统。 系统功能应包括: 1)声卡参数设置:包括设备ID号,采样模式,每通道采样点 数,采样率,通道数等参数设置; 2) 文件操作:实现…...
告别重复造轮子:用快马AI一键生成极客日报的高效数据管道代码
告别重复造轮子:用快马AI一键生成极客日报的高效数据管道代码 作为一个技术资讯类应用的开发者,我深知数据管道的搭建有多耗时。从内容抓取到清洗处理,再到分类归档,每个环节都需要大量重复性编码。最近尝试了InsCode(快马)平台的…...
LVGL实战:用外部按键(Keypad)和旋转编码器(Encoder)在无触摸屏设备上实现流畅UI交互
LVGL物理交互实战:用按键与编码器打造无触摸屏的流畅UI控制 在智能家居控制面板、工业HMI设备等场景中,物理按键和旋转编码器因其可靠性和低成本优势,成为触摸屏的理想替代方案。本文将深入探讨如何通过LVGL的输入设备子系统,实现…...
用Neural Renderer和PyTorch搞定3D车辆模型渲染:从.obj文件到Carla数据集实战
3D车辆模型渲染实战:Neural Renderer与Carla数据集深度整合指南 在自动驾驶和计算机视觉领域,逼真的3D车辆模型渲染技术正成为算法开发和测试的关键环节。传统渲染方法往往难以平衡效率与真实感,而基于神经网络的渲染技术为解决这一难题提供了…...
终极OptiScaler配置指南:3步掌握免费游戏画质提升神器
终极OptiScaler配置指南:3步掌握免费游戏画质提升神器 【免费下载链接】OptiScaler DLSS replacement for AMD/Intel/Nvidia cards with multiple upscalers (XeSS/FSR2/DLSS) 项目地址: https://gitcode.com/GitHub_Trending/op/OptiScaler 想要在不升级硬件…...
开发者利器:OpenClaw调用nanobot自动生成Python单元测试
开发者利器:OpenClaw调用nanobot自动生成Python单元测试 1. 为什么需要AI生成单元测试? 作为一名长期奋战在一线的开发者,我深知单元测试的重要性,但同时也饱受编写测试用例的折磨。每次面对一个新函数,我需要&#…...
网易云音乐评论爬虫实战:破解加密接口抓取数据
一、接口分析:找准评论数据请求入口 在抓取网易云音乐歌曲评论时,我们不难发现,页面并不会随着翻页刷新跳转,评论的加载属于异步AJAX请求。这类接口有一个明显特点:接口地址固定不变,分页切换完全依靠请求载…...
【已验证】基于STM32和HAL库的大夏龙雀BT311-10C02S蓝牙模块驱动
最近买了一个大夏龙雀家的蓝牙模块DX-BT311-10C02S,这个蓝牙是一款基于BLE 5.4规范的串口透传模块,支持AT指令配置、主从模式切换,非常适合与单片机搭配实现无线数据传输。如果是第一次买还是很便宜的,他家的模块有一说一是真的不…...
10xGenomics单细胞测序选3‘还是5‘?一文讲清免疫组库与基因表达分析的黄金选择
10xGenomics单细胞测序:3与5端策略在免疫组库与基因表达分析中的科学抉择 当实验室的离心机停止运转,科研人员往往面临一个关键抉择:该选择3还是5端单细胞测序?这个看似技术性的选择,实则直接影响着后续免疫组库分析的…...
OpenClaw从入门到应用——安装:更新OpenClaw
通过OpenClaw实现副业收入:《OpenClaw赚钱实录:从“养龙虾“到可持续变现的实践指南》 推荐方式:重新运行网站安装程序(原地升级) 首选的更新方式是重新运行官网提供的安装脚本。该脚本会自动检测现有安装࿰…...

