关注 “Java艺术” 我们一起成长!
你好,我是吴就业,
这是我的第170篇原创文章,欢迎阅读!
或许你也发现了,在配置项多的情况下,
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
接口,但只订阅两种类型的事件,如下图所示。
现在我们只关心
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 {@Overridepublic 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> 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
配置。