或许你也发现了,在配置项多的情况下,application-xx.yml配置文件显得过于臃肿,并且在一个分布式项目中,数据库、redis等配置通常是每个微服务都会用到的配置,也都是相同的配置。

为了解决单一配置文件过于臃肿的问题,并且实现让多个微服务共用一些配置文件,我们在新项目中将以往的单配置文件拆分成了多个配置文件。

另外,我们使用kubernetesConfigMap资源作为“配置中心”,可以为每个配置文件创建一个ConfigMap资源,每个微服务项目需要哪些配置文件就可以只引用哪些ConfigMap资源。spring-cloud-kubernete-config会自动读取引用的ConfigMap资源中的配置信息,并写入到Environment中。

虽然通过配置中心加载配置可以去掉配置文件,但本地测试我们通常不会通过配置中心去读取,因此,将单一配置文件拆分为多个配置文件之后,本地测试如何让SpringBoot加载这些配置文件就是我们要解决的问题。

springboot 添加自定义classpath springboot加载自定义配置文件_linux

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

SpringBoot加载配置文件的原理

要实现加载自定义yml文件,我们先要了解SpringBoot是在何时,以及如何加载application-xx.yml配置文件的,为什么配置spring.profiles.active就能导入相应的配置文件。

通过猜测,配置文件的加载应该在容器初始化之前,因为我们经常会在Configuration中就要使用到一些配置,如果在Configuration开始工作之前,配置还没有加载,必然会抛出异常。

根据猜测,我们找到SpringApplication#run方法,如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_java_02

SpringBoot在创建ApplicationContext之前,会先调用prepareEnvironment方法准备创建容器所需要的环境,即创建Environment,并加载配置到Environment。这个过程中还会调用SpringApplicationRunListeners#environmentPrepared方法发布Environment准备事件。

springboot 添加自定义classpath springboot加载自定义配置文件_java_03

执行上图中画线的代码最终会调用EventPublishingRunListener#environmentPrepared方法,该方法广播一个ApplicationEnvironmentPreparedEvent事件(事件同步广播同步消费),只要实现ApplicationListener接口并且订阅ApplicationEnvironmentPreparedEvent事件的订阅者都会接收到该事件,onApplicationEvent方法被调用。

由于Spring实现事件的发布订阅是同步的,在不清楚到底有多少个ApplicationEnvironmentPreparedEvent事件订阅者、不知道哪个订阅者才是负责加载spring.profiles.active配置项指定环境的配置文件时,我们可通过下断点调试方式一步步查找。我们也可以通过IDEA快速查找都有哪些类引用了ApplicationEnvironmentPreparedEvent,如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_java_04

最终找到ConfigFileApplicationListener这个订阅者,该订单者实现ApplicationListener<ApplicationEvent>接口,但只订阅两种类型的事件,如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_linux_05

现在我们只关心ConfigFileApplicationListener是如何消费ApplicationEnvironmentPreparedEvent事件的,所以我们接着看onApplicationEnvironmentPreparedEvent方法,如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_spring_06

Spring框架提供很多的前置处理器,我们所了解的Bean前置处理器可在Bean实例化后创建Bean的代理对象,将代理对象注入Bean工厂,而不是原对象。同样的,Spring也提供Environment的前置处理器,用于往Environment中添加新的环境变量或者修改环境变量的值、移除环境变量。

ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent方法可以看出,该方法首先调用loadPostProcessors方法获取所有的EnvironmentPostProcessor,通过@Order排序之后依次遍历调用EnvironmentPostProcessor对象的postProcessEnvironment方法。

由于环境准备阶段容器并未创建,更没有初始化,所以EnvironmentPostProcessor是无法通过@Bean@Component方式注册的。那Spring是怎么获取EnvironmentPostProcessor的呢,看下图。

springboot 添加自定义classpath springboot加载自定义配置文件_spring_07

loadPostProcessors方法通过SpringFactoriesLoaderspring.factories文件中加载EnvironmentPostProcessor。所以,如果我们想自定义EnvironmentPostProcessor来添加环境变量,首先我们需要实现EnvironmentPostProcessor接口,然后将自定义的EnvironmentPostProcessor添加到spring.factories文件。

SpringBoot实现的这种factories机制类似于JavaSPI,但JavaSPI只能配置接口的实现类,每个接口都需要一个配置文件,springfactories机制则没有这种限制。

SpringBoot默认配置的EnvironmentPostProcessor如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_spring_08

从名字来看,这些EnvironmentPostProcessor都与加载application配置文件无关。可我们疏忽了一点,ConfigFileApplicationListener也实现了EnvironmentPostProcessor接口,并且在onApplicationEnvironmentPreparedEvent方法中也调用了自身的postProcessEnvironment方法,如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_spring_09

如果你看ConfigFileApplicationListener的源码,也能从它的一些静态变量看出它就是负责加载spring.profiles.activespring.profiles.include配置项指定配置文件的EnvironmentPostProcessor,如下图所示。

springboot 添加自定义classpath springboot加载自定义配置文件_android_10

具体的实现就不往下分析了。

通过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_ACTIVEprd(在我们项目中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-rdscommon-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容器的环境(前者最终变为后者的父容器),下面是笔者的实现,仅供参考。

springboot 添加自定义classpath springboot加载自定义配置文件_linux_11

  • 通过在bootstrap.yaml配置文件中配置spring.cloud.config.choose指定当前应用需要导入哪些配置文件。当spring.profiles.active配置为dev时才去加载spring.cloud.config.choose指定的配置文件。
  • 由于Spring Cloud启动的容器与Spring Boot启动的容器使用的不是同一个ProfileEnvironmentPostProcessor对象,但使用的是同一个类加载器加载的类,因此可以通过静态变量共享spring.cloud.config.choose配置。