18张图,详解SpringBoot解析yml全流程
原创:微信公众号
码农参上
,欢迎分享,转载请保留出处。
前几天的时候,项目里有一个需求,需要一个开关控制代码中是否执行一段逻辑,于是理所当然的在yml
文件中配置了一个属性作为开关,再配合nacos
就可以随时改变这个值达到我们的目的,yml文件中是这样写的:
switch:
turnOn: on
程序中的代码也很简单,大致的逻辑就是下面这样,如果取到的开关字段是on
的话,那么就执行if
判断中的代码,否则就不执行:
@Value("${switch.turnOn}")
private String on;
@GetMapping("testn")
public void test(){
if ("on".equals(on)){
//TODO
}
}
但是当代码实际跑起来,有意思的地方来了,我们发现判断中的代码一直不会被执行,直到debug一下,才发现这里的取到的值居然不是on
而是true
。
看到这,是不是感觉有点意思,首先盲猜是在解析yml的过程中把on
作为一个特殊的值进行了处理,于是我干脆再多测试了几个例子,把yml中的属性扩展到下面这些:
switch:
turnOn: on
turnOff: off
turnOn2: 'on'
turnOff2: 'off'
再执行一下代码,看一下映射后的值:
可以看到,yml中没有带引号的on
和off
被转换成了true
和false
,带引号的则保持了原来的值不发生改变。
到这里,让我忍不住有点好奇,为什么会发生这种现象呢?于是强忍着困意翻了翻源码,硬磕了一下SpringBoot加载yml配置文件的过程,终于让我看出了点门道,下面我们一点一点细说!
因为配置文件的加载会涉及到一些SpringBoot启动的相关知识,所以如果对SpringBoot启动不是很熟悉的同学,可以先提前先看一下Hydra在古早时期写过一篇Spring Boot零配置启动原理预热一下。下面的介绍中,只会摘出一些对加载和解析配置文件比较重要的步骤进行分析,对其他无关部分进行了省略。
加载监听器
当我们启动一个SpringBoot程序,在执行SpringApplication.run()
的时候,首先在初始化SpringApplication
的过程中,加载了11个实现了ApplicationListener
接口的拦截器。
这11个自动加载的ApplicationListener
,是在spring.factories
中定义并通过SPI
扩展被加载的:
这里列出的10个是在spring-boot
中加载的,还有剩余的1个是在spring-boot-autoconfigure
中加载的。其中最关键的就是ConfigFileApplicationListener
,它和后面要讲到的配置文件的加载相关。
执行run方法
在实例化完成SpringApplication
后,会接着往下执行它的run
方法。
可以看到,这里通过getRunListeners
方法获取的SpringApplicationRunListeners
中,EventPublishingRunListener
绑定了我们前面加载的11个监听器。但是在执行starting
方法时,根据类型进行了过滤,最终实际只执行了4个监听器的onApplicationEvent
方法,并没有我们希望看到的ConfigFileApplicationListener
,让我们接着往下看。
当run
方法执行到prepareEnvironment
时,会创建一个ApplicationEnvironmentPreparedEvent
类型的事件,并广播出去。这时所有的监听器中,有7个会监听到这个事件,之后会分别调用它们的onApplicationEvent
方法,其中就有了我们心心念念的ConfigFileApplicationListener
,接下来让我们看看它的onApplicationEvent
方法中做了什么。
在方法的调用过程中,会加载系统自己的4个后置处理器以及ConfigFileApplicationListener
自身,一共5个后置处理器,并执行他们的postProcessEnvironment
方法,其他4个对我们不重要可以略过,最终比较关键的步骤是创建Loader
实例并调用它的load
方法。
加载配置文件
这里的Loader
是ConfigFileApplicationListener
的一个内部类,看一下Loader
对象实例化的过程:
在实例化Loader
对象的过程中,再次通过SPI扩展的方式加载了两个属性文件加载器,其中的YamlPropertySourceLoader
就和后面的yml文件的加载、解析密切关联,而另一个PropertiesPropertySourceLoader
则负责properties
文件的加载。创建完Loader
实例后,接下来会调用它的load
方法。
在load
方法中,会通过嵌套循环方式遍历默认配置文件存放路径,再加上默认的配置文件名称、以及不同配置文件加载器对应解析的后缀名,最终找到我们的yml配置文件。接下来,开始执行loadForFileExtension
方法。
在loadForFileExtension
方法中,首先将classpath:/application.yml
加载为Resource
文件,接下来准备正式开始,调用了之前创建好的YamlPropertySourceLoader
对象的load
方法。
封装Node
在load
方法中,开始准备进行配置文件的解析与数据封装:
load
方法中调用了OriginTrackedYmlLoader
对象的load
方法,从字面意思上我们也可以理解,它的用途是原始追踪yml的加载器。中间一连串的方法调用可以忽略,直接看最后也是最重要的是一步,调用OriginTrackingConstructor
对象的getData
接口,来解析yml并封装成对象。
在解析yml的过程中实际使用了Composer
构建器来生成节点,在它的getNode
方法中,通过解析器事件来创建节点。通常来说,它会将yml中的一组数据封装成一个MappingNode
节点,它的内部实际上是一个NodeTuple
组成的List
,NodeTuple
和Map
的结构类似,由一对对应的keyNode
和valueNode
构成,结构如下:
好了,让我们再回到上面的那张方法调用流程图,它是根据文章开头的yml文件中实际内容内容绘制的,如果内容不同调用流程会发生改变,大家只需要明白这个原理,下面我们具体分析。
首先,创建一个MappingNode
节点,并将switch
封装成keyNode
,然后再创建一个MappingNode
,作为外层MappingNode
的valueNode
,同时存储它下面的4组属性,这也是为什么上面会出现4次循环的原因。如果有点困惑也没关系,看一下下面的这张图,就能一目了然了解它的结构。
在上图中,又引入了一种新的ScalarNode
节点,它的用途也比较简单,简单String类型的字符串用它来封装成节点就可以了。到这里,yml中的数据被解析完成并完成了初步的封装,可能眼尖的小伙伴要问了,上面这张图中为什么在ScalarNode
中,除了value
还有一个tag
属性,这个属性是干什么的呢?
在介绍它的作用前,先说一下它是怎么被确定的。这一块的逻辑比较复杂,大家可以翻一下ScannerImpl
类fetchMoreTokens
方法的源码,这个方法会根据yml中每一个key
或value
是以什么开头,来决定以什么方式进行解析,其中就包括了{
、[
、'
、%
、?
等特殊符号的情况。以解析不带任何特殊字符的字符串为例,简要的流程如下,省略了一些不重要部分:
在这张图的中间步骤中,创建了两个比较重要的对象ScalarToken
和ScalarEvent
,其中都有一个为true
的plain
属性,可以理解为这个属性是否需要解释,是后面获取Resolver
的关键属性之一。
上图中的yamlImplicitResolvers
其实是一个提前缓存好的HashMap,已经提前存储好了一些Char
类型字符与ResolverTuple
的对应关系:
当解析到属性on
时,取出首字母o
对应的ResolverTuple
,其中的tag
就是tag:yaml.org.2002:bool
。当然了,这里也不是简单的取出就完事了,后续还会对属性进行正则表达式的匹配,看与regexp
中的值是否能对的上,检查无误时才会返回这个tag
。
到这里,我们就解释清楚了ScalarNode
中tag
属性究竟是怎么获取到的了,之后方法调用层层返回,返回到OriginTrackingConstructor
父类BaseConstructor
的getData
方法中。接下来,继续执行constructDocument
方法,完成对yml文档的解析。
调用构造器
在constructDocument
中,有两步比较重要,第一步是推断当前节点应该使用哪种类型的构造器,第二步是使用获得的构造器来重新对Node
节点中的value
进行赋值,简易流程如下,省去了循环遍历的部分:
推断构造器种类的过程也很简单,在父类BaseConstructor
中,缓存了一个HashMap,存放了节点的tag
类型到对应构造器的映射关系。在getConstructor
方法中,就使用之前节点中存入的tag
属性来获得具体要使用的构造器:
当tag
为bool
类型时,会找到SafeConstruct
中的内部类 ConstructYamlBool
作为构造器,并调用它的construct
方法实例化一个对象,来作为ScalarNode
节点的value
的值:
在construct
方法中,取到的val就是之前的on
,至于下面的这个BOOL_VALUES
,也是提前初始化好的一个HashMap,里面提前存放了一些对应的映射关系,key是下面列出的这些关键字,value则是Boolean
类型的true
或false
:
到这里,yml中的属性解析流程就基本完成了,我们也明白了为什么yml中的on
会被转化为true
的原理了。至于最后,Boolean
类型的true
或false
是如何被转化为的字符串,就是@Value
注解去实现的了。
思考
那么,下一个问题来了,既然yml文件解析中会做这样的特殊处理,那么如果换成properties
配置文件怎么样呢?
sw.turnOn=on
sw.turnOff=off
执行一下程序,看一下结果:
可以看到,使用properties
配置文件能够正常读取结果,看来是在解析的过程中没有做特殊处理,至于解析的过程,有兴趣的小伙伴可以自己去阅读一下源码。
那么,今天就写到这里,我们下期见。
作者简介,码农参上,一个热爱分享的公众号,有趣、深入、直接,与你聊聊技术。个人微信DrHydra9,欢迎添加好友,进一步交流。
18张图,详解SpringBoot解析yml全流程的更多相关文章
- 【PHP】震惊,一张图详解递归函数!!!!
在PHP学习中,递归函数是一个非常重要也是非常难以理解的部分,本博文将通过一张图尽可能演示这个过程,不对之处还请指出
- 六张图详解LinkedList 源码解析
LinkedList 底层基于链表实现,增删不需要移动数据,所以效率很高.但是查询和修改数据的效率低,不能像数组那样根据下标快速的定位到数据,需要一个一个遍历数据. 基本结构 LinkedList 是 ...
- 关于Redis哨兵机制,7张图详解!
写在前面 之前有位朋友去面试被问到Redis哨兵机制,这道题其实很多小伙伴都应该有被问到过!本文将跟大家一起来探讨如何回答这个问题!同时用XMind画了一张导图记录Redis的学习笔记和一些面试解析( ...
- CAS (6) —— Nginx代理模式下浏览器访问CAS服务器网络顺序图详解
CAS (6) -- Nginx代理模式下浏览器访问CAS服务器网络顺序图详解 tomcat版本: tomcat-8.0.29 jdk版本: jdk1.8.0_65 nginx版本: nginx-1. ...
- SPI总线协议及SPI时序图详解
SPI,是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口.SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚 ...
- (转)CAS (4) —— CAS浏览器SSO访问顺序图详解(CAS Web Flow Diagram by Example)
CAS (4) —— CAS浏览器SSO访问顺序图详解(CAS Web Flow Diagram by Example) tomcat版本: tomcat-8.0.29 jdk版本: jdk1.8.0 ...
- SPI总线协议及SPI时序图详解【转】
转自:https://www.cnblogs.com/adylee/p/5399742.html SPI,是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接 ...
- 十图详解tensorflow数据读取机制(附代码)转知乎
十图详解tensorflow数据读取机制(附代码) - 何之源的文章 - 知乎 https://zhuanlan.zhihu.com/p/27238630
- CAS (4) —— CAS浏览器SSO访问顺序图详解(CAS Web Flow Diagram by Example)
CAS (4) -- CAS浏览器SSO访问顺序图详解(CAS Web Flow Diagram by Example) tomcat版本: tomcat-8.0.29 jdk版本: jdk1.8.0 ...
随机推荐
- <转>C/S架构分析
系统架构师-基础到企业应用架构-客户端/服务器 开篇 上篇,我们介绍了,单机软件的架构,其实不管什么软件系统,都是为了解决实际中的一些问题,软件上为了更好的解决实际的问题才会产生,那么对于单机软 件的 ...
- 两大js移动端调试神器 / 调试工具分享 !
分享大家一个CDN网站:https://www.bootcdn.cn/ eruda 移动端网页调试工具的使用: <script src="https://cdn.bootcdn.net ...
- 测试开发实战[提测平台]17-Flask&Vue文件上传实现
微信搜索[大奇测试开],关注这个坚持分享测试开发干货的家伙. 先回顾下在此系列第8次分享给出的预期实现的产品原型和需求说明,如下图整体上和前两节实现很相似,只不过一般测试报告要写的内容可能比较多,就多 ...
- Spring5 概述及Spring IOC学习
Spring Framework 5 1. Spring框架 1.1 Spring框架概述 1.2 主要内容 Spring框架是一个开源的JavaEE的应用程序 主要核心是 IOC(控制反转)和AOP ...
- LuoguP6553 Strings of Monody 题解
Content 给定一个长度为 \(n\) 的字符串 \(s\)(仅包含 \(1,4,5\) 三种字符,\(n\) 在本题中无需输入),有 \(m\) 个操作,每次操作给定两个整数 \(l,r\),再 ...
- LuoguB2001 入门测试题目 题解
Update \(\texttt{2021.7.3}\) 经测试,本题 \(a,b\) 范围在 long long,对代码进行了修改,并修改一些笔误,更新了数据范围. \(\texttt{2021.7 ...
- CF1492B Card Deck 题解
Content 有 \(n\) 张纸牌组成的一个牌堆,每张纸牌都有一个价值 \(p_1,p_2,\dots,p_n\).每次选出最顶上的几个牌放到另外一个一开始为空的牌堆里面.定义一个牌堆的总值为 \ ...
- Boost Asio要点概述(一)
[注]本文不是boost asio的完整应用讲述,而是仅对其中要点的讲解,主要参考了Boost Asio 1.68的官方文档(https://www.boost.org/doc/libs/1_68_0 ...
- 【LeetCode】910. Smallest Range II 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...
- 第四十二个知识点:看看你的C代码为蒙哥马利乘法,你能确定它可能在哪里泄漏侧信道路吗?
第四十二个知识点:看看你的C代码为蒙哥马利乘法,你能确定它可能在哪里泄漏侧信道路吗? 几个月前(回到3月份),您可能还记得我在这个系列的52件东西中发布了第23件(可以在这里找到).这篇文章的标题是& ...