当前位置: 首页 > 科技观察

18张图,详解SpringBoot解析yml的全过程

时间:2023-03-14 17:48:24 科技观察

背景前几天项目中有个需求,需要一个switch来控制代码中是否执行一段逻辑,所以当然在yml文件中配置了一个属性作为switch,通过nacos,我们可以随时更改这个值来达到我们的目的。yml文件是这样写的:switch:turnOn:on程序中的代码也很简单。大体逻辑如下。如果获取到的switch字段为Ifitison,则执行if判断中的代码,否则不执行:@Value("${switch.turnOn}")privateStringon;@GetMapping("testn")publicvoidtest(){if("on".equals(on)){//TODO}}但当代码实际运行时,有趣的部分来了。我们发现判断中的代码在调试之前是不会执行的,而且发现这里获取的值不是on而是true。看到这里,是不是觉得有点意思?首先盲猜是在解析yml的过程中把on当作一个特殊的值,所以我简单多测试了几个例子,将yml中的属性扩展为如下这些:switch:turnOn:onturnOff:offturnOn2:'on'turnOff2:'off'再次执行代码,查看映射值:可以看到yml中不带引号的on和off转换为true和false,带引号的保持原值不变。说到这里,我不禁有些好奇,为什么会出现这种现象呢?于是强压着睡意翻了翻源码,狠狠的看了一眼SpringBoot加载yml配置文件的过程,终于让我看到了一点办法,下面就一点一点的说吧!因为配置文件的加载会涉及到一些SpringBoot启动的知识,如果你对这方面不是很熟悉,可以看看Hydra早期写的一篇文章来热身一下。在下面的介绍中,只会提取分析加载和解析配置文件的一些重要步骤,其他不相关的部分将省略。加载监听器当我们启动一个SpringBoot程序并执行SpringApplication.run()时,我们首先在SpringApplication的初始化过程中加载了11个实现了ApplicationListener接口的拦截器。11个自动加载的ApplicationListener定义在spring.factories中,通过SPI扩展加载:这里列出的10个是在spring-boot中加载的,剩下的1个是在spring-boot-autoconfigure中加载的。其中最关键的就是ConfigFileApplicationListener,这个跟后面要说的配置文件的加载有关。run方法的执行实例化了SpringApplication之后,就会执行它的run方法。可以看到,在通过getRunListeners方法获取到的SpringApplicationRunListener中,EventPublishingRunListener绑定了我们之前加载的11个监听器。但是在执行启动方法时,会根据类型进行过滤。最后实际执行的只有4个监听器的onApplicationEvent方法,并没有我们想看到的ConfigFileApplicationListener。让我们继续往下看。当run方法执行到prepareEnvironment时,将创建并广播一个ApplicationEnvironmentPreparedEvent类型的事件。这时所有的监听器中,有7个会监听这个事件,然后分别调用他们的onApplicationEvent方法。其中就有我们想到的ConfigFileApplicationListener。接下来,让我们看看它在它的onApplicationEvent方法中做了什么。在调用方法的过程中,会加载系统自带的4个后处理器和ConfigFileApplicationListener本身,一共会执行5个后处理器,并执行它们的postProcessEnvironment方法。其他4个对我们来说并不重要,可以跳过,而最后比较关键的一步是创建Loader实例并调用它的load方法。加载配置文件。这里的Loader是ConfigFileApplicationListener的一个内部类。看一下实例化Loader对象的过程:在实例化Loader对象的过程中,通过SPI扩展再次加载了两个property文件loader,其中YamlPropertySourceLoader是和下面yml的加载解析密切相关的文件,另一个PropertiesPropertySourceLoader负责加载属性文件。创建Loader实例后,接下来将调用其加载方法。在load方法中,会通过嵌套循环遍历默认的配置文件存放路径,加上默认的配置文件名,以及不同的配置文件加载器解析出的相应后缀,最终找到我们的yml配置文件。接下来,开始执行loadForFileExtension方法。在loadForFileExtension方法中,首先加载classpath:/application.yml作为Resource文件,然后准备正式启动,调用之前创建的YamlPropertySourceLoader对象的load方法。在load方法中封装Node,开始为配置文件解析和数据封装做准备:load方法调用了OriginTrackedYmlLoader对象的load方法,从字面意思我们也可以理解,它的用途就是原始的trackingymlloader。中间的一系列方法调用可以忽略,最后也是最重要的一步就是调用OriginTrackingConstructor对象的getData接口解析yml并封装成对象。在解析yml的过程中,实际上是使用了Composerbuilder来生成节点,在它的getNode方法中,通过parserevents来创建节点。通俗的说就是将yml中的一组数据封装成一个MappingNode节点。它的内部其实是一个由NodeTuple组成的List。NodeTuple和Map的结构类似,由一对对应的keyNode和valueNode组成。结构如下:好了,我们回到上面的方法调用流程图。是根据文章开头的yml文件中的实际内容绘制的。如果内容不同,调用过程就会发生变化。你只需要了解这个原理,下面就来详细分析一下。首先创建一个MappingNode节点,将switch封装成一个keyNode,然后再创建一个MappingNode作为外层MappingNode的valueNode,下面存放4组属性,这就是上面4个循环的原因。有点迷糊也没关系,看下图就可以一目了然地了解它的结构。在上图中,引入了一个新的ScalarNode节点。它的使用也比较简单。SimpleString类型的字符串可以用它封装成节点。至此,yml中的数据已经解析完毕,初步打包完成。可能眼尖的朋友会问,为什么上图中,ScalarNode中除了value之外,还有一个tag属性。这个属性有什么用?在介绍它的作用之前,先说说它是如何确定的。这部分的逻辑比较复杂。大家可以看看ScannerImpl类的fetchMoreTokens方法的源码。这个方法会根据yml中每个key或value的开头来决定如何解析,包括{,[,',%,?和其他特殊符号。以解析一个没有任何特殊字符的字符串为例,简要流程如下,省略了一些不重要的部分:在这个图的中间步骤,创建了两个比较重要的对象ScalarToken和ScalarEvent,每个对象都有一个plain属性thatistrue可以理解为这个属性是否需要解释,是后面获取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节点中的值进行重新赋值。简单过程如下,省略了循环遍历的部分:推断构造函数类型的过程也很简单。在父类BaseConstructor中,缓存了一个HashMap,存储了节点的标签类型与对应构造函数的映射关系。在getConstructor方法中,通过上一个节点中保存的tag属性获取具体要使用的构造函数:当tag为bool类型时,会在SafeConstruct中寻找内部类ConstructYamlBool作为构造函数,并调用其构造方法实例将一个对象转换为ScalarNode节点的值:在construct方法中,得到的val就是前面的on。至于后面的BOOL_VALUES,也是一个预先初始化好的HashMap,里面预先存储了一些对应的映射关系,key是下面列出的关键字,value是布尔类型的true或者false:到这里,yml中的属性解析过程就基本完成了,也明白了为什么yml中的on要转为true的原理了。想了想,下一个问题来了。既然在yml文件的解析中会做这样的特殊处理,那如果换成properties配置文件呢?sw.turnOn=onsw.turnOff=off执行程序,看结果:可以看到使用properties配置文件可以正常读取结果。解析过程中似乎没有做任何特殊处理。至于解析过程,有兴趣的朋友可以自行阅读源码。