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的内部,会做两件事:

22f585c35f4a46ebafe90c5b4b7475d5.png

        创建SpringApplication对象:

522f66ff590f4edb95edb2c1ed367f2d.png

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

26c58a68365d45c6bc980068190f425d.png

        在创建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是一个枚举类:

6138abe362cf4ef8bb9c826bffeb8cbe.png

        简单说明一下这段代码:第一个if判断,如果是reactive.DispatcherHandler,并且不是servlet.DispatcherServlet和servlet.ServletContainer,就推断类型为REACTIVE并返回。

        如果上面的条件不成立,也就是应用类型没有被推断为REACTIVE,就会循环成员变量的SERVLET_INDICATOR_CLASSES数组,该数组中有两个元素:

  • javax.servlet.Servlet
  • org.springframework.web.context.ConfigurableWebApplicationContext

        如果任意一个元素不存在就推断类型为NONE并返回。

        最后推论类型为SERVLET并返回

2a71cef044264a848d37e31994170090.png

1.3、ApplicationContext初始化器

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

        获取所有注册的 ApplicationContextInitializer实例,并将其设置为应用程序的初始化器。

        通过SpringApplication实例的.addInitializers()方法注册初始化器

springApplication.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
            @Override
            public 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>() {
            @Override
            public void onApplicationEvent(ApplicationEvent event) {
                System.out.println("事件:"+event.getClass());
            }
        });

1.5、主类推断

        this.mainApplicationClass = deduceMainApplicationClass();

        调用deduceMainApplicationClass() 方法进行主类推论:

  • 通过创建一个新的 RuntimeException对象来获取当前线程的堆栈轨迹(stack trace),即调用堆栈信息。
  • 遍历堆栈信息,找到方法名为"main"的函数,并返回类的class对象
  • 没有找到就抛出异常

85874401b9f444c5bfb539ec77c0bfc4.png

2、Spring Boot 执行流程-run

2.1、事件发布器

           d910d04b858341c3afc678c13a7bb17a.png

        在run方法中,首先会通过getRunListeners(args);得到 SpringApplicationRunListeners。

        SpringApplicationRunListeners是一个事件发布器,会在run方法的执行不同阶段中发布对应的事件:

  1. 得到事件发布器后,会发布listeners.starting(); 事件,代表spring boot 开始启动。
  2. prepareEnvironment(listeners, applicationArguments);方法执行时,会发布listeners.environmentPrepared(environment); 事件,代表环境信息准备完成
  3. prepareContext(context, environment, listeners, applicationArguments, printedBanner); 方法执行时,会发布listeners.contextPrepared(context);事件,代表spring容器创建,但未调用初始化器。listeners.contextLoaded(context);事件,代表所有bean definition加载完毕。(然后会调用context.refresh();方法
  4. 在调用context.refresh();方法后,会发布listeners.started(context);事件
  5. 如果在这个过程中发生了异常,会发布listeners.failed(context, exception); 事件
  6. 最后会发布listeners.running(context);事件,代表spring boot 启动完成

        在第六点中,如果在发布listeners.running(context);事件的过程中出现异常,不会发布listeners.failed(context, exception); 事件:

ef5f39e6433548668611e340c89488a5.png

260a99e12c0d49f19a260fa1ebdce8cf.png       

        我们也可以模拟一下上述过程:

        事件发布器SpringApplicationRunListener是一个接口,其与子类的对应关系是放在spring.factories的配置文件中:

42315fec479047b9bc7f85d29aca3fbe.png

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>() {
            @Override
            public 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启动时运行其中的逻辑)

9bf857101cd944539bf5d2d606b3a81f.png


2.3、创建环境对象

        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); 用于创建环境对象:

e376894743bd424e9163effe090efcf5.png        getOrCreateEnvironment()方法用于根据不同的webApplication类型,去创建对应的环境对象。(webApplication类型是在构造SpringApplication时推论出的)

9e91e62e88024d188904a6b782449028.png

        configureEnvironment(ConfigurableEnvironment environment, String[] args) 方法用于配置应用程序的环境:

        environment.setConversionService(new ApplicationConversionService()); 会先添加转换器:

40534477df4745289a9c6994213a4d3e.png

f63d3f39dbfe483281db883c7926bed0.png

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

0b3c803e4d284264bd02b3e32a542e07.png

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

        首先从环境中获取systemProperties和systemEnvironment

dc1a19647ac64696a2f0506c23c4e158.png

3449b0ed3b7b4b5da20df4b42007ad70.png


2.4、配置文件读取统一命名处理

       prepareEnvironment中的 ConfigurationPropertySources.attach(standardEnvironment); 方法用于统一命名处理:

8497702d5a0c4b5e860b898ae15ed00a.png

        因为配置文件在读取的过程中可能会存在这样的问题:

        我们定义了一个配置文件,每个键的格式都不一样:

b720eb3e16574bdb921be32276e0af3c.png

        当我们不做任何处理,在读取时键名统一使用"-"分隔时,只能读取到第一个的值,因为命名和文件中不匹配。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的头部,以便优先被访问

b33e9df412fb463393ad611d00d93ebf.png


2.5、EnvironmentPostProcessor功能扩展

        EnvironmentPostProcessor相当于环境对象的后处理器,可以增加一些扩展。

        prepareEnvironment 中的listeners.environmentPrepared(bootstrapContext, environment);

使用事件发布器去触发监听器,调用其中的后处理器方法:(监听器是在SpringApplication构造时加入的,但是需要等到环境对象创建后才应该触发其中的那些后处理器

       通过debug发现,在构造SpringApplication时已经加入了对于环境对象后处理器的监听器

52a53295ade24a8d8154ef57b0998042.png

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

5e09a9ac00824e73af908f9dbbedfb21.png

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

453066fbdfa843d4bfc88b018d877fc0.png

c9e97b50c672451ab5e10409d4ba5701.png

75d5ff5f732f436580b4209551be4ab2.png


 

2.6、将环境中的键值和SpringApplication中的属性匹配

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

b72a60d4c0734658b1a47401c328577f.png

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

668d5b0a1d8f4840a48afec04482dd7f.png

4a42c00700e84074bee890781ffddcce.png

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

1f5e6f021ce84c8aa0ecefec7bba7eef.png

87f3dfc3caa34d6ea389ed9da07c90fc.png


2.7、输出Banner图标

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

96d6d075fcfa4252b369cd24ddd58a03.png

ea69ad024c624ec2aac5844c64f39e4f.png


2.8、创建容器

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

73e6687c718e421dae4e6c562e517ab4.png 
052d38d3bcb34ee8a01faac7d2d02884.png

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

98d326d1f8ae427792e6a51bb080e6b4.png


 

2.9、准备容器

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

a04c9dc70e894af58e22444cdf4185d4.png

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

f64f9744db2a4f03b5e586c2140a9313.png

c39e4ee7dcac4945aa4a2e84f139c9ef.png


2.10、加载Bean定义

         prepareContext方法中的load(context, sources.toArray(new Object[0]));

0e0ea6d492074fd28726bb5348fb6600.png

        这段代码大致的意思是,加载应用程序的配置源,并根据需要设置相关的配置,然后执行加载操作。
46bf029bf9434c9c9e38f0c03fbb10cb.png

        根据不同的配置类型去分派对应的加载方法:

c9cba4538722446a8bced548ab59e1f0.png


2.11、refresh

        执行run中的refreshContext()方法:

c4f2634a924c4c5c954fae84e10eb03b.png

        调用refresh方法,加载配置、初始化所有单例 、重新启动应用程序上下文。

        在这之前还会判断,如果this.registerShutdownHook为true,那么会通过 shutdownHook.registerApplicationContext(context);方法为当前的应用程序上下文注册一个关机钩子(shutdown hook)这个钩子用于在应用程序关闭时执行一些清理操作或释放资源。

3df94684cea24c13b993d61c0e75d857.png


2.12、执行初始化方法

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

06142929780743eda6f48148429ef2b2.png

        “2.2、封装args参数”的作用就体现在此:

        ApplicationRunner需要将args参数包装成为ApplicationArguments类型:

1bbd14248de84524bf1366a9f80f3c96.png        虽然无论是ApplicationRunner还是CommandLineRunner,调用callRunner() 方法时传递的args参数类型都是ApplicationArguments。但是不代表CommandLineRunner需要ApplicationArguments参数类型:

        会取出原本的String args 参数

fb79392faa1b4806adfd38ddf229c765.png

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(一)Linux的vim编辑器的使用

一.vim编辑器 Vim 是从 vi 发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。简单的来说, vi 是老式的字处理器,不过功能已经很齐全了,但是还是有可以进步的地方。 vim 则可以说是程序开发者的一项很好用的工具。 二…

关于勒索攻击,绝大多数企业存在的三个认知误区

网络空间&#xff0c;有一个挥之不去的“幽灵”&#xff0c;它的名字就叫勒索攻击。 近年来&#xff0c;企业遭受勒索攻击的事件被频频曝光。就在不久前&#xff0c;国家安全部曝光了一起境外黑客组织对我国某高新科技企业实施勒索攻击的案例&#xff0c;该企业的相关信息化系统…

Window7镜像注入USB驱动,解决系统安装后无法识别USB

Window7镜像注入usb驱动 Window7镜像注入usb驱动方法一方法二 Window7镜像注入usb驱动 一般4代酷睿之后的主机需要安装usb驱动才能驱动usb&#xff0c;导致很多Windows原版镜像安装后无法识别usb键盘 方法一 1.直接采购PS2 接口键盘、PS2 接口鼠标 方法二 使用联想镜像注入…

光峰科技2023年营收、净利润均双位数下滑,新一年延续?

近日&#xff0c;深圳光峰科技股份有限公司&#xff08;688007.SH&#xff0c;下称“光峰科技”&#xff09;对外公布了2023年和2024年一季度的经营“成绩单”。 透视财报不难看出&#xff0c;虽然光峰科技在降低成本、提振销售等层面下足了功夫&#xff0c;但受制于市场需求式…

测试项目实战——安享理财1(测试用例)

说明&#xff1a; 1.访问地址&#xff1a; 本项目实战使用的是传智播客的安享理财项目&#xff08;找了半天这个项目能免费用且能够满足测试实战需求&#xff09; 前台&#xff1a;http://121.43.169.97:8081/ 后台&#xff1a;http://121.43.169.97:8082/ &#xff08;点赞收藏…

Git泄露(CTFHUB的git泄露)

log 当dirsearch 扫描一下&#xff0c;命令&#xff1a; python dirsearch.py -u url/.git 发现存在了git泄露 借助kali里面&#xff0c;打开GitHack所在的目录&#xff0c;然后 输入&#xff1a; python2 GitHack.py -u url/.git/ 必须要用Python2 tree 命令 可以看到…

Paddle 基于ANN(全连接神经网络)的GAN(生成对抗网络)实现

什么是GAN GAN是生成对抗网络&#xff0c;将会根据一个随机向量&#xff0c;实现数据的生成&#xff08;如生成手写数字、生成文本等&#xff09;。 GAN的训练过程中&#xff0c;需要有一个生成器G和一个鉴别器D. 生成器用于生成数据&#xff0c;鉴定器用于鉴定数据的准确性&…

车载测试___面试题和答案归纳

车载面试题 一、实车还在设计开发阶段&#xff0c;大部分测试通过什么测试&#xff1f; 答案&#xff1a;通过台架和仿真来完成的 二、测试部分划分&#xff1f; 测试部门是分为自研&#xff0c;系统&#xff0c;验收&#xff0c;自研部门是开发阶段测试&#xff0c;系统部门…

95、动态规划-编辑距离

递归暴力解法 递归方法的基本思想是考虑最后一个字符的操作&#xff0c;然后根据这些操作递归处理子问题。 递归函数定义&#xff1a;定义一个递归函数 minDistance(i, j)&#xff0c;表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最小操作数。 递归终止条件…

llama.cpp制作GGUF文件及使用

llama.cpp的介绍 llama.cpp是一个开源项目&#xff0c;由Georgi Gerganov开发&#xff0c;旨在提供一个高性能的推理工具&#xff0c;专为在各种硬件平台上运行大型语言模型&#xff08;LLMs&#xff09;而设计。这个项目的重点在于优化推理过程中的性能问题&#xff0c;特别是…

(七)JSP教程——session对象

浏览器和Web服务器之间的交互通过HTTP协议来完成&#xff0c;HTTP协议是一种无状态的协议&#xff0c;服务器端无法保留浏览器每次与服务器的连接信息&#xff0c;无法判断每次连接的是否为同一客户端。为了让服务器端记住客户端的连接信息&#xff0c;可以使用session对象来记…

Java毕设之基于springboot的医护人员排班系统

运行环境 开发语言:java 框架:springboot&#xff0c;vue JDK版本:JDK1.8 数据库:mysql5.7(推荐5.7&#xff0c;8.0也可以) 数据库工具:Navicat11 开发软件:idea/eclipse(推荐idea) 系统详细实现 医护类型管理 医护人员排班系统的系统管理员可以对医护类型添加修改删除以及…

Error: error:0308010C:digital envelope routines::unsupported 问题如何解决

Error: error:0308010C:digital envelope routines::unsupported 通常与 Node.js 的加密库中对某些加密算法的支持有关。这个错误可能是因为 Node.js 的版本与某些依赖库不兼容导致的。特别是在 Node.js 17 版本中&#xff0c;默认使用 OpenSSL 3&#xff0c;而一些旧的加密方式…

电商核心技术揭秘53:社群营销的策略与实施

相关系列文章 电商技术揭秘相关系列文章合集&#xff08;1&#xff09; 电商技术揭秘相关系列文章合集&#xff08;2&#xff09; 电商技术揭秘相关系列文章合集&#xff08;3&#xff09; 电商技术揭秘四十一&#xff1a;电商平台的营销系统浅析 电商技术揭秘四十二&#…

Python轻量级Web框架Flask(13)—— Flask个人博客项目

0、前言: ★这部分内容是基于之前Flask学习内容的一个实战项目梳理内容,没有可以直接抄下来跑的代码,是学习了之前Flask基础知识之后,再来看这部分内容,就会对Flask项目开发流程有更清楚的认知,对一些开发细节可以进一步的学习。项目功能,通过Flask制作个人博客。项目架…

YOLOv9中模块总结补充|RepNCSPELAN4详图

专栏地址&#xff1a;目前售价售价69.9&#xff0c;改进点70 专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 1. RepNCSPELAN4详图 RepNCSPELAN4是YOLOv9中的特征提取-融合模块&#xff0c;类似前几…

制造业的智慧进化:机器学习与人工智能的全方位渗透

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

libcity 笔记:添加自定义dataset

假设我们把libcity/data/dataset/trajectory_dataset.py复制一份到libcity/data/dataset/dataset_subclass/GeolifeDM_dataset.py&#xff0c;里面内容不变&#xff0c;只是把class的名字换了 那其他需要修改哪些内容&#xff0c;使得这个dataset生效呢 libcity/data/dataset/d…

国内唯一!阿里云荣膺MongoDB“2024年度DBaaS认证合作伙伴奖”

近日&#xff0c;在MongoDB用户大会纽约站上&#xff0c;阿里云荣膺MongoDB“2024年度DBaaS认证合作伙伴奖”。这是阿里云连续第五年斩获MongoDB合作伙伴奖项&#xff0c;也是唯一获此殊荣的中国云厂商。 MongoDB是当今全球最受欢迎的非关系型数据库之一。凭借灵活的模式和丰富…

Web3探索加密世界:如何避免限制并增加空投成功的几率

今天分享空投如何避免限制以提高效率&#xff0c;增加成功几率&#xff0c;首先我们来了解什么是空投加密&#xff0c;有哪些空投类型。 一、什么是空投加密&#xff1f; 加密货币空投是一种营销策略&#xff0c;包括向用户的钱包地址发送免费的硬币或代币。 加密货币项目使用…
最新文章