Spring Boot实现加载自定义配置文件

原创 吴就业 146 0 2020-09-20

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/fd016fe2422e4cde9c18345bb607d2ae

作者:吴就业
链接:https://wujiuye.com/article/fd016fe2422e4cde9c18345bb607d2ae
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2020年09月20日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。

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

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

另外,我们使用kubernetesConfigMap资源作为“配置中心”,可以为每个配置文件创建一个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方法通过SpringFactoriesLoaderspring.factories文件中加载EnvironmentPostProcessor。所以,如果我们想自定义EnvironmentPostProcessor来添加环境变量,首先我们需要实现EnvironmentPostProcessor接口,然后将自定义的EnvironmentPostProcessor添加到spring.factories文件。

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

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

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

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

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

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

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

02-为什么需要服务降级以及常见的几种降级方式

上一篇笔者跟大家分享了一个真实的服务雪崩的故事,也分析了造成服务雪崩的真正原因,那么,如何才能避免服务雪崩的出现呢?

序言:为什么写这个专栏

随着微服务的流行,很多公司也在逐渐的将单体架构项目重构为微服务项目,单体架构微服务化后也将面临更多的挑战。服务的调用错综复杂,如何保护自身不被其它服务打垮也是项目微服务化后重点需要考虑的问题。

01-分享一次服务雪崩问题排查经历

笔者想跟大家分享笔者经历的一次服务雪崩事故,分析导致此次服务雪崩事故的原因。或许大多数读者都有过这样的经历,这是项目给我们上的一次非常宝贵的实战课程。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。

从HotSpot虚拟机源码了解Java的访问控制修饰符

类、字段、方法都有哪些访问控制修饰符? 今天我们就深入java虚拟机去探究这些访问控制修饰符语意的实现。