springboot创建并配置环境3 - 配置扩展属性(下)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: springboot创建并配置环境3 - 配置扩展属性(下)

一、介绍

上一篇文章:springboot如何创建并配置环境3 - 配置扩展属性(上) 中我们介绍了springboot对配置文件的处理逻辑,但是由于篇幅过长,决定分上下集两部分讲解。

本文基于以下版本进行展开:

  • jdk:1.8
  • springboot:2.4.3

另外:由于篇幅过长,决定分四集文章来讲解分析

一、创建环境

二、配置基础环境

三、配置扩展属性(上)

四、配置扩展属性(下)

二、对contributors中的配置属性进行处理

上集对processAndApply()方法的分析中,概括来讲就是分四步:①在确定profiles前处理contributors中的配置属性。②确定profiles。③在确定profiles后处理contributors中的配置属性。④将contributors中的配置属性应用到当前运行环境Environment中。

因此下面我们来分析以下逻辑

  • 处理contributors中的配置属性
  • 确定profiles
  • 将配置属性应用到当前运行环境Environment中。

三、处理contributors中的配置属性

该逻辑通过三个方法完成,分别是processInitial()processWithoutProfiles()processWithProfiles()。而这三个方法其实内部实现都是通过调用contributors对象的withProcessedImports()方法完成的,他们之间的区别就是是否传入确定的profiles

处理contributors对象中的配置属性.png

因此我们主要对withProcessedImports()方法进行分析。

先来看一下该方法的源码如下:

ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
            ConfigDataActivationContext activationContext) {
   
    // 根据环境激活上下文获取导入阶段,所谓导入阶段为profiles激活前和profiles激活后两个阶段
    ImportPhase importPhase = ImportPhase.get(activationContext);
    this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
                                        (activationContext != null) ? activationContext : "no activation context"));
    // this表示contributors表示的对象,将该对象赋值给result,本质上仍然是保存contributor集合的contributors对象
    ConfigDataEnvironmentContributors result = this;
    int processed = 0;
    while (true) {
   
        // 从contributor集合中获取下一个将要处理的contributor
        ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
        if (contributor == null) {
   
            // 如果contributor集合中没有要处理的元素,则返回该集合
            this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
            return result;
        }
        if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
   
            // 如果contributor的类型为UNBOUND_IMPORT(未绑定导入)

            // 从当前正处理的contributor对象中获取ConfigurationPropertySource
            Iterable<ConfigurationPropertySource> sources = Collections
                .singleton(contributor.getConfigurationPropertySource());
            // 创建placeholder解析器,用来解析${}
            PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(
                result, activationContext, true);
            // 创建binder对象,binder中包含了ConfigurationPropertySource解析器
            Binder binder = new Binder(sources, placeholdersResolver, null, null, null);
            // 将当前contributor复制给一个新的对象,并将类型修改为BOUND_IMPORT(已绑定导入)
            ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);
            // 根据原contributor集合重新创建一个Contributors对象,并将当前正处理的contributor对象进行替换
            result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                                                           result.getRoot().withReplacement(contributor, bound));
            continue;
        }
        // 创建位置解析器上下文
        ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
            result, contributor, activationContext);
        // 创建配置数据加载器上下文
        ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
        // 从contributor中获取imports,imports中包含了当前contributor对象要处理的配置文件路径
        List<ConfigDataLocation> imports = contributor.getImports();
        this.logger.trace(LogMessage.format("Processing imports %s", imports));
        // 调用importer的resolveAndLoad()方法来解析并读取配置数据
        // 在importer中已经包含了位置解析器、配置数据加载器。
        // 返回值是一个map对象,其中key中包含了配置文件的路径及其资源,value中包含的是配置文件中的配置数据
        Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
                                                                                          locationResolverContext, 
                                                                                       loaderContext, 
                                                                                       imports);
        this.logger.trace(LogMessage.of(() -> getImportedMessage(imported.keySet())));
        // 将读取到的配置通过父子关系,设置为当前正处理的contributor的children,
        // asContributors()方法将读取到的配置封装成contributor对象并设置其类型为UNBOUND_IMPORT(未绑定导入)
        ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
                                                                                           asContributors(imported));
        // 根据原contributor集合重新创建一个Contributors对象,并将当前正处理的contributor对象进行替换
        result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                                                       result.getRoot().withReplacement(contributor, contributorAndChildren));
        processed++;
    }
}

其中,在contributor类型为UNBOUND_IMPORT的if代码块中,主要是把该contributor的类型修改为BOUND_IMPORT,并作为一个新的contributor对象将原contributor对象进行替换。如下所示

将contributor对象的类型进行修改.png

下面,我们分析withProcessedImports()方法剩余部分逻辑:

withProcessedImports方法的主要逻辑.png

其中,我们先进入importer.resolveAndLoad()方法,该方法返回一个map对象(key为配置文件资源对象,value为从配置文件资源中加载的配置属性),其内部逻辑分两部分:①解析配置文件位置和资源,②加载配置属性

resolveAndLoad()方法.png

1. 解析配置文件位置和资源

我们进入resolve()方法查看如何解析配置文件的位置和资源。

对resolve()方法的调用.png

从上面源码中可以看到,resolve()方法对其locations参数(配置文件位置)进行遍历,对每一个配置文件位置再调用重载的resolve()方法进行解析。真正的解析过程是通过调用配置文件位置解析器resolversresolve()方法实现的。

调用配置文件位置解析器的resolve()方法.png

前面讲过,springboot提供了两个配置文件解析器:①ConfigTreeConfigDataLocationResolver;②StandardConfigDataLocationResolver。我们从这两个解析器的isResolvable()方法便可以判断出区别:前者用于解析带有前缀configtree:的配置文件路径;后者解析任意配置文件路径。

下面我们以StandardConfigDataLocationResolver为例,分析如何解析配置文件位置。

其中resolve()方法和resolveProfileSpecific()方法逻辑大致相同,只是后者携带有效的profile参数

因此我们分析其resolve()方法,该方法先获取配置文件资源的引用再根据该文件引用,获取该文件资源

获取配置文件资源的引用1.png

下面我们看如何获取配置文件资源的引用,以目录为例,查看getReferencesForDirectory()方法

获取配置文件资源的引用.png

进一步查看配置文件引用的构造方法

配置文件资源引用的构造方法.png

由此我们便可以知道,springboot是如何根据spring.profiles.active属性确定profiles对应的配置文件资源

当我们得到配置文件资源的引用后,通过该引用获取对应的配置文件资源

解析配置文件资源的引用得到文件资源.png

我们再回到配置文件位置解析器resolversresolve()方法。

将配置文件资源封装到解析结果中.png

下面我们回到resolveAndLoad()方法,其中resolved集合中包含了默认指定的以及指定profile对应的配置文件资源。然后在调用load()方法,从配置文件资源中加载配置属性即可。

resolveAndLoad()方法.png

2. 加载配置属性

这里我们关注resolveAndLoad()方法对load()方法的调用。

该方法以配置文件资源分析结果集合为参数,返回一个Map集合,其中key为配置文件资源分析结果,value为配置文件资源中的配置属性。

根据配置文件资源分析结果加载配置.png

进入加载器loadersload()方法,该方法用于加载指定配置文件资源中并返回该配置文件中的配置属性

调用配置属性加载器的load方法.png

从该方法中看到,配置属性加载器有两种,分别是ConfigTreeConfigDataLoaderStandardConfigDataLoader,还记得前面我们分析的配置文件位置解析器也有两个分别是ConfigTreeConfigDataLocationResolverStandardConfigDataLocationResolver,他们是一一对应的。

我们以StandardConfigDataLocationResolver为例,查看它的load()方法。

调用加载yaml或properties的load方法.png

从该方法中我们看到,对配置文件中配置属性的加载是通过配置属性加载器中的load()方法实现的,而该加载器又分为propertiesyaml两种。

load()方法将我们在配置文件中定义的配置属性进行加载,并转化为propertySource集合。再将该集合封装到ConfigData对象中并返回。

而该加载器中对配置文件资源中的配置属性的加载过程我们这里就不做分析了,请有兴趣的读者自查。

最后再回到withProcessedImports()方法

withProcessedImports方法的主要逻辑.png

resolveAndLoad()方法我们就分析结束了,该方法返回的imported对象为map集合,其中key为配置文件资源分析结果,value为配置文件资源中的配置属性。最后通过withChildren()方法将该map集合转为contributor对象并保存到children属性中,再通过withReplacement()方法将contributor对象更新

至此我们在配置文件中定义的所有配置属性均已保存到contributors对象中并返回。此时contributors对象的结构如下

contributors最终结构.png

四、确定当前运行环境激活的profile

此过程由processAndApply()方法中调用withProfiles()方法完成

processAndApply方法调用withProfiles.png

下面我们进入该方法源码查看

withProfiles方法.png

1. 获取附加的addtionalProfiles

从源码上看,addtionalProfiles属性是在ConfigDataEnvironment类的成员变量中直接定义的,且该属性是通过该类的构造方法设置的

class ConfigDataEnvironment {
   
    // ...
    private final Collection<String> additionalProfiles;    
    // ...

    ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
            ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles,
            ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
   
        // ...
        this.additionalProfiles = additionalProfiles;
        // ...
    }
}

通过该构造方法的调用链我们可以发现,该构造方法的调用如下所示

ConfigDataEnvironment构造方法的调用链.png

由此可知,附加profiles是从SpringApplication类中获取的,那么是否也是由SpringApplication类设置的呢?答案是肯定的。

SpringApplication类中有对应的方法定义

SpringApplication设置addtionalProfiles.png

因此我们可以在springboot的主启动方法中通过以下方式设置

设置addtionalProfiles示例.png

2. 获取spring.profiles.include定义的配置

进入getIncludedProfiles()方法

获取include指定的profiles.png

从该方法中看到,对contributors中的contributor进行遍历,从中获取key为spring.profiles.include的配置属性,将其添加到集合中并返回。

3. 获取spring.profiles.active定义的配置

进入Profiles的构造方法查看,

Profiles的构造方法.png

该构造方法中定义了三种profiles,分别是spring.profiles.group定义的profiles、spring.profiles.active定义的profiles和spring.profiles.default定义的profiles。我们逐个查看

  • spring.profiles.group定义的profiles

    Profiles的构造方法得知,springboot通过spring.profiles.group定义profiles分组,且定义方式为Map集合。我们通过下面示例说明

    配置文件中定义分组profiles.png

调式源码

配置文件设置分组profiles调式结果.png

  • spring.profiles.active定义的profiles

    Profiles的构造方法得知,springboot通过getActivatedProfiles()方法获取spring.profiles.active定义的profiles

    获取active定义的profiles.png

getActivatedProfiles()方法中,springboot获取spring.profiles.active定义的profiles,并将前面获取的additionalProfiles一同添加到集合中并返回,作为Profiles实例的activeProfiles属性。

  • spring.profiles.default定义的profiles

    Profiles的构造方法得知,springboot通过getDefaultProfiles()方法获取spring.profiles.default定义的profiles

    获取default定义的profiles.png

在`getDefaultProfiles()`方法中,springboot获取`spring.profiles.default`定义的profiles(默认为`default`),并将其添加到集合中并返回,作为`Profiles`实例的`defaultProfiles`属性。

最后,在将所有定义的profiles封装到Profiles实例后,通过activationContext.withProfiles()方法将该Profiles实例添加到profiles激活上下文中。

withProfiles方法.png

五、将contributors中保存的配置信息应用到当前运行环境中

下面我们便到达processAndApply()方法的最后一步,将contributors中保存的配置信息应用到当前运行环境

processAndApply方法调用applyToEnvironment.png

下面进入applyToEnvironment方法的源码

将配置属性应用到当前运行环境中.png

从源码可见,该方法虽然较长,但逻辑比较简单,就是将contributor集合中来自配置文件的配置属性添加到当前运行环境的配置属性集合中,然后对当前运行环境设置profiles。

此时,运行环境中的所有配置属性均已设置完毕,包含来自系统的配置属性以及来自配置文件的配置属性等,如下所示

运行环境中保存的属性4.png




至此,我们对springboot创建并配置运行环境的整个过程就分析结束了,其过程虽然繁琐,但如果认真梳理,其处理逻辑并不复杂,只需我们在阅读源码时耐得住寂寞沉得住气即可。

六、总结

  • 在启动环境中主要保存配置信息和当前操作系统的配置信息以及环境变量
  • 针对不同的应用程序类型,springboot创建对应的运行环境实例,如StandardEnvironmentStandardServletEnvironmentStandardReactiveWebEnvironment
  • 在创建运行环境实例时,其构造器内部就已经首先将系统属性环境变量保存到其内部属性中了。
  • 通过观察者模式发布环境准备就绪事件,由监听该事件的各种监听器针对该事件进行不同的逻辑处理。
  • 涉及到的设计模式
    • 观察者模式:发布环境准备就绪事件,由对应的监听器执行逻辑
    • 工厂模式:环境后处理器工厂
  • 通过contributor对象临时保存所有配置文件中的配置属性
  • 配置文件的格式有多种,propertiesxmlyaml、以及yml
  • 配置文件的位置有多种,classpath:/classpath:/config/file:./file:./config/file:./config/*/、以及指定的位置spring.config.location
  • 最后将contributor对象集合中的配置属性再应用到运行环境中。

点此进入上一集:配置扩展属性(上)




纸上得来终觉浅,绝知此事要躬行。

————————我是万万岁,我们下期再见————————

相关文章
|
8天前
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
28 0
|
1月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
42 4
|
1月前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
36 0
|
12天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
25 2
 SpringBoot入门(7)- 配置热部署devtools工具
|
10天前
|
Java 数据库连接
SpringBoot配置多数据源实战
第四届光学与机器视觉国际学术会议(ICOMV 2025) 2025 4th International Conference on Optics and Machine Vision
38 8
|
8天前
|
Java 数据库连接 数据库
springboot启动配置文件-bootstrap.yml常用基本配置
以上是一些常用的基本配置项,在实际应用中可能会根据需求有所变化。通过合理配置 `bootstrap.yml`文件,可以确保应用程序在启动阶段加载正确的配置,并顺利启动运行。
15 2
|
20天前
|
Java Spring 容器
SpringBoot读取配置文件的6种方式,包括:通过Environment、@PropertySource、@ConfigurationProperties、@Value读取配置信息
SpringBoot读取配置文件的6种方式,包括:通过Environment、@PropertySource、@ConfigurationProperties、@Value读取配置信息
43 3
|
22天前
|
Java 开发者 Spring
精通SpringBoot:16个扩展接口精讲
【10月更文挑战第16天】 SpringBoot以其简化的配置和强大的扩展性,成为了Java开发者的首选框架之一。SpringBoot提供了一系列的扩展接口,使得开发者能够灵活地定制和扩展应用的行为。掌握这些扩展接口,能够帮助我们写出更加优雅和高效的代码。本文将详细介绍16个SpringBoot的扩展接口,并探讨它们在实际开发中的应用。
37 1
|
1月前
|
druid Java Maven
|
26天前
|
监控 Java 开发者
掌握SpringBoot扩展接口:提升代码优雅度的16个技巧
【10月更文挑战第20天】 SpringBoot以其简化配置和快速开发而受到开发者的青睐。除了基本的CRUD操作外,SpringBoot还提供了丰富的扩展接口,让我们能够更灵活地定制和扩展应用。以下是16个常用的SpringBoot扩展接口,掌握它们将帮助你写出更加优雅的代码。
46 0