图解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启动的相关知识,所以如果对这⼀块不是很熟悉的同学,可以先提前先看⼀下Hydra在古早时期写过⼀篇⽂章预热⼀下。下⾯的介绍中,只会摘出⼀些对加载和解析配置⽂件⽐较重要的步骤进⾏分析,对其他⽆关部分进⾏了省略。
加载
当我们启动⼀个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⽅法。
在loadForFileExtension⽅法中,⾸先将classpath:/l加载为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就是2002:bool。当然了,这⾥也不是简单的取出就完事了,后续还会对属性进⾏正则表达式的匹配,看与regexp中的值是否能对的上,检查⽆误时才会返回这个tag。
到这⾥,我们就解释清楚了ScalarNode中tag属性究竟是怎么获取到的了,之后⽅法调⽤层层返回,返回到Origi
调⽤构造器
在constructDocument中,有两步⽐较重要,第⼀步是推断当前节点应该使⽤哪种类型的构造器,第⼆步是使⽤获得的构造器来重新对Node节点中的value进⾏赋值,简易流程如下,省去了循环遍历的部分:
nTrackingConstructor⽗类BaseConstructor的getData⽅法中。接下来,继续执⾏constructDocument⽅法,完成对yml⽂档的解析。
推断构造器种类的过程也很简单,在⽗类BaseConstructor中,缓存了⼀个HashMap,存放了节点的tag类型到对应构造器的映射关系。
springboot结构在getConstructor⽅法中,就使⽤之前节点中存⼊的tag属性来获得具体要使⽤的构造器:
当tag为bool类型时,会到SafeConstruct中的内部类ConstructYamlBool作为构造器,并调⽤它的construct⽅法实例化⼀个对象,来作
为ScalarNode节点的value的值:
在construct⽅法中,取到的val就是之前的on,⾄于下⾯的这个BOOL_VALUES,也是提前初始化好的⼀个HashMap,⾥⾯提前存放了⼀些对应的映射关系,key是下⾯列出的这些关键字,value则是Boolean类型的true或false:
到这⾥,yml中的属性解析流程就基本完成了,我们也明⽩了为什么yml中的on会被转化为true的原理了。
思考
那么,下⼀个问题来了,既然yml⽂件解析中会做这样的特殊处理,那么如果换成properties配置⽂件怎么样呢?
sw.turnOn=on
sw.turnOff=off
执⾏⼀下程序,看⼀下结果:
可以看到,使⽤properties配置⽂件能够正常读取结果,看来是在解析的过程中没有做特殊处理,⾄于解析的过程,有兴趣的⼩伙伴可以⾃⼰去阅读⼀下源码。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论