源码解读 Spring Boot Profiles

前言

上文《一文掌握 Spring Boot Profiles》 是对 Spring Boot Profiles 的介绍和使用,因此本文将从源码角度探究 Spring Boot Profiles,让我们看下 Spring Boot 底层是如何应用 Profiles 进行环境配置的隔离与生效的。

正文

首先,我们先来看下一个简单的 Spring Boot 示例程序,

664672-20190815083430658-1821313923.jpg

在主程序方法中,打印容器中获取到 User 对象,它只有一个 name 属性。

664672-20190815083430949-1288266480.jpg

这里 name 属性引用了外部配置 user.username 的值,它是从配置文件中读取,这里我定义两个配置文件设置该属性,application.propertiesapplication-prod.properties

664672-20190815083431141-1878291779.jpg

有了配置文件之后,启动 SimapleSpringApplication 程序,我们首先可以看到日志输入:User Bean: User(name=one),由此可以看出程序读取了 application.propertiesuser.username 配置。现在我们在 application.properties 中加入一行:

664672-20190815083431302-1173189891.jpg

再次重启启动程序,可以看到控制台如下日志:

664672-20190815083432219-2128392461.jpg

此时 User 对象的name属性变成了 application-prod.properties 中定义的值,并且日志提示 The following profiles are active: prod 表明了名称为 prod 的Profile 在程序中激活。接下来我们就从这个日志入手,探究下这一切是如何发生的。

首先,根据 IDE 的全局查找功能,直接搜索 The following profiles are active: 这些词出现的位置,进行定位,可以找到这个日志出现于 SpringApplication#logStartupProfileInfo 方法之中。

664672-20190815083434120-972485284.jpg

664672-20190815083435026-1702306564.jpg

从日志方法可以看出打印的 activeProfiles 来自上下文关联的 environment 对象,再进一步查看 logStartupProfileInfo 的调用位置,可以在 SpringApplication#prepareContext 方法之中找到,这个方法从命名上就可以看出是主要负责 Spring Boot 运行前容器上下文的预备工作,

664672-20190815083436066-224758425.jpg

我们重新运行程序,通过断点方式拦截 SpringApplication#prepareContext 方法的指向, 获取 environment对象真实的类型为 StandardEnvironment,是 Environment 接口非Web环境的标准实现,存储着一些应用配置和 Profiles 信息,如果在Web环境下,context 关联的就是 StandardServletEnvironment 类型的对象。

664672-20190815083438320-835914705.jpg

知道了日志打印来自 StandardEnvironment 对象的 activeProfiles 属性之后,就需要来看它是在什么时间被赋值的了。继续从调用链的上一级查找,就到了 SpringApplication#run(java.lang.String...),这也是整个程序启动的主要方法。

664672-20190815083441213-2135883199.jpg

从图中可以看出第一次获取到的 environment 对象来自 SpringApplication#prepareEnvironment 内部生成, prepareEnvironment 方法内部首先通过 getOrCreateEnvironment 获取一个基础的 ConfigurableEnvironment 实例,然后对该实例对象初始化配置返回。

664672-20190815083442324-100207535.jpg

正在创建 environment 对象来自 SpringApplication#getOrCreateEnvironment,看它的实现就可以验证我们之前提到 environment 对象类型为 StandardEnvironment。

664672-20190815083442951-1197596969.jpg

了解完 environment 的创建,接下来就关注 environment 的初始化了,这里我们需要关注 listeners.environmentPrepared(environment) 这行代码,这里的 listeners 为 SpringApplicationRunListeners 实例,是监听器 SpringApplicationRunListener 的集合对象, SpringApplicationRunListener#environmentPrepared 方法中就是对每个 SpringApplicationRunListener 对象遍历指向类似的 environmentPrepared 方法,当前集合中只有一个 EventPublishingRunListener 实例,查看其 environmentPrepared 方法就可以看到它主要就是用于发布包含 environment 实例的 ApplicationEnvironmentPreparedEvent 事件,让其他所有监听该事件的监听器进行 environment 实例的配置。

664672-20190815083443448-436020211.jpg

事件对象 ApplicationEnvironmentPreparedEvent 还有一个 getEnvironment 方法获取所传递的 environment实例,我们可以通过看这个方法被使用的地方,获取有哪些类在配置 environment 对象。

664672-20190815083444743-1398244512.jpg

经过多次的查看,从上图可以定位到 ConfigFileApplicationListener 类内的方法对 environment 对象进行扩展,从命名可以看出这个监听器跟配置文件相关,比如它的一些常量属性:CONFIG_NAME_PROPERTYCONFIG_LOCATION_PROPERTY等。从类的注释可以看出,Spring Boot 程序启动所加载的 application.propertiesapplication.yml 默认从四个路径下加载,我们最常用的就是最后一种,它也可以告诉我们还可以把配置文件放在哪,如何自定义加载配置文件的路径。

  • file:./config/:
  • file:./
  • classpath:config/
  • classpath:

664672-20190815083445615-782577863.jpg

将程序断点设置于 ConfigFileApplicationListener#onApplicationEvent 方法之内,重新运行程序就看到程序此时运行到了 ConfigFileApplicationListener 类之中,内部经过多个方法调用从 onApplicationEvent 来到了 addPropertySources 方法,这个方法就是配置文件的属性源加载到 environment 环境去的。

664672-20190815083447323-858605912.jpg

这里的 LoaderConfigFileApplicationListener类内部私有类,用于协调属性源和配置 Profiles,我们再进一步跟踪到它的 load 方法。

664672-20190815083448705-238426719.jpg

我们主要看这个方法中的是三个方法:

  • Loader#initializeProfiles
  • Loader#addProfileToEnvironment
  • Loader#load(Profile, DocumentFilterFactory, DocumentConsumer)

第一个方法 initializeProfiles 初始化 Profiles,给 profiles 属性添加两个元素,null 和 默认的Profile。

第二个方法 addProfileToEnvironment 就是将 Profile 添加到 environment 对象的 activeProfiles 里,也就是最开始日志打印的 activeProfiles

第三个方法就是加载配置文件的数据源和 Profies 相关的属性。

进入 load 方法,这个方法内部通过不同配置路径去尝试执行另一个 load 方法加载配置文件,这里 name 就是配所要搜索的配置文件名称,默认为 application

664672-20190815083449190-386877578.jpg

由于我们的配置文件在 ClassPath 下,所以只要留意当 locationclasspath:/ 的程序执行情况即可。

664672-20190815083450406-456776559.jpg

由于SpringBoot 配置文件支持xmlpropertiesyml 格式,就需要不同 PropertySourceLoader 支持其文件内容的加载:PropertiesPropertySourceLoader 支持 xmlproperties 文件,YamlPropertySourceLoader 支持 yml 文件,加载以 .yml.yaml 后缀的文件,Loader#loadForFileExtension 方法就完成了对这些配置文件的加载。

我们示例程序只有 properties 文件,所以只需要关注当 loader 为 PropertiesPropertySourceLoader时的 Loader#loadForFileExtension 方法的执行情况。

664672-20190815083452029-1747470607.jpg

loadForFileExtension 内部调用另外一个加载配置文件的 load 方法,当读取到ClassPath下的application.properties 时,会执行到 Loader#loadDocuments 方法,这个方法就是把配置文件作为文档进行加载,所有键值对配置都会以存在 PropertySource 之中,存储到 Document 对象中。

!](http://ww3.sinaimg.cn/large/006tNc79ly1g5x80ivld4j31rk0qg130.jpg)

并且 documents 对象经过 Loader#asDocuments 方法关联上 spring.profiles.active 属性,profiles 属性添加一个定义为 prod 的 Profile,为后面的 Environment 对象添加 Profile 做准备,到这里默认的配置文件 application.properties 加载完毕了,方法又回到了 Loader#load() 上。

664672-20190815083453846-690445385.jpg

有了新添加的 Profile,继续进入循环,就会通过 Loader#addProfileToEnvironment 方法,为 environment 对象保存激活的 Profile,并且按照之前的逻辑,读取名为 application-prod.properties 的配置文件,命名方式可以从之前的 Loader#loadForFileExtension 的第462行就可以看出:

664672-20190815083454074-1204563104.jpg

Loader#load() 方法读取了所有配置文件后,执行 Loader#addLoadedPropertySources,将对应属性源 PropertySource 存储到 environment 对象中,并且 application-prod.properties 顺序先于默认配置文件,就是为了后面程序应用相同名称配置的时候,优先采用元素位置在前的配置。

664672-20190815083456804-246956748.jpg

至此,所有配置文件上的数据加载完存储到了与当前上下文关联的 environment 对象中,将 prod 作为 Active Profile 激活特定环境配置的工作就完成了。

小结

虽然只是探究 Spring Boot 程序如何加载和应用 Profile,但通过这次源码分析,我们可以发现 SpringBoot 虽简单易用,但是内部实现逻辑设计是比较复杂的,无论是资源的加载,数据的解析都有专门的组件类去处理,大量使用事件通知和设计模式,在分析源码时少不了一次又一次的运行断点,不过这需要我们充分利用DE工具调试功能,在错综复杂的代码中能更准确地定位目标。

664672-20190815083457431-1544636515.jpg

推荐阅读

转载于:https://www.cnblogs.com/one12138/p/11355789.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值