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…...
掌握Android Fragment开发之魂:Fragment的深度解析(下)
在上一篇文章中,我们深入探讨了Fragment 通信,包含Fragment 向 Activity 传递数据、Activity 向 Fragment 传递数据、Fragment 之间的通信方式。感兴趣的朋友,请前往查阅: 掌握Android Fragment开发之魂:Fragment的深度…...

小巧简单实用的Linux端口转发工具Rinetd
Linux下实现端口转发有很多种方法,尤其是在可以联网的情况下,更是容易。最近在资源受限的定制系统中,找到一个方便离线安装和使用的端口转发工具Rinetd,安装包仅几十K,而且有很多版本的Linux发行系统的支持。 1、安装…...

HackBar 新手使用教程(入门)
啥是Hackbar? Hackbar是一个Firefox 的插件,它的功能类似于地址栏,但是它里面的数据不受服务器的相应触发的重定向等其它变化的影响。 有网址的载入于访问,联合查询,各种编码,数据加密功能。 这个Hackbar可以帮助你在测试SQL注入,XSS漏洞和网站的安全性,主要是帮助…...

<Linux> 权限
目录 权限人员相对于文件来说的分类更改权限文件的拥有者与所属组umask粘滞位 权限 权限是操作系统用来限制对资源访问的机制,权限一般分为读、写、执行。系统中的每个文件都拥有特定的权限、所属用户及所属组,通过这样的机制来限制哪些用户、哪些组可以…...

Nacos Docker 快速部署----解决nacos鉴权漏洞问题
Nacos Docker 快速部署 1. 说明 1.1 官方文档 官方地址 https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html docker启动文件的gitlhub地址 https://github.com/nacos-group/nacos-docker.git 问题: 缺少部分必要配置与说明 1.2 部署最新版本Nacos&…...

存储或读取时转换JSON数据
一、 数据库类型 二、使用Hutool工具 存储时将数据转换为JSON数据 获取时将JSON数据转换为对象 发现问题: 原本数据对象是Address 和 Firend但是转换完成后数据变成了JSONArray和JSONObject 三、自定义TypeHandler继承Mybatis的BaseTypeHandler处理器 package …...
Spring Data JPA的作用和用法
Spring Data JPA 是 Spring 框架的一个模块,它提供了一种数据访问抽象,允许以一种声明式和简洁的方式来处理数据库操作。它基于 Java Persistence API (JPA),是一个行业标准的 ORM(对象关系映射)规范,用于将…...

【go项目01_学习记录08】
学习记录 1 模板文件1.1 articlesStoreHandler() 使用模板文件1.2 统一模板 1 模板文件 重构 articlesCreateHandler() 和 articlesStoreHandler() 函数,将 HTML 抽离并放置于独立的模板文件中。 1.1 articlesStoreHandler() 使用模板文件 . . . func articlesSt…...

Java中的线程
一、创建线程的几种方式? ① 通过继承Thread类并重写run方法 ,实现简单但不可以继承其他类 Thread底层也是实现了Runnable接口,重写的是run而不是start方法 ②实现Runnable接口并重写run方法, 避免了单继承的局限性ÿ…...
顺序表的实现(迈入数据结构的大门)(完整代码)
seqlist.h #pragma once typedef int SLDataType;#include<stdio.h> #include<stdlib.h> #include<assert.h>typedef struct SeqList {SLDataType* a;int size; // 有效数据个数int capacity; // 空间容量 }SL;//初始化和销毁 void SLInit(SL* ps); void SL…...

neo4j-5.11.0安装APOC插件or配置允许使用过程的权限
在已经安装好neo4j和jdk的情况下安装apoc组件,之前使用neo4j-community-4.4.30,可以找到配置apoc-4.4.0.22-all.jar,但是高版本neo4j对应没有apoc-X.X.X-all.jar。解决如下所示: 1.安装好JDK与neo4j 已经安装对应版本的JDK 17.0…...
mybatis 中 #{}和 ${}的区别是什么?
在 MyBatis 中,#{} 和 ${} 是两种用于参数替换的语法,但它们之间存在一些重要的区别,主要体现在安全性、预编译和动态 SQL 上。 安全性: #{}:这是预编译处理,MyBatis 会为传入的参数生成 PreparedStatement…...
深入解析C#中的接口设计原则
深入解析C#中的接口设计原则 目录 深入解析C#中的接口设计原则 一、接口设计的SOLID原则 二、接口设计的最佳实践 三、接口设计的高级技术 四、结论 接口在面向对象编程中扮演着至关重要的角色。它们是定义行为契约的一种方式,允许实现者提供这些行为的具体实现…...

106短信群发平台在金融和法务行业的应用分析
一、金融行业应用 1.客户通知与提醒:银行、证券、保险等金融机构经常需要向客户发送各类通知和提醒,如账户余额变动、交易确认、扣费通知、理财产品到期提醒等。106短信群发平台可以快速、准确地将这些信息发送到客户的手机上,确保客户及时获…...

Spring AOP(2)
目录 Spring AOP详解 PointCut 切面优先级Order 切点表达式 execution表达式 切点表达式示例 annotation 自定义注解MyAspect 切面类 添加自定义注解 Spring AOP详解 PointCut 上面代码存在一个问题, 就是对于excution(* com.example.demo.controller.*.*(..))的大量重…...

Spring-依赖注入的处理过程
前置知识 1 入口 DefaultListableBeanFactory#resolveDependency 2 每个依赖都有对应的DependencyDescriptor 3 自定绑定候选对象处理器AutowireCapableBeanFactory 注入处理 我们可以看到接口AutowireCapableBeanFactory中有两个方法。 第一个是单个注入: Null…...
2.用python爬取的保存在text文件中的格式为MP4的视频url
文章目录 一、url的保存格式二、MP4视频获取 一、url的保存格式 爬取的视频名字和url保存在text文件中,每一个视频都是一个单独的text,其中text的文件名就是视频的名字,text内容是视频的下载url,并且所有的text都保存在同一个文件…...

Java基于B/S医院绩效考核管理平台系统源码java+springboot+MySQL医院智慧绩效管理系统源码
Java基于B/S医院绩效考核管理平台系统源码javaspringbootMySQL医院智慧绩效管理系统源码 医院绩效考核系统是一个关键的管理工具,旨在评估和优化医院内部各部门、科室和员工的绩效。一个有效的绩效考核系统不仅能帮助医院实现其战略目标,还能提升医疗服…...

UE 蓝图堆栈调试
蓝图打断点后如果想查看断点前的执行逻辑,Tools→Debug→BlueprintDebugger 然后打断点运行,执行顺序是从下往上...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...

华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...

[ACTF2020 新生赛]Include 1(php://filter伪协议)
题目 做法 启动靶机,点进去 点进去 查看URL,有 ?fileflag.php说明存在文件包含,原理是php://filter 协议 当它与包含函数结合时,php://filter流会被当作php文件执行。 用php://filter加编码,能让PHP把文件内容…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准
城市路内停车管理常因行道树遮挡、高位设备盲区等问题,导致车牌识别率低、逃费率高,传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法,正成为破局关键。该设备安装于车位侧方0.5-0.7米高度,直接规避树枝遮…...

uniapp 小程序 学习(一)
利用Hbuilder 创建项目 运行到内置浏览器看效果 下载微信小程序 安装到Hbuilder 下载地址 :开发者工具默认安装 设置服务端口号 在Hbuilder中设置微信小程序 配置 找到运行设置,将微信开发者工具放入到Hbuilder中, 打开后出现 如下 bug 解…...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...