【Spring Boot 源码学习】初识 SpringApplication

Spring Boot 源码学习系列

在这里插入图片描述

初识 SpringApplication

  • 引言
  • 往期内容
  • 主要内容
    • 1. Spring Boot 应用程序的启动
    • 2. SpringApplication 的实例化
      • 2.1 构造方法参数
      • 2.2 Web 应用类型推断
      • 2.3 加载 BootstrapRegistryInitializer
      • 2.4 加载 ApplicationContextInitializer
      • 2.5 加载 ApplicationListener
      • 2.6 推断应用入口类
  • 总结

引言

往期的博文,Huazie 围绕 Spring Boot 的核心功能,带大家从总整体上了解 Spring Boot 自动配置的原理以及自动配置核心组件的运作过程。这些内容大家需要重点关注,只有了解这些基础的组件和功能,我们在后续集成其他三方类库的 Starters 时,才能够更加清晰地了解它们都运用了自动配置的哪些功能。

在学习上述 Spring Boot 核心功能的过程中,相信大家可能都会尝试启动自己新建的 Spring Boot 的项目,并 Debug 看看具体的执行过程。本篇开始就将从 Spring Boot 的启动类 SpringApplication 上入手,带领大家了解 Spring Boot 启动过程中所涉及到的源码和知识点。

在这里插入图片描述

往期内容

在开始本篇的内容介绍之前,我们先来看看往期的系列文章【有需要的朋友,欢迎关注系列专栏】:

Spring Boot 源码学习
Spring Boot 项目介绍
Spring Boot 核心运行原理介绍
【Spring Boot 源码学习】@EnableAutoConfiguration 注解
【Spring Boot 源码学习】@SpringBootApplication 注解
【Spring Boot 源码学习】走近 AutoConfigurationImportSelector
【Spring Boot 源码学习】自动装配流程源码解析(上)
【Spring Boot 源码学习】自动装配流程源码解析(下)
【Spring Boot 源码学习】深入 FilteringSpringBootCondition
【Spring Boot 源码学习】OnClassCondition 详解
【Spring Boot 源码学习】OnBeanCondition 详解
【Spring Boot 源码学习】OnWebApplicationCondition 详解
【Spring Boot 源码学习】@Conditional 条件注解
【Spring Boot 源码学习】HttpEncodingAutoConfiguration 详解
【Spring Boot 源码学习】RedisAutoConfiguration 详解
【Spring Boot 源码学习】JedisConnectionConfiguration 详解

主要内容

1. Spring Boot 应用程序的启动

在 《【Spring Boot 源码学习】@SpringBootApplication 注解》这篇博文中,我们新建了一个基于 Spring Boot 的测试项目。

在这里插入图片描述

如上图中的 DemoApplication 就是我们这里 Spring Boot 项目的入口类。

同时,我们可以看到 DemoApplicationmain 方法中,直接调用了 SpringApplication 的静态方法 run,用于启动整个 Spring Boot 项目。

先来看看 run 方法的源码:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[] { primarySource }, args);
}public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return new SpringApplication(primarySources).run(args);
}

阅读上述 run 方法,我们可以看到实际上是 new 了一个SpringApplication 对象【其构造参数 primarySources 为加载的主要资源类,通常就是 SpringBoot 的入口类】,并调用其 run 方法【其参数 args 为传递给应用程序的参数信息】启动,然后返回一个应用上下文对象 ConfigurableApplicationContext

通过观察这个内部的 run 方法实现,我们也可以在自己的 Spring Boot 启动入口类中,像如下这样去写 :

@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication springApplication = new SpringApplication(DemoApplication.class);// 这里可以调用 SpringApplication 提供的 setXX 或 addXX 方法来定制化设置springApplication.run(args);}}

2. SpringApplication 的实例化

上面已经看到我们在实例化 SpringApplication 了,废话不多说,直接翻看其源码【Spring Boot 2.7.9】:

	public SpringApplication(Class<?>... primarySources) {this(null, primarySources);}@SuppressWarnings({ "unchecked", "rawtypes" })public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 推断web应用类型this.webApplicationType = WebApplicationType.deduceFromClasspath();// 加载并初始化 BootstrapRegistryInitializer及其实现类this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));// 加载并初始化 ApplicationContextInitializer及其实现类setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 加载并初始化ApplicationListener及其实现类setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 推断入口类this.mainApplicationClass = deduceMainApplicationClass();}

由上可知,SpringApplication 提供了两个构造方法,而其核心的逻辑都在第二个构造方法中实现。

2.1 构造方法参数

我们从上述源码可知,SpringApplication 的第二个构造方法有两个参数,分别是:

  • ResourceLoader resourceLoaderResourceLoader 为资源加载的接口,它用于在Spring Boot 启动时打印对应的 banner 信息,默认采用的就是 DefaultResourceLoader。实操过程中,如果未按照 Spring Boot 的 “约定” 将 banner 的内容放置于 classpath 下,或者文件名不是 banner.* 格式,默认资源加载器是无法加载到对应的 banner 信息的,此时则可通过 ResourceLoader 来指定需要加载的文件路径【这个后面我们专门来实操一下,敬请期待】。
  • Class<?>... primarySources :该参数为可变参数,默认我们会传入 Spring Boot 的入口类【即 main 方法所在的类】,如上面我们的 DemoApplication 。如果作为项目的引导类,该类需要满足一个条件,就是被注解 @EnableAutoConfiguration 或其组合注解标注。在前面的《【Spring Boot 源码学习】@SpringBootApplication 注解》博文中,我们已经知道 @SpringBootApplication 注解中包含了 @EnableAutoConfiguration 注解,因此被 @SpringBootApplication 注解标注的类也可作为参数传入。当然,primarySources 也可传入其他普通类,但只有传入被@EnableAutoConfiguration 标注的类才能够开启 Spring Boot 的自动配置。

有些朋友,可能对 primarySources 这个可变参数的描述有点疑惑,下面我们就用实例来演示以其他引导类为入口类进行 Spring Boot 项目启动:

  • 首先,我们在入口类 DemoApplication 的同级目录创建一个SecondApplication类,使用 @SpringBootApplication 进行注解。

    @SpringBootApplication
    public class SecondApplication {
    }
    
  • 然后,将 DemoApplication 修改成如下:

    public class DemoApplication {public static void main(String[] args) {SpringApplication.run(SecondApplication.class, args);}
    }
    
  • 最后,我们来运行 DemoApplicationmain 方法。
    在这里插入图片描述
    从上图可以看出,我们的应用依然能正常启动,并完成自动配置。因此,决定 Spring Boot 启动的入口类并不是一定是 main 方法所在类,而是直接或间接被 @EnableAutoConfiguration 标注的类。

翻看 SpringApplication 的源码,我们在其中还能看到它提供了追加 primarySources 的方法,如下所示:

public void addPrimarySources(Collection<Class<?>> additionalPrimarySources) {this.primarySources.addAll(additionalPrimarySources);
}

如果采用 1 中最后的方式启动 Spring Boot ,我们就可以调用 addPrimarySources 方法来追加额外的 primarySources

我们继续回到 SpringApplication 的构造方法里,可以看到如下的代码:

this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

上述这里将 primarySources 参数转换为 LinkedHashSet 集合,并赋值给SpringApplication 的私有成员变量 Set<Class<?>> primarySources

知识点: LinkedHashSetJava 集合框架中的类,它继承自 HashSet,因此具有哈希表的查找性能。这是一个同时使用链表和哈希表特性的数据结构,其中链表用于维护元素的插入顺序。也即是说,当你向 LinkedHashSet 添加元素时,元素将按照添加的顺序被存储,并且能够被遍历输出。
此外,LinkedHashSet 还确保了 元素的唯一性,即重复的元素在集合中只会存在一份
如果需要频繁遍历集合,那么 LinkedHashSet 可能会比 HashSet 效率更高,因为其通过维护一个双向链表来记录元素的添加顺序,从而支持按照插入顺序排序的迭代。但需要注意的是,LinkedHashSet 是非线程安全的,如果有多个线程同时访问该集合容器,可能会引发并发问题。

2.2 Web 应用类型推断

我们继续往下翻看源码,这里调用了 WebApplicationTypededuceFromClasspath 方法来进行 Web 应用类型的推断。

this.webApplicationType = WebApplicationType.deduceFromClasspath();

我们继续翻看 WebApplicationType 的源码:

public enum WebApplicationType {// 非Web应用类型NONE,// 基于Servlet的Web应用类型SERVLET,// 基于reactive的Web应用类型REACTIVE;private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext" };private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";static WebApplicationType deduceFromClasspath() {if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {return WebApplicationType.REACTIVE;}for (String className : SERVLET_INDICATOR_CLASSES) {if (!ClassUtils.isPresent(className, null)) {return WebApplicationType.NONE;}}return WebApplicationType.SERVLET;}
}

WebApplicationType 是一个定义了可能的Web应用类型的枚举类,该枚举类中包含了三块逻辑:

  • 枚举类型 :非 Web 应用、基于 ServletWeb 应用和基于 reactiveWeb 应用。
  • 用于下面推断的常量
  • 推断类型的方法 deduceFromClasspath
    • DispatcherHandler 存在,并且 DispatcherServletServletContainer 都不存在,则返回类型为 WebApplicationType.REACTIVE
    • ServletConfigurableWebApplicationContext 任何一个不存在时,则说明当前应用为非 Web 应用,返回 WebApplicationType.NONE
    • 当应用不为 reactive Web 应用,并且 ServletConfigurableWebApplicationContext 都存在的情况下,则返回 WebApplicationType.SERVLET

在上述的 deduceFromClasspath 方法中,我们可以看到,在判断的过程中使用到了 ClassUtilsisPresent 方法。该工具类方法就是通过反射创建指定的类,根据在创建过程中是否抛出异常来判断该类是否存在。

在这里插入图片描述

2.3 加载 BootstrapRegistryInitializer

this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));

上述逻辑用于加载并初始化 BootstrapRegistryInitializer 及其相关的类。

BootstrapRegistryInitializerSpring Cloud Config 的组件之一,它的作用是在应用程序启动时初始化 Spring Cloud Config 客户端。

Spring Cloud Config 中,客户端通过向配置中心(Config Server)发送请求来获取应用程序的配置信息。而 BootstrapRegistryInitializer 就是负责将配置中心的相关信息注册到 Spring 容器中的。

由于篇幅有限,有关 BootstrapRegistryInitializer 更详细的内容,笔者后续专门讲解。

2.4 加载 ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

上述代码用于加载并初始化 ApplicationContextInitializer 及其相关的类。

ApplicationContextInitializerSpring 框架中的一个接口,它的主要作用是在Spring容器刷新之前初始化 ConfigurableApplicationContext。这个接口的实现类可以被视为回调函数,它们的 onApplicationEvent 方法会在Spring 容器启动时被自动调用,从而允许开发人员在容器刷新之前执行一些自定义的操作。

例如,我们可能需要在这个时刻加载一些配置信息,或者对某些 bean 进行预处理等。通过实现 ApplicationContextInitializer 接口并重写其onApplicationEvent 方法,就可以完成这些定制化的需求。

由于篇幅有限,有关 ApplicationContextInitializer 更详细的内容,笔者后续专门讲解。

2.5 加载 ApplicationListener

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

上述代码用于加载并初始化 ApplicationListener 及其相关的类。

ApplicationListenerSpring 框架提供的一个事件监听机制,它是Spring 应用内部的事件驱动机制,通常被用于监控应用内部的运行状况。其实现的原理是 观察者设计模式,该设计模式的初衷是为了实现系统业务逻辑之间的解耦,从而提升系统的可扩展性和可维护性。

我们可以通过自定义一个类来实现 ApplicationListener 接口,然后在这个类中定义需要监听的事件处理方法。当被监听的事件发生时,Spring 会自动调用这个方法来处理事件。例如,在一个 Spring Boot 项目中,我们可能想要在容器启动时执行一些特定的操作,如加载配置等,就可以通过实现 ApplicationListener 接口来完成。

由于篇幅有限,有关 ApplicationListener 更详细的内容,笔者后续专门讲解。

2.6 推断应用入口类

最后一步,调用 SpringApplicationdeduceMainApplicationClass 方法来进行入口类的推断:

private Class<?> deduceMainApplicationClass() {try {StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();for (StackTraceElement stackTraceElement : stackTrace) {if ("main".equals(stackTraceElement.getMethodName())) {return Class.forName(stackTraceElement.getClassName());}}} catch (ClassNotFoundException ex) {// 这里捕获异常,并继续执行后续逻辑}return null;
}

上述代码的思路就是:

  • 首先,创建一个运行时异常,并获得其堆栈数组。
  • 接着,遍历数组,判断类的方法中是否包含 main 方法。第一个被匹配的类会通过Class.forName 方法创建对象,并将其被返回。
  • 最后,将上述创建的 Class 对象赋值给 SpringApplication 的成员变量 mainApplicationClass

总结

本篇 Huazie 带大家初步了解了 SpringApplication 的实例化过程,当然由于篇幅受限,还有些内容暂时无法详解,Huazie 将在后续的博文中继续深入分析。

只有了解 Spring Boot 在启动时都做了些什么,我们才能在后续的实践的过程中更好地理解其运行机制,以便遇到问题能更快地定位和排查,使我们应用能够更容易、更方便地接入 Spring Boot

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/189759.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Codeforces Round 788 (Div. 2) E. Hemose on the Tree(树上构造)

题目 t(t<5e4)组样例&#xff0c;每次给定一个数p&#xff0c; 表示一棵节点数为的树&#xff0c; 以下n-1条边&#xff0c;读入树边 对于n个点和n-1条边&#xff0c;每个点需要赋权&#xff0c;每条边需要赋权&#xff0c; 权值需要恰好构成[1,2n-1]的排列 并且当你赋…

阿里云ACK(Serverless)安装APISIX网关及APISIX Ingress Controller

在k8s上安装apisix全家&#xff0c;通过helm安装很简单&#xff0c;但是会遇到一些问题。 安装 首先登录阿里云控制台&#xff0c;在ACK集群详情页&#xff0c;进入CloudShell&#xff0c;执行下面helm命令安装apisix、apisix-ectd、apisix-dashboard和apisix-ingress-contro…

汽车ECU的虚拟化技术初探(一)

目录 1.为什么要提汽车ECU的虚拟化&#xff1f; 2.虚拟化技术分类 2.1 硬件虚拟化 2.2 操作系统虚拟化 问题引入&#xff1a; Hypervisor是如何来管理和隔离硬件资源&#xff0c;保证各个不同功能的应用程序的资源使用安全和资源调度&#xff1f;没有MMU就做不了虚拟化&am…

扭矩传感器信号模拟地、数据地与电源地

在电子电路中&#xff0c;电源地、信号地、数字地和模拟地都是不同的地&#xff08;ground&#xff09;节点&#xff0c;它们在电路中有不同的作用。 电源地&#xff08;Power Ground&#xff09;是指用于连接电源电源回路的地节点。在大多数电子设备中&#xff0c;电源地通常是…

Git Commit 之道:规范化 Commit Message 写作指南

1 commit message 规范 commit message格式都包括三部分&#xff1a;Header&#xff0c;Body和Footer <type>(<scope>): <subject><body><footer>Header是必需的&#xff0c;Body和Footer则可以省略 1.1 Header Type&#xff08;必需&#xf…

高级项目管理总结

目录 一、背景介绍二、思路&方案三、过程1.升维思考2.结构化3.心理、知识阶段检验4.微观 四、总结 一、背景介绍 天性对学习对考试充满敌意的我&#xff0c;转变为依赖学习谋生&#xff0c;再到后来书中自有黄金屋&#xff0c;到现在学习对我而言就如同一日三餐&#xff1…

Three.js——基于原生WebGL封装运行的三维引擎

文章目录 前言一、什么是WebGL&#xff1f;二、Three.js 特性 前言 Three.js中文官网 Three.js是基于原生WebGL封装运行的三维引擎&#xff0c;在所有WebGL引擎中&#xff0c;Three.js是国内文资料最多、使用最广泛的三维引擎。既然Threejs是一款WebGL三维引擎&#xff0c;那么…

【操作系统】考研真题攻克与重点知识点剖析 - 第 3 篇:内存管理

前言 本文基础知识部分来自于b站&#xff1a;分享笔记的好人儿的思维导图与王道考研课程&#xff0c;感谢大佬的开源精神&#xff0c;习题来自老师划的重点以及考研真题。此前我尝试了完全使用Python或是结合大语言模型对考研真题进行数据清洗与可视化分析&#xff0c;本人技术…

JPA Buddy快速创建update、find、count、delete、exists方法

JPA Buddy快速创建update、find、count、delete、exists方法&#xff0c;JPA默认提供的CrudRepository\JpaRepository提供的方法比较少&#xff0c;一般我们会手写一些方法&#xff0c;这里我们选择通过JPA Buddy快速生成&#xff0c;之前文章中讲到了JPA Buddy原本是IDEA收费插…

Spark Job优化

1 Map端优化 1.1 Map端聚合 map-side预聚合&#xff0c;就是在每个节点本地对相同的key进行一次聚合操作&#xff0c;类似于MapReduce中的本地combiner。map-side预聚合之后&#xff0c;每个节点本地就只会有一条相同的key&#xff0c;因为多条相同的key都被聚合起来了。其他节…

前端面试题 计算机网络

文章目录 ios 7层协议tcp协议和udp协议的区别tcp协议如何确保数据的可靠http和tcp的关系url输入地址到呈现网页有哪些步骤post和get本质区别&#xff0c;什么时候会触发二次预检GET请求&#xff1a;POST请求&#xff1a;触发二次预检&#xff08;CORS中的预检请求&#xff09;&…

SolidWorks绘制花瓶教程

这个花瓶是我学习solidworks画图以来用时最长的一个图形了&#xff0c;特此记录一下&#xff0c;用了我足足两个早晨才把他给画出来&#xff0c;我这是跟着哔站里的隔壁老王学习的&#xff0c;下面是视频地址&#xff1a;点击我一下看视频教程 下面是我的绘图过程&#xff0c;…

美国材料与试验协会ASTM发布新版玩具安全标准 ASTM F963-23

美国材料与试验协会ASTM发布新版玩具安全标准 ASTM F963-23 2023年10月13日&#xff0c;美国材料与试验协会&#xff08;ASTM&#xff09;发布了新版玩具安全标准ASTM F963-23 ​根据CPSIA的规定&#xff0c;当ASTM将ASTM F963的拟定修订意见通知CPSC时&#xff0c;若CPSC认为…

如何配置Apple推送证书 push证书

转载&#xff1a;如何配置Apple推送证书 push证书 想要制作push证书&#xff0c;就需要使用快捷工具appuploader工具制 作证书&#xff0c;然后使用Apple的推送功能配置push证书&#xff0c;就可以得到了。PS&#xff1a;push没有描述文件&#xff0c;所以不 要问推送选择哪…

【c++随笔12】继承

【c随笔12】继承 一、继承1、继承的概念2、3种继承方式3、父类和子类对象赋值转换4、继承中的作用域——隐藏5、继承与友元6、继承与静态成员 二、继承和子类默认成员函数1、子类构造函数 二、子类拷贝构造函数3、子类的赋值重载4、子类析构函数 三、单继承、多继承、菱形继承1…

视觉大模型DINOv2:自我监督学习的新领域

1 DINOv2 1.1 DINOv2特点 前段时间&#xff0c;Meta AI 高调发布了 Segment Anything&#xff08;SAM&#xff09;&#xff0c;SAM 以交互式方式快速生成 Mask&#xff0c;并可以对从未训练过的图片进行精准分割&#xff0c;可以根据文字提示或使用者点击进而圈出图像中的特定…

Flink SQL 表值聚合函数(Table Aggregate Function)详解

使用场景&#xff1a; 表值聚合函数即 UDTAF&#xff0c;这个函数⽬前只能在 Table API 中使⽤&#xff0c;不能在 SQL API 中使⽤。 函数功能&#xff1a; 在 SQL 表达式中&#xff0c;如果想对数据先分组再进⾏聚合取值&#xff1a; select max(xxx) from source_table gr…

第24章_mysql性能分析工具的使用

文章目录 1. 数据库服务器的优化步骤2.查看系统性能参数3. 统计SQL的查询成本&#xff1a;last_query_cost4. 定位执行慢的 SQL&#xff1a;慢查询日志4.1 开启慢查询日志参数4.2 查看慢查询数目4.3 测试慢sql语句&#xff0c;查看慢日志4.4 系统变量 log_output&#xff0c; l…

用excel计算一个矩阵的转置矩阵

假设我们的原矩阵是一个3*3的矩阵&#xff1a; 125346789 现在求它的转置矩阵&#xff1a; 鼠标点到一个空白的地方&#xff0c;用来存放结果&#xff1a; 插入-》函数&#xff1a; 选择TRANSPOSE&#xff0c;这个就是求转置矩阵的函数&#xff1a; 点击“继续”&#xff1a…

Windows查看端口占用情况

Windows如何查看端口占用情况 方法1. cmd命令行执行netstat命令&#xff0c;查看端口占用情况 netstat -ano 以上命令输出太多信息&#xff0c;不方便查看&#xff0c;通过如下命令搜索具体端口占用情况&#xff0c;例如&#xff1a;8080端口 netstat -ano | findstr "…