Springboot是目前开源项目中很常用的框架技术,今天就来聊一下Springboot中多环境配置是如何实现的。即application.yml中spring.profiles.active 配置不同环境,系统是如何自动加载识别加载的。
Spring Boot 程序启动所加载的 application.properties 或 application.yml 默认从四个路径下加载,我们最常用的就是最后一种,它也可以告诉我们还可以把配置文件放在哪,如何自定义加载配置文件的路径。
file:./config/:
file:./
classpath:config/
classpath:
我们以Springboot的启动类的主程序来讲解application的加载逻辑。
在主程序方法中,打印容器中获取到 User 对象,它只有一个 name 属性。
这里 name 属性引用了外部配置 user.username 的值,它是从配置文件中读取,这里我定义两个配置文件设置该属性,application.properties 和 application-prod.properties。
有了配置文件之后,启动 SimapleSpringApplication 程序,我们首先可以看到日志输入:User Bean: User(name=one),由此可以看出程序读取了 application.properties 的 user.username 配置。现在我们在 application.properties 中加入一行:
再次重启启动程序,可以看到控制台如下日志:
此时 User 对象的name属性变成了 application-prod.properties 中定义的值,并且日志提示 The following profiles are active: prod 表明了名称为 prod 的Profile 在程序中激活。接下来我们就从这个日志入手,探究下这一切是如何发生的。
首先,根据 IDE 的全局查找功能,个人习惯可以设置快捷键,比如我的是同时按住Crtl+Shift+R之后搜索 The following profiles are active: 这些词出现的位置,进行定位,可以找到这个日志出现于 SpringApplication#logStartupProfileInfo 方法之中。
从log.info日志中分析,我们可以看出打印的 activeProfiles 来自上下文的 environment 对象,然后向上追踪查看 logStartupProfileInfo 的调用位置,可以在 SpringApplication#prepareContext 方法之中找到,这个方法从命名上就可以看出是主要负责 Spring Boot 运行前容器上下文的预备工作,
我们重新运行程序,通过断点方式拦截 SpringApplication#prepareContext 方法的指向, 获取 environment对象真实的类型为 StandardEnvironment,是 Environment 接口非Web环境的标准实现,存储着一些应用配置和 Profiles 信息,如果在Web环境下,context 关联的就是 StandardServletEnvironment 类型的对象。
知道了日志打印来自 StandardEnvironment 对象的 activeProfiles 属性之后,就需要来看它是在什么时间被赋值的了。继续从调用链的上一级查找,我们看到 SpringApplication#run(java.lang.String...),这也是整个程序启动的主要方法。
从图中程序我们可以看出第一次得到的 environment 对象来自 SpringApplication#prepareEnvironment 内部生成, prepareEnvironment 方法内部首先通过 getOrCreateEnvironment 获取一个基础的 ConfigurableEnvironment 实例,然后对该实例对象初始化配置返回。
正在创建 environment 对象来自 SpringApplication#getOrCreateEnvironment,看它的实现就可以验证我们之前提到 environment 对象类型为 StandardEnvironment。
了解完 environment 的创建之后,接下来就关注 environment 的初始化了,进一步跟踪 listeners.environmentPrepared(environment) 这行代码,这里的 listeners 为 SpringApplicationRunListeners 实例,是监听器 SpringApplicationRunListener 的集合对象, SpringApplicationRunListener#environmentPrepared 方法中就是对每个 SpringApplicationRunListener 对象遍历指向类似的 environmentPrepared 方法,当前集合中只有一个 EventPublishingRunListener 实例,查看其 environmentPrepared 方法就可以看到它主要就是用于发布包含 environment 实例的 ApplicationEnvironmentPreparedEvent 事件,让其他所有监听该事件的监听器进行 environment 实例的配置。
事件对象 ApplicationEnvironmentPreparedEvent 还有一个 getEnvironment 方法获取所传递的 environment实例,我们可以通过看这个方法被使用的地方,获取有哪些类在配置 environment 对象。
经过多次的查看,从上图可以定位到 ConfigFileApplicationListener 类内的方法对 environment 对象进行扩展,从命名可以看出这个监听器跟配置文件相关,比如它的一些常量属性:CONFIG_NAME_PROPERTY,CONFIG_LOCATION_PROPERTY等。从类的注释可以看出,Spring Boot 程序启动所加载的 application.properties 或 application.yml 默认从四个路径下加载,我们最常用的就是最后一种,它也可以告诉我们还可以把配置文件放在哪,如何自定义加载配置文件的路径。
file:./config/:
file:./
classpath:config/
classpath:
将程序断点设置于 ConfigFileApplicationListener#onApplicationEvent 方法之内,重新运行程序就看到程序此时运行到了 ConfigFileApplicationListener 类之中,内部经过多个方法调用从 onApplicationEvent 来到了 addPropertySources 方法,这个方法就是配置文件的属性源加载到 environment 环境去的。
这里的 Loader 是 ConfigFileApplicationListener类内部私有类,用于协调属性源和配置 Profiles,我们再进一步跟踪到它的 load 方法。
我们主要看这个方法中的是三个方法:
Loader#initializeProfilesLoader#addProfileToEnvironmentLoader#load(Profile, DocumentFilterFactory, DocumentConsumer)第一个方法 initializeProfiles 初始化 Profiles,给 profiles 属性添加两个元素,null 和 默认的Profile。
第二个方法 addProfileToEnvironment 就是将 Profile 添加到 environment 对象的 activeProfiles 里,也就是最开始日志打印的 activeProfiles。
第三个方法就是加载配置文件的数据源和 Profies 相关的属性。
进入 load 方法,这个方法内部通过不同配置路径去尝试执行另一个 load 方法加载配置文件,这里 name 就是配所要搜索的配置文件名称,默认为 application。
由于我们的配置文件在 ClassPath 下,所以只要留意当 location 为 classpath:/ 的程序执行情况即可。
由于SpringBoot 配置文件支持xml,properties, yml 格式,就需要不同 PropertySourceLoader 支持其文件内容的加载:PropertiesPropertySourceLoader 支持 xml,properties 文件,YamlPropertySourceLoader 支持 yml 文件,加载以 .yml 或 .yaml 后缀的文件,Loader#loadForFileExtension 方法就完成了对这些配置文件的加载。
我们示例程序只有 properties 文件,所以只需要关注当 loader 为 PropertiesPropertySourceLoader时的 Loader#loadForFileExtension 方法的执行情况。
loadForFileExtension 内部调用另外一个加载配置文件的 load 方法,当读取到ClassPath下的application.properties 时,会执行到 Loader#loadDocuments 方法,这个方法就是把配置文件作为文档进行加载,所有键值对配置都会以存在 PropertySource 之中,存储到 Document 对象中。并且 documents 对象经过 Loader#asDocuments 方法关联上 spring.profiles.active 属性,profiles 属性添加一个定义为 prod 的 Profile,为后面的 Environment 对象添加 Profile 做准备,到这里默认的配置文件 application.properties 加载完毕了,方法又回到了 Loader#load() 上。
有了新添加的 Profile,继续进入循环,就会通过 Loader#addProfileToEnvironment 方法,为 environment 保存新添加的 Profile,并且按照之前的逻辑,读取名为 application-prod.properties 的配置文件,命名方式可以从之前的 Loader#loadForFileExtension 的源代码中可以看出:
在 Loader#load() 方法读取了所有配置文件后,执行 Loader#addLoadedPropertySources,将对应属性源 PropertySource 存储到 environment 对象中,并且 application-prod.properties 顺序先于默认配置文件,就是为了后面程序应用相同名称配置的时候,优先采用元素位置在前的配置。
至此,所有配置文件上的数据加载完存储到了 environment 对象中,spring.profiles.active=prod 配置自动加载识别的工作就完成了
虽然一开始的出发点只是探究 Spring Boot 程序如何加载和应用 Profile,但通过一连串的源码分析,我们发现 SpringBoot 虽简单易用,但是内部实现逻辑设计是比较复杂的,逐步跟踪源代码,会发现数据的解析、资源的加载等都有专门的组件类去处理,并且使用了大量的事件通知和设计模式,这也给我平时在工作学习中提出了更高的要求,学会在错综复杂的代码中更准确地定位目标,知其然还要知其所以然。