spring boot 环境配置加载原理

4月 11, 2023 188点热度 2人点赞 4条评论

spring boot的出现,大大减少了配置文件的数量,我们可以通过代码的方式完成spring的所有配置,在spring boot启动过程中,重要的就是环境变量数据的加载,这些环境变量会成为后面容器启动的关键配置信息,因此主要看下环境配置加载的原理。

1. 使用实例

在开始了解配置加载以前,首先以demo的方式实验配置的使用,以及可能存在配置文件之间的配置覆盖。

1.1 maven项目搭建

maven的项目结构,还是按照parent-child的方式创建,parent负责jar包的版本管理,这样可以实现版本的统一管理。

parent pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.spring.cloud.alibaba.learn</groupId>
  <artifactId>spring-cloud-alibaba</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>spring-cloud-alibaba</name>
  <url>http://maven.apache.org</url>


  <modules>
    <module>spring-cloud-alibaba-nacos</module>
  </modules>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2021.0.5.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>2021.0.5</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.6.13</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

child pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.spring.cloud.alibaba.learn</groupId>
    <artifactId>spring-cloud-alibaba</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>

  <groupId>org.example</groupId>
  <artifactId>spring-cloud-alibaba-nacos</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>spring-cloud-alibaba-nacos</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
  </dependencies>
</project>

则整体的项目结构如下:

1.2 创建配置文件

我们只是单纯的查看配置文件的加载原理,因此不需要太多的配置信息。从上图可以知道,主要包含了三个配置文件:

  • bootstrap.yml
  • application.yml
  • application-dev.yml

这三个文件存在加载顺序的先后关系,按照加载顺序排列。

bootstrap.yml文件的加载,在spring-boot中是不会加载的,该配置文件是在spring-cloud-bootstrap中引入并加载。

bootstrap.yml的配置如下:

config:
  name: bootstrap
  bootstrap: app-boot
spring:
  application:
    name: spring-cloud-alibaba-nacos

application.yml

config:
  name: application
  app: application
spring:
  profiles:
    active: dev

application-dev.yml

config:
  name: spplication-dev

1.3 程序实现

@SpringBootApplication
public class SpringApplicationNacos {
    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringApplicationNacos.class, args);
        System.out.println(applicationContext);
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        System.out.println(environment.getProperty("config.name"));
        System.out.println(environment.getProperty("config.app"));
        System.out.println(environment.getProperty("config.bootstrap"));
    }
}

从代码的实现中可以看出,程序还是很简单的,就是创建ApplicationContext并从Environment中获取对应的变量信息。

执行以上代码,可以得到以下输出结果信息:

spplication-dev
application
app-boot

可以看出,相同的配置文件信息,application-dev.yml具有更高的优先级,因此我们以上面这段代码为例,跟踪环境信息加载源码。

2. 环境配置加载

2.1 SpringApplication

spring boot的启动都是以SpringApplication为入口,通过run()方法发起容器的启动。在SpringApplication类中,主要通过new的方式创建SpringApplication对象,并执行目标run()方法。

最终代码会走到SpringApplication构造器中,构造器中包含了包含了初始化的一些代码逻辑,如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 资源加载器
    this.resourceLoader = resourceLoader;
    // 参数必要验证,启动类不能为空
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 确定应用类型,主要通过加载特定的class对象判定是否成功,如果成功则为指定类型。
    // 在web中主要包含了Servlet和WebFlux响应式的两种,如果没有引入web包,则为null
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 获取BootstrapRegistryInitializer对象,这里主要通过Spring SPI的方式加载对应的类型
    // 程序会加载所有jar包中的spring.factories的文件中的配置,并缓存
    this.bootstrapRegistryInitializers = new ArrayList<>(
            getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    // 获取并初始化ApplicationContextInitializer实例
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 获取并初始化ApplicationListener实例
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

run()

public ConfigurableApplicationContext run(String... args) {
    long startTime = System.nanoTime();
    // 创建启动上下文
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    // 设置系统信息
    configureHeadlessProperty();
    // 获取SpringApplicationRunLister对象,通过SPI从spring.factories中加载
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 执行starting方法, 通过SpringApplicationRunListeners发送ApplicationStartingEvent事件
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
        // 创建命令行参数对象
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 准备Environment对象
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        configureIgnoreBeanInfo(environment);
        ....
    }
    ...
    return context;
}

在执行环境变量操作之前,主要执行了几个重要的步骤:

  • 创建DefaultBootstrapContext对象,这个对象主要是上下文,用于存放在执行过程中的一些重要的结果
  • 获取SpringApplicationRunListener对象,这个对象是对容器开始执行的监听,然后在执行前通过ApplicationStartingEvent事件触发。这个对象的扩展,主要通过Spring的SPI机制来完成。

prepareEnvironment()

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
        // 创建或获取Environment对象
        // 该方法会根据webApplicationType创建不同的Environment对象
        // 如果environment对象已经存在,则直接返回
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        // 配置Environment, 
        // 主要包括设置ConversionService对象
        // 合并默认的配置信息以及命令行的配置信息,并将source名称定义为commandLineArgs
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        // 关联Environment对象,
        // 该方法主要判断在环境变量中是否已经包含了configurationProperties的resource信息
        // 如果已经包含,则将该resource方法resources列表的头部,如果不存在,则创建SpringConfigurationPropertySources
        ConfigurationPropertySources.attach(environment);
        // 发送ApplicationEnvironmentPreparedEvent事件,并由监听器执行处理该事件
        // 这里的事件处理机制也是加载各种配置文件的入口地方
        listeners.environmentPrepared(bootstrapContext, environment);
        // 将defaultProperties的配置信息移动到resources末尾,相当于降低优先级
        DefaultPropertiesPropertySource.moveToEnd(environment);
        Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
                "Environment prefix cannot be set via properties.");
        // 将environment和SpringApplication进行绑定
        bindToSpringApplication(environment);

        // 如果是自定义的环境变量,则需要将environment对象转换为StandardEnvironment对象
        if (!this.isCustomEnvironment) {
            environment = convertEnvironment(environment);
        }
        // 将configurationProperties配置放到resources头部
        ConfigurationPropertySources.attach(environment);
        // 返回环境变量对象
        return environment;
    }
  • configureEnvironment():该方法主要合并了默认配置信息和命令行的配置信息
  • ConfigurationPropertySources.attach():该方主要是配置configurationProperties的配置员source,并将该配置放在首位,这样具有高优先权
  • environmentPrepared():该方法则是真正加载配置文件的地方,作为重点的对象

2.2 SpringApplicationRunListener

该类主要用于执行在Environment对象的对象操作,该类的实现类是通过spring的SPI的机制加载到内存中,即:spring.factories文件进行配置,具体配置信息如下:

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

这里也是作为spring的SPI机制的扩展重点,因为这些扩展点我们也可以通过这种方式加载自己的配置文件源,然后做自定义的实现。

另外还有一点很有意思的是,spring并不是直接遍历SpringApplicationRunListener列表,而是通过一个对象SpringApplicationRunListeners来间接的遍历,并提供了Iterator的能力,这种方式可以避免集合的遍历代码分散,实现功能的内敛

在以上的实例中,该对象之引入了一个实例,

也就是EventPublishingRunListener对象,该对象也就是处理加载配置文件的入口。

2.3 EventPublishingRunListener

我们查看该类的实现接口SpringApplicationRunListener, 可以看出,该类就是定义了容器启动中每个阶段不同处理,我们可以根据自己需求,实现每个阶段中的一部分进行扩展。

构造器

public EventPublishingRunListener(SpringApplication application, String[] args) {
    // 当前正在启动的SpringApplication对象
    this.application = application;
    // 命令行参数
    this.args = args;
    // 事件分发器初始化
    this.initialMulticaster = new SimpleApplicationEventMulticaster();
    // 这里我们知道,在初始化SpringApplication的时候,是从spring.factories中
    // 加载了配置的ApplicationListener实现实例,因此从application中获取并和
    // 事件分发器进行绑定
    for (ApplicationListener<?> listener : application.getListeners()) {
        this.initialMulticaster.addApplicationListener(listener);
    }
}

environmentPrepared()

该方法就是用来处理环境environment准备的入口,具体代码如下:

public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
        ConfigurableEnvironment environment) {
    this.initialMulticaster.multicastEvent(
            new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
}

这个方法就很简单了,主要分为两步:

  • 创建ApplicationEnvironmentPreparedEvent事件对象
  • 分发该事件到ApplicationListener示例并进行处理。

2.4 SimpleApplicationEventMulticaster

在执行事件分发的时候,对应的ApplicationListener实际上已经实例化完成,可以通过堆栈查看对应的列表:

在默认的配置下,有11个ApplicationListener被加载,对于事件的分发,并不是所有的Listener都可以执行,因此在具体的分发的时候,需要过滤掉不能处理的listener.

multicaseEvent()

该方法则是具体执行事件分发的地方,具体代码如下:

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    // 解析泛型,主要获取event的实际类型
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    // 获取线程池对象,如果没有开启异步事件分发,则为null
    Executor executor = getTaskExecutor();
    // getApplicationListeners()方法主要根据event的实际类型判断,
    // listener是否能够处理当前event, 如果能,则返回; 否则过滤掉
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        // 如果线程池对象不为空,则异步执行事件分发
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            // 否则同步执行分发
            invokeListener(listener, event);
        }
    }
}

通过事件类型过滤后,实际上只有8个listener能够处理当前的事件。具体处理逻辑,可以自行查看对应源码。

doInvokeListener()

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
    try {
        listener.onApplicationEvent(event);
    }
    catch (ClassCastException ex) {
        ....
    }
}

执行listener就只是调用对应的onApplicaiontEvent()方法。

2.5 ApplicationListener

该类就是具体处理以上各种事件的入口,对于配置文件二院,我们不需要关心所有的类,而是只关注配置文件相关的类即可。因此,在一下的源码梳理中,不会列出所有类的实现。

2.5.1 BootstrapApplicationListener

该类不是spring-boot引入,而是由spring-cloud引入。因此这里需要区分,当我们只使用spring-boot时,是没有这个类型的。

onApplicationEvent()
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    // 获取环境对象
    ConfigurableEnvironment environment = event.getEnvironment();

    // bootstrapEnabled: 主要判断是否启用bootstrap, 可以通过判断spring.cloud.bootstrap.enabled=true或org.springframework.cloud.bootstrap.marker.Marker
    // 能够被加载,因此就算不开启配置,只要引入了spring-cloud-bootstrap包,也就默认配置的开启
    // useLegacyProcessing()方法用于判断spring.config.use-legacy-processing是否为true
    if (!bootstrapEnabled(environment) && !useLegacyProcessing(environment)) {
        return;
    }
    // don't listen to events in a bootstrap context
    // 判断是否包含了bootstrap的配置员,主要是为防止循环监听和加载
    if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
        return;
    }

    ConfigurableApplicationContext context = null;
    // 这里是获取spring.cloud.bootstrap.name的值,默认只为bootstrap.
    // 这里需要注意,这个时候,环境配置中只包含了环境变量和系统变量以及命令行参数,因此这个配置如果需要
    // 该表该值,是不可以通过配置文件进行修改,或者定义更高级别的处理方式
    String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");

    // 这里是容器初始代码,这个代码也是在初始化SpringApplication时通过SPI的方式加载
    // 这段代码主要是获取ApplicationContext容器,如果是具有层级关系的初始化器时,则尝试
    // 从初始化器中获取ApplicationContext容器
    for (ApplicationContextInitializer<?> initializer : event.getSpringApplication().getInitializers()) {
        if (initializer instanceof ParentContextApplicationContextInitializer) {
            context = findBootstrapContext((ParentContextApplicationContextInitializer) initializer, configName);
        }
    }

    // 如果ApplicationContext容器化没有创建,则创建
    if (context == null) {
        context = bootstrapServiceContext(environment, event.getSpringApplication(), configName);
        event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));
    }

    apply(context, event.getSpringApplication(), environment);
}

在该方法中主要有以下步骤:

  • 环境配置中是否已经加入了bootstrap源,如果包含了,则不再重复加载
  • 是否已经创建ApplicationContext上下文,如果没有包含,则创建ApplicaontContext上下文,并重新执行SpringApplication.run()方法

这里bootstrap创建的上下文信息,以及在环境变量中的内容,最终是不会合并到当前正在执行的application之上,而是以parent的方式存在

bootstrapServiceContext()
private ConfigurableApplicationContext bootstrapServiceContext(ConfigurableEnvironment environment,
            final SpringApplication application, String configName) {
        ConfigurableEnvironment bootstrapEnvironment = new AbstractEnvironment() {
        };

        // 获取配置源列表
        MutablePropertySources bootstrapProperties = bootstrapEnvironment.getPropertySources();
        // 获取bootstrap配置文件列表
        String configLocation = environment.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
        // 获取bootstrap额外配置信息
        String configAdditionalLocation = environment
                .resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
        
        // 创建bootstrap配置映射
        Map<String, Object> bootstrapMap = new HashMap<>();
        // 设置spring.config.name配置为bootstrap
        bootstrapMap.put("spring.config.name", configName);
        bootstrapMap.put("spring.main.web-application-type", "none");
        if (StringUtils.hasText(configLocation)) {
            bootstrapMap.put("spring.config.location", configLocation);
        }
        if (StringUtils.hasText(configAdditionalLocation)) {
            bootstrapMap.put("spring.config.additional-location", configAdditionalLocation);
        }
        // 加入bootstrap配置信息
        bootstrapProperties.addFirst(new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
        // 从已有environment中读取源数据并加入到新的environment中
        for (PropertySource<?> source : environment.getPropertySources()) {
            if (source instanceof StubPropertySource) {
                continue;
            }
            bootstrapProperties.addLast(source);
        }

        // 构建SpringApplicaionBuilder对象
        SpringApplicationBuilder builder = new SpringApplicationBuilder().profiles(environment.getActiveProfiles())
                .bannerMode(Mode.OFF).environment(bootstrapEnvironment)
                .registerShutdownHook(false).logStartupInfo(false).web(WebApplicationType.NONE);
        final SpringApplication builderApplication = builder.application();
        
        // 如果启动class为空,则使用当前application的主class
        if (builderApplication.getMainApplicationClass() == null) {
            builder.main(application.getMainApplicationClass());
        }

        // 如果包含refreshArgs配置源,则过滤listener
        if (environment.getPropertySources().contains("refreshArgs")) {
            builderApplication.setListeners(filterListeners(builderApplication.getListeners()));
        }

        // configuration源设置
        builder.sources(BootstrapImportSelectorConfiguration.class);
        // 执行run方法,跟SpringApplication.run()方法类似,只是很多参数设置为固定值
        // 这个时候实际上创建了一个新的ApplicationContext对象
        final ConfigurableApplicationContext context = builder.run();
        // 设置当前ApplicationContext的id为bootstrap
        context.setId("bootstrap");
        // 为AncestorInitializer对象设置parent的容器信息
        addAncestorInitializer(application, context);
        // 从配置源中移除bootstrap配置信息
        bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
        // 合并配置
        mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
        return context;
    }

2.5.2 EnvironmentPostProcessorApplicationListener

该监听器主要用于处理EnvironmentPostProcessor的实现,具体加载代码如下:

static EnvironmentPostProcessorsFactory fromSpringFactories(ClassLoader classLoader) {
    return new ReflectionEnvironmentPostProcessorsFactory(classLoader,
            SpringFactoriesLoader.loadFactoryNames(EnvironmentPostProcessor.class, classLoader));
}

可以看出,EnvironmentPostProcessor的执行通过SPI机制进行加载,具体在spring.factories的定义如下:

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

这里的定义只是取了其中很小的一部分,并不是全部内容,只是为了说明配置方式。

onApplicationEvent()

事件的执行入口方法,该方法很简单,

public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent();
    }
    if (event instanceof ApplicationFailedEvent) {
        onApplicationFailedEvent();
    }
}

该类能够处理多种事件类型,在该阶段主要关心ApplicationEnvironmentPreparedEvent即可

onApplicationEnvironmentPreparedEvent()
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    // 获取环境变量对象
    ConfigurableEnvironment environment = event.getEnvironment();
    // 获取SpringApplication对象
    SpringApplication application = event.getSpringApplication();
    // 获取EnvironmentPostProcessor列表
    for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(),
            event.getBootstrapContext())) {
        // 遍历并同步调用方法
        postProcessor.postProcessEnvironment(environment, application);
    }
}

该方法主要包含了两个重要的点:

  • spring.factories中获取EnvironmentPostProcessor对象列表,主要通过SPI机制继续加载
  • 调用EnvironmentPostProcessor.postProcessEnvironment()方法

2.6 EnvironmentPostProcessor

该类是对Environment对象的后置处理器,该接口定义也很简单:

public interface EnvironmentPostProcessor {
    void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}

在以上配置中,可以查看从spring.factories中加了处理器列表:

2.6.1 ConfigDataEnvironmentPostProcessor

配置文件数据处理器,在该处理器中会主要处理配置文件相关的加载.

void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
            Collection<String> additionalProfiles) {
        try {
            this.logger.trace("Post-processing environment to add config data");
            // 获取ResourceLoader对象
            resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
            // 获取ConfigDataEnvironment对象,并调用processAndApply()方法
            getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
        }
        catch (UseLegacyConfigProcessingException ex) {
            ...
        }
    }

    ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
            Collection<String> additionalProfiles) {
        // 创建爱你ConfigDataEnvironment对象
        return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader,
                additionalProfiles, this.environmentUpdateListener);
    }

对于环境配置处理,最终委派到了ConfigDataEnvironment对象中,因此我们需要主要查看该对象内部的处理逻辑。

2.7 ConfigDataEnvironment

该类需要关注类型初始化的逻辑,其中包含了多的潜在的逻辑,对后续阅读会有很大的帮助。

类初始化

类初始化主要是为了初始化静态变量相关数据,在该类中,静态初始化主要定义了扫描的配置文件路径信息。

static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static {
    List<ConfigDataLocation> locations = new ArrayList<>();
    locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));
    locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/"));
    DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
}

这里的配置文件的路径包含了两种:

  • classpath下寻找配置文件
    • /
    • /config/
  • 在当前路径下寻找
    • ./
    • ./config/
    • ./config/*/

相比之下,在当前路径下寻找多了一个正则匹配的项,因此扫描的路径更广

构造器

ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
            ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles,
            ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
        // 创建数据绑定器
        Binder binder = Binder.get(environment);
        // 判断是否包含了spring.config.use-legacy-processing的配置, 
        UseLegacyConfigProcessingException.throwIfRequested(binder);
        // 创建日志工厂对象
        this.logFactory = logFactory;
        // 获取日志打印对象
        this.logger = logFactory.getLog(getClass());
        // spring.config.on-not-found配置绑定, 如果没有配置,则使用FAIL对象
        this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
                .orElse(ConfigDataNotFoundAction.FAIL);
        // 启动上下文
        this.bootstrapContext = bootstrapContext;
        // 环境变量
        this.environment = environment;
        // ConfigDataLocationResolver获取并创建
        // 从spring.factories中加载ConfigDataLocationResolver并实例化
        this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
        this.additionalProfiles = additionalProfiles;
        this.environmentUpdateListener = (environmentUpdateListener != null) ? environmentUpdateListener
                : ConfigDataEnvironmentUpdateListener.NONE;
        // ConfigDataLoaders对象, 用于加载配置文件
        this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, resourceLoader.getClassLoader());
        // 创建ConfigDataEnvironmentContributors对象
        this.contributors = createContributors(binder);
    }

该类的初始化是比较复杂的,包含了多的功能都在初始化的时候完成。因此按照从上到下的顺序一次查看对应的源码。

createConfigDataLocationResolvers()

protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
        ConfigurableBootstrapContext bootstrapContext, Binder binder, ResourceLoader resourceLoader) {
    return new ConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
}

该类方法主要就是为了创建ConfigDataLocationResolvers对象,该对象主要持有ConfigDataLocationResolver列表,以便后期使用。

ConfigDataLocationResolvers

ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
            Binder binder, ResourceLoader resourceLoader) {
        // 在这一步中,最主要的就是加载ConfigDataLocationResolver的实例,这个实例加载出来的主要是名称的列表
        this(logFactory, bootstrapContext, binder, resourceLoader, SpringFactoriesLoader
                .loadFactoryNames(ConfigDataLocationResolver.class, resourceLoader.getClassLoader()));
    }
    ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
            Binder binder, ResourceLoader resourceLoader, List<String> names) {
        // 这一步主要是创建实例化对象,标记了需要实例化的类为ConfigDataLocationResolver, 并且向可用参数中放入了可能得一下参数列表
        // 当在实例化Instantiator的时候,就可以利用对应的参数信息
        Instantiator<ConfigDataLocationResolver<?>> instantiator = new Instantiator<>(ConfigDataLocationResolver.class,
                (availableParameters) -> {
                    availableParameters.add(Log.class, logFactory::getLog);
                    availableParameters.add(DeferredLogFactory.class, logFactory);
                    availableParameters.add(Binder.class, binder);
                    availableParameters.add(ResourceLoader.class, resourceLoader);
                    availableParameters.add(ConfigurableBootstrapContext.class, bootstrapContext);
                    availableParameters.add(BootstrapContext.class, bootstrapContext);
                    availableParameters.add(BootstrapRegistry.class, bootstrapContext);
                });
        // 这里对ConfigDataLocationResolver进行实例化,并且对实例化的列表进行重排序
        this.resolvers = reorder(instantiator.instantiate(resourceLoader.getClassLoader(), names));
    }

对于Instantiator的初始化逻辑而言,主要是根据构造器的参数类型,获取对应的绑定对应,这样就可以实现对象的初始化操作。这部分代码就不做分析,感兴趣可以查看对应的源码部分。通过该方法我们就拿到了ConfigDataLocationResolver对象的列表。

ConfigDataLoaders

该类的初始化,主要是对配置文件加载的实现具体逻辑,这里主要看下在该类初始化的时候,做了那些主要的事情。

ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ClassLoader classLoader) {
    // 通过SPI的方式加载ConfigDataLoader对象
    this(logFactory, bootstrapContext, classLoader,
        SpringFactoriesLoader.loadFactoryNames(ConfigDataLoader.class, classLoader));
}

ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ClassLoader classLoader, List<String> names) {
    this.logger = logFactory.getLog(getClass());
    Instantiator<ConfigDataLoader<?>> instantiator = new Instantiator<>(ConfigDataLoader.class,
        (availableParameters) -> {
            availableParameters.add(Log.class, logFactory::getLog);
            availableParameters.add(DeferredLogFactory.class, logFactory);
            availableParameters.add(ConfigurableBootstrapContext.class, bootstrapContext);
            availableParameters.add(BootstrapContext.class, bootstrapContext);
            availableParameters.add(BootstrapRegistry.class, bootstrapContext);
        });
    // 初始化ConfigDataLoader对象
    this.loaders = instantiator.instantiate(classLoader, names);
    // 因为ConfigDataLoader定义为泛型,因此这里通过反射的方式获取泛型的具体类型
    // 因此这里记录了加载资源类型的列表
    this.resourceTypes = getResourceTypes(this.loaders);
}

从这里就可以看到,其实对于类型初始化方式都很像,都是通过Instantiator类型来完成的,然后定义可能得参数列表

createContributors()

该方法主要用于创建ConfigDataEnvironmentContributors对象,该对象持有了ConfigDataEnvironmentContributor对象的列表,具体源码如下:

private ConfigDataEnvironmentContributors createContributors(Binder binder) {
    //记录日志
    this.logger.trace("Building config data environment contributors");
    // 获取当前环境中的配置源列表
    MutablePropertySources propertySources = this.environment.getPropertySources();
    List<ConfigDataEnvironmentContributor> contributors = new ArrayList<>(propertySources.size() + 10);
    PropertySource<?> defaultPropertySource = null;
    // 遍历配置元对象
    for (PropertySource<?> propertySource : propertySources) {
        // 判断是否为默认配置源
        if (DefaultPropertiesPropertySource.hasMatchingName(propertySource)) {
            defaultPropertySource = propertySource;
        }
        else {
            // 记录日志
            this.logger.trace(LogMessage.format("Creating wrapped config data contributor for '%s'",
                    propertySource.getName()));
            // 创建ConfigDataEnvironmentContributor对象,并放入列表
            contributors.add(ConfigDataEnvironmentContributor.ofExisting(propertySource));
        }
    }

    // 这里是获取初始化的导入Contributors, 主要包括了:
    // spring.config.import 配置
    // spring.config.additional-location 配置
    // spring.config.location 配置
    contributors.addAll(getInitialImportContributors(binder));
    if (defaultPropertySource != null) {
        this.logger.trace("Creating wrapped config data contributor for default property source");
        contributors.add(ConfigDataEnvironmentContributor.ofExisting(defaultPropertySource));
    }
    // 创建ConfigDataEnvironmentContributors对象
    return createContributors(contributors);
}

因此在代码执行完成后,这里的Contributors主要是根据当前已经加载的Source和需要加载的Source几部分组成,因此Contributors的列表代码大致如下:

Contributor有几部分组成,可以看到Kind值有很多种,

  • EXISTING:表示了已经加载的配置列表
  • INTIAL_IMPORT:表示了初始化导入,但这部分的配置文件导入还没有完成

processAndApply()

void processAndApply() {
    // 创建ConfigDataImporter对象
    ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
            this.loaders);
    // 注册启动binder对象
    registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
    // 执行初始化, 包括了加载配置文件信息
    ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
    // 创建ConfigDataActivationContext对象
    ConfigDataActivationContext activationContext = createActivationContext(contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
    // 执行在没有指定profile的时候配置加载
    contributors = processWithoutProfiles(contributors, importer, activationContext);
    // 主要从现有的配置源中加载spring.profiles.include信息,并将Profile信息存放在ConfigDataActivationContext中
    activationContext = withProfiles(contributors, activationContext);
    // 当设置了Profiles信息的时候,重新加载profile文件信息,例如根据激活的profile信息,则对应加载bootstrap-default.yml等
    contributors = processWithProfiles(contributors, importer, activationContext);
    // 将配置信息应用到environment环境中
    applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
            importer.getOptionalLocations());
}

该方法处理中,主要包含了三个关键的步骤:

  • 根据配置信息加载配置文件
  • 从已经加载的配置文件中加载Profile相关信息,然后加载与profile有关的配置文件
  • 将加载的配置文件应用到environment以便于使用

2.8 ConfigDataEnvironmentContributors

执行数据导入的时候,最终通过该类实现配置文件的导入工作。

withProcessedImports()

该方法会执行所有的活跃的Contributor对象,并且返回一个新的ConfigDataEnvironmentContributors实例对象。

ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
        ConfigDataActivationContext activationContext) {
    // 获取导入的阶段
    // 当activationContext为空的时候,返回:BEFORE_PROFILE_ACTIVATION
    // 当activetionContext不为空,并且Profiles不为空的时候,则返回:AFTER_PROFILE_ACTIVATION
    ImportPhase importPhase = ImportPhase.get(activationContext);
    this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
            (activationContext != null) ? activationContext : "no activation context"));
    ConfigDataEnvironmentContributors result = this;
    int processed = 0;
    while (true) {
        // 获取下一个需要执行Contributor对象, 判断是否需要执行,需要满足以下几个条件:
        // 1. 当前contributor的kink 等于 UNBOUND_IMPORT
        // 或者
        // 1. contributor持有的配置properties为空
        // 2. contributor对应的properties处于active状态
        ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);

        // 如果contributor为空,则表示所有的contributor已经执行完成
        if (contributor == null) {
            this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
            return result;
        }

        // 判断需要执行的contributor的kind是否为UNBOUND_IMPORT, 如果是,则重新构建ConfigDataEnvironmentContributors并继续遍历Contributor
        // UNBOUND_IMPORT表示了当前的ConfigData数据依赖了其他的Contributor配置
        if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
            // 创建UNBOUND_IMPORT的kind 类型contributor
            ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(result, activationContext);
            result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                    result.getRoot().withReplacement(contributor, bound));
            continue;
        }

        // 创建ConfigDataLocationResolverContext上下文对象
        ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
                result, contributor, activationContext);
        // 创建ConfigDataLoaderContext上下文对象
        ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
        // 获取当前contributor需要加载的配置文件路径列表
        List<ConfigDataLocation> imports = contributor.getImports();
        this.logger.trace(LogMessage.format("Processing imports %s", imports));
        // 根据需要加载的文件路径列表,从路径加载对应文件,并存储在ConfigData中
        Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
                locationResolverContext, loaderContext, imports);
        this.logger.trace(LogMessage.of(() -> getImportedMessage(imported.keySet())));

        // 这里主要是创建一个包含当前Contributor的一个ConfigDataEnvironmentContributor对象
        ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
                asContributors(imported));
        // 这里会生成一个新的ConfigDataEnvironmentContributors对象,新的对象中会将当前的contributor对象的处理结果进行替换
        // 这样做的目的是:当有resource被处理完成后,就不需要重复处理了。
        // 当有的资源依赖于其他的contributor处理结果时,还有可以被继续处理的机会
        result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                result.getRoot().withReplacement(contributor, contributorAndChildren));
        processed++;
    }
}

2.9 ConfigDataImporter

该类主要负责的是对配置文件数据的加载,因此主要关注对应的加载过程和源码。

resolveAndLoad()

    Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,
        ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,
        List<ConfigDataLocation> locations) {
    try {
        Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
        // 根据locations定义,封装成为ConfigDataResolutionResult对象,该对象中包含了需要处理的配置文件路径信息。
        // 这里的处理主要通过SPI加载到的ConfigDataLocationResolver实例,并判断ConfigDataLocationResolver是否能够处理路径的文件
        // 这里其实只是做了一个判断,如果不满足的ConfigDataLocationResolver会被过滤掉,最终不会执行
        List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
        // 根据ConfigDataResolutionResult结果,加载对应配置文件
        return load(loaderContext, resolved);
    }
    catch (IOException ex) {
        throw new IllegalStateException("IO error on loading imports from " + locations, ex);
    }
}

load()

private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext,
List<ConfigDataResolutionResult> candidates) throws IOException {
    Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();

    // 开始遍历配置文件路径结果
    for (int i = candidates.size() - 1; i >= 0; i--) {
        ConfigDataResolutionResult candidate = candidates.get(i);
        // 获取文件路径
        ConfigDataLocation location = candidate.getLocation();
        // 获取文件资源
        ConfigDataResource resource = candidate.getResource();
        // 资源是否为可选,该值是在创建ConfigDataResource的时候,需要手动设置该值true/false; 默认值为false
        if (resource.isOptional()) {
            this.optionalLocations.add(location);
        }
        // 判断当前的资源时候覅偶已经被加载, 如果资源已经被加载,则放入到loadedLocations列表中
        if (this.loaded.contains(resource)) {
            this.loadedLocations.add(location);
        }
        else {
            try {
                // 代码走到这里,就表示了资源没有被加载, 通过ConfigDataLoader进行配置文件加载
                ConfigData loaded = this.loaders.load(loaderContext, resource);
                // 不为空,则表示资源加载成功, 则进行资源状态的映射和存储
                if (loaded != null) {
                    this.loaded.add(resource);
                    this.loadedLocations.add(location);
                    result.put(candidate, loaded);
                }
            }
            catch (ConfigDataNotFoundException ex) {
                handle(ex, location, resource);
            }
        }
    }
    return Collections.unmodifiableMap(result);
}

2.10 ConfigDataLoaders

该类则主要用于加载配置文件的具体入口。该类主要功能实际上是提供了遍历的功能,主要持有ConfigDataLoader列表,并提供遍历调用和对异常结果的处理。

load()

<R extends ConfigDataResource> ConfigData load(ConfigDataLoaderContext context, R resource) throws IOException {
    // 根据resource类型获取ConfigDataLoader对象, 这里的resource实际上就是泛型中定义的resource类型
    ConfigDataLoader<R> loader = getLoader(context, resource);
    this.logger.trace(LogMessage.of(() -> "Loading " + resource + " using loader " + loader.getClass().getName()));
    // 调用load方法加载配置文件。
    // 配置文件的加载每个Loader的实现各异
    return loader.load(context, resource);
}

getLoader()方法主要是根据Resource具体的类型来获取对应的ConfigDataLoader类,这样就可以直接调用到具体的加载逻辑,因此getLoader()的方法可以自行查看。

2.11 StandardConfigDataLoader

在通用的配置文件加载逻辑中,只关注该类就可以了。因为该类实际上就是那些常用配置文件加载的地方。因此就查看该类的源码即可。

load()

public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResource resource)
throws IOException, ConfigDataNotFoundException {
    // 如果资源目标是一个文件夹,则返回空配置数据
    if (resource.isEmptyDirectory()) {
        return ConfigData.EMPTY;
    }

    // 如果配置文件不存在,则抛出异常
    ConfigDataResourceNotFoundException.throwIfDoesNotExist(resource, resource.getResource());
    // 获取资源文件的引用
    StandardConfigDataReference reference = resource.getReference();
    // 获取资源文件源对象
    Resource originTrackedResource = OriginTrackedResource.of(resource.getResource(),
        Origin.from(reference.getConfigDataLocation()));
    String name = String.format("Config resource '%s' via location '%s'", resource,
        reference.getConfigDataLocation());
    // 加载资源对象,这里资源的对象会根据后缀的不同,使用不同的对象进行加载。
    List<PropertySource<?>> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource);
    // 判断资源是否制定了profile, 如果制定,则包装成为PropertySourceOptions对象
    PropertySourceOptions options = (resource.getProfile() != null) ? PROFILE_SPECIFIC : NON_PROFILE_SPECIFIC;
    return new ConfigData(propertySources, options);
}

2.12 ConfigDataLocationResolvers

该类也是很重要的类,主要的用处在于需要扫描特定的路径下的文件,并判断对应的配置文件是否存在:

  • 当指定spring.config.name的时候,加载对应配置文件名称和与profile有关的配置文件
  • 当未指定spring.config.name的时候,默认加载application以及与profile有关的配置文件

resolve()

private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver,
    ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
    
    // 这里主要用于获取默认的配置文件信息,这里的加载不带有任何环境信息
    List<ConfigDataResolutionResult> resolved = resolve(location, false, () -> resolver.resolve(context, location));
    if (profiles == null) {
        return resolved;
    }

    // 这里的文件的配置,是带有Profile信息的配置文件加载。
    // 当第一次加载的时候,其实并没有profiles信息,因此这里是加载不到的
    // 当默认application配置中配置了spring.profiles.active的时候,则带有profiles配置,
    // 该配置就会生效
    List<ConfigDataResolutionResult> profileSpecific = resolve(location, true,
        () -> resolver.resolveProfileSpecific(context, location, profiles));
    return merge(resolved, profileSpecific);
}

2.13 合并配置

在以上的代码中我们知道,因为spring-cloud的引入,会自动的引入bootstrap配置以及与spring-cloud相关的配置,但是在创建的时候,却是单独的加载以及创建了一个父容器,这是就会涉及到对两个环境配置的合并。

在ApplicationContext被加载以前,会做容器初始化必要的环境准备,在加载流程中可以分为三个步骤:

prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);

在prepareContext中就会包含了环境配置合并的操作。

context.setEnvironment(environment);
postProcessApplicationContext(context);
// 执行初始化器
applyInitializers(context);
listeners.contextPrepared(context);

BootstrapApplicationListener#AncestorInitializer

该类实在bootstrap的从容器初始化完成后,被加入到SpringApplication的初始化器列表哦之中,具体可以参考BootstrapApplicationListener处理事件的源码。

private void reorderSources(ConfigurableEnvironment environment) {
    // 从配置源中移除springCloudDefaultProperties配置
    PropertySource<?> removed = environment.getPropertySources().remove(DEFAULT_PROPERTIES);
    // 判断是否为ExtendedDefaultPropertySource的子类
    if (removed instanceof ExtendedDefaultPropertySource) {
        ExtendedDefaultPropertySource defaultProperties = (ExtendedDefaultPropertySource) removed;
        // 将springCloudDefaultProperties重新加入到配置源中
        environment.getPropertySources()
                .addLast(new MapPropertySource(DEFAULT_PROPERTIES, defaultProperties.getSource()));
        // 遍历springCloudDefaultProperties中的已经加载的配置源信息,这里就主要包括了
        // bootstrap, bootstrap-profile等配置文件内容
        // 这里我们可以知道,bootstrap的优先级在application之后的, 因此在启动的过程中,可以知道bootstrap的优先级很低
        for (PropertySource<?> source : defaultProperties.getPropertySources().getPropertySources()) {
            if (!environment.getPropertySources().contains(source.getName())) {
                environment.getPropertySources().addBefore(DEFAULT_PROPERTIES, source);
            }
        }
    }
}

以上就是spring-boot在处理环境配置时的逻辑。后续会有关于nacos做配置管理的相关文章分析。

专注着

一个奋斗在编程路上的小伙

文章评论

  • 头像
    yt

    真是太棒了,给作者加鸡腿

    4月 12, 2023
  • 头像
    wp

    学到了,学到了

    4月 11, 2023