或许你也发现了,在配置项多的情况下,application-xx.yml
配置文件显得过于臃肿,并且在一个分布式项目中,数据库、redis
等配置通常是每个微服务都会用到的配置,也都是相同的配置。
为了解决单一配置文件过于臃肿的问题,并且实现让多个微服务共用一些配置文件,我们在新项目中将以往的单配置文件拆分成了多个配置文件。
另外,我们使用kubernetes
的ConfigMap
资源作为“配置中心”,可以为每个配置文件创建一个ConfigMap
资源,每个微服务项目需要哪些配置文件就可以只引用哪些ConfigMap
资源。spring-cloud-kubernete-config
会自动读取引用的ConfigMap
资源中的配置信息,并写入到Environment
中。
虽然通过配置中心加载配置可以去掉配置文件,但本地测试我们通常不会通过配置中心去读取,因此,将单一配置文件拆分为多个配置文件之后,本地测试如何让SpringBoot
加载这些配置文件就是我们要解决的问题。
本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot
加载配置文件的流程,从而加深理解。
SpringBoot加载配置文件的原理
要实现加载自定义yml
文件,我们先要了解SpringBoot
是在何时,以及如何加载application-xx.yml
配置文件的,为什么配置spring.profiles.active
就能导入相应的配置文件。
通过猜测,配置文件的加载应该在容器初始化之前,因为我们经常会在Configuration
中就要使用到一些配置,如果在Configuration
开始工作之前,配置还没有加载,必然会抛出异常。
根据猜测,我们找到SpringApplication#run
方法,如下图所示。
SpringBoot
在创建ApplicationContext
之前,会先调用prepareEnvironment
方法准备创建容器所需要的环境,即创建Environment
,并加载配置到Environment
。这个过程中还会调用SpringApplicationRunListeners#environmentPrepared
方法发布Environment
准备事件。
执行上图中画线的代码最终会调用EventPublishingRunListener#environmentPrepared
方法,该方法广播一个ApplicationEnvironmentPreparedEvent
事件(事件同步广播同步消费),只要实现ApplicationListener
接口并且订阅ApplicationEnvironmentPreparedEvent
事件的订阅者都会接收到该事件,onApplicationEvent
方法被调用。
由于Spring
实现事件的发布订阅是同步的,在不清楚到底有多少个ApplicationEnvironmentPreparedEvent
事件订阅者、不知道哪个订阅者才是负责加载spring.profiles.active
配置项指定环境的配置文件时,我们可通过下断点调试方式一步步查找。我们也可以通过IDEA
快速查找都有哪些类引用了ApplicationEnvironmentPreparedEvent
,如下图所示。
最终找到ConfigFileApplicationListener
这个订阅者,该订单者实现ApplicationListener<ApplicationEvent>
接口,但只订阅两种类型的事件,如下图所示。
现在我们只关心ConfigFileApplicationListener
是如何消费ApplicationEnvironmentPreparedEvent
事件的,所以我们接着看onApplicationEnvironmentPreparedEvent
方法,如下图所示。
Spring
框架提供很多的前置处理器,我们所了解的Bean
前置处理器可在Bean
实例化后创建Bean
的代理对象,将代理对象注入Bean
工厂,而不是原对象。同样的,Spring
也提供Environment
的前置处理器,用于往Environment
中添加新的环境变量或者修改环境变量的值、移除环境变量。
从ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent
方法可以看出,该方法首先调用loadPostProcessors
方法获取所有的EnvironmentPostProcessor
,通过@Order
排序之后依次遍历调用EnvironmentPostProcessor
对象的postProcessEnvironment
方法。
由于环境准备阶段容器并未创建,更没有初始化,所以EnvironmentPostProcessor
是无法通过@Bean
、@Component
方式注册的。那Spring
是怎么获取EnvironmentPostProcessor
的呢,看下图。
loadPostProcessors
方法通过SpringFactoriesLoader
从spring.factories
文件中加载EnvironmentPostProcessor
。所以,如果我们想自定义EnvironmentPostProcessor
来添加环境变量,首先我们需要实现EnvironmentPostProcessor
接口,然后将自定义的EnvironmentPostProcessor
添加到spring.factories
文件。
SpringBoot
实现的这种factories
机制类似于Java
的SPI
,但Java
的SPI
只能配置接口的实现类,每个接口都需要一个配置文件,spring
的factories
机制则没有这种限制。
SpringBoot
默认配置的EnvironmentPostProcessor
如下图所示。
从名字来看,这些EnvironmentPostProcessor
都与加载application
配置文件无关。可我们疏忽了一点,ConfigFileApplicationListener
也实现了EnvironmentPostProcessor
接口,并且在onApplicationEnvironmentPreparedEvent
方法中也调用了自身的postProcessEnvironment
方法,如下图所示。
如果你看ConfigFileApplicationListener
的源码,也能从它的一些静态变量看出它就是负责加载spring.profiles.active
、spring.profiles.include
配置项指定配置文件的EnvironmentPostProcessor
,如下图所示。
具体的实现就不往下分析了。
通过spring.profiles.include导入
实现加载自定义配置文件最简单的方式,我们可以通过配置spring.profiles.include
导入指定的自定义配置文件,这是springboot
为我们提供的拆分配置文件的功能,但配置文件的命令必须以application-
开头。
如本地测试将spring.profiles.active
配置为dev
,则会导入application-dev.yml
配置文件,我们只需要在application-dev.yml
中配置spring.profiles.include
导入用于测试环境的自定义配置文件即可。
例如导入application-rds-dev.yml
,则配置如下。
spring:
profiles:
include: rds-dev
除此之外,我们还可以直接在application.yml
配置文件中配置spring.profiles.include
,例如:
spring:
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
include: rds-${SPRING_PROFILES_ACTIVE:dev}
在本例中,使用${SPRING_PROFILES_ACTIVE:dev}
根据环境(测试环境、预发布环境、生产环境)选择不同的rds
配置文件。
当SPRING_PROFILES_ACTIVE
变量不存在时,则默认为dev
环境,include
导入application-rds-dev.yml
配置文件;如果是生产环境,则SPRING_PROFILES_ACTIVE
为prd
(在我们项目中prd
为什么环境),include
将导入application-rds-prd.yml
配置文件。
通过java
命令启动springboot
应用,可以在启动时再通过-Dspring.profiles.active
参数切换配置,而本例使用环境变量主要是解决将应用构建为Docker
镜像时,无法在启动时再通过-Dspring.profiles.active
参数切换配置的问题。
通过自定义EnvironmentPostProcessor导入
通过配置spring.profiles.include
导入自定义文件有一个强制约定,文件名必须以application-
开头。
在不想使用application-
作为文件名前缀的情况下,并且想让SpringBoot
能够根据环境选择是否加载resources
目录下的自定义配置文件时,就无法使用spring.profiles.include
。
那有没有一种方式能够实现更灵活的加载自定义配置文件?通过前面对SpringBoot
加载配置文件的了解,相信你已经有了答案。没错,可是通过自定义EnvironmentPostProcessor
实现。
将配置文件拆分后,我们将文件改为以common-
开头,例如:common-rds
、common-redis
。如果是线上环境直接从配置中心读取,只在本地测试不想从配置中心读取的情况下,自定义的EnvironmentPostProcessor
才会加载自定义配置文件。
通过自定义EnvironmentPostProcessor
加载自定义配置文件,导入配置信息,整体上只需要两步:
第一步:自定义ProfileEnvironmentPostProcessor
实现EnvironmentPostProcessor
接口,代码如下。
public class ProfileEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// .......
// 加载配置
PropertySource<?> source = loadProfiles(resource);
// 添加到Environment
environment.getPropertySources().addFirst(source);
}
}
loadProfiles
方法实现如下,通过YamlPropertySourceLoader
解析yml
配置文件。
private PropertySource<?> loadProfiles(Resource resource) {
YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySources = sourceLoader.load(resource.getFilename(), resource);
return propertySources.get(0);
}
第二步:将ProfileEnvironmentPostProcessor
配置到spring.factories
,配置如下。
org.springframework.boot.env.EnvironmentPostProcessor=\
com.xxx.spring.profile.ProfileEnvironmentPostProcessor
最后,我们也可以将ProfileEnvironmentPostProcessor
封装成一个starter
包,以便服务于每个微服务项目。
到现在,我们也只是实现了如何读取自定义配置文件,将配置写入Environment
中。实际还有很多细节需要我们考虑,例如,如何判断只在spring.profiles.active
配置为dev
时才加载自定义文件、如何区分当前是准备启动Spring Cloud
容器的环境还是准备启动Spring Boot
容器的环境(前者最终变为后者的父容器),下面是笔者的实现,仅供参考。
- 通过在
bootstrap.yaml
配置文件中配置spring.cloud.config.choose
指定当前应用需要导入哪些配置文件。当spring.profiles.active
配置为dev
时才去加载spring.cloud.config.choose
指定的配置文件。 - 由于
Spring Cloud
启动的容器与Spring Boot
启动的容器使用的不是同一个ProfileEnvironmentPostProcessor
对象,但使用的是同一个类加载器加载的类,因此可以通过静态变量共享spring.cloud.config.choose
配置。