盘点 SpringBoot : Application配置的读取流程

1,824 阅读9分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一. 前言

文章目的 :

  • 梳理 Applicaiton 的加载方式
  • 梳理 Profile 的处理

Spring-enviroment.jpg

二 . 扫描的触发

启动的源头任然是SpringApplication#run , 回顾之前的一篇源码 , 在 SpringApplication 中 ,会执行一段代码 :

ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

而一切的起点就是那里 , SpringBoot 通过这一句加载所有的环境信息 :

2.1 SpringApplication # prepareEnvironment

在该环节中 , 对 Environment 进行操作的处理 , 其中包括几个主要的操作 :

  1. ConfigurableEnvironment 的生成
  2. configureEnvironment 细粒度处理
  3. 对 configurationProperties 属性进行处理
  4. 发布 listener 处理执行不同类型配置文件的处理
  5. 将 environment 绑定到 SpringApplication
C- SpringApplication # prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
    // 内部通过 WebApplicationType 生成不同的 Environment (可以set 自己的 Environment)
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 重写此方法以完全控制环境自定义,或者重写上述方法之一以分别对属性源或概要文件进行细粒度控制。
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 对 configurationProperties 属性进行处理 
    ConfigurationPropertySources.attach(environment);
    // 发布 listener 处理
    listeners.environmentPrepared(environment);
    // 将 environment 绑定到 SpringApplication
    bindToSpringApplication(environment); -> M1_25
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

2.1.1 ConfigurableEnvironment 的生成

此处会生成一个 StandardServletEnvironment , 他是 ConfigurableEnvironment 的实现类 , 该对象中存在2个方法 , 分别配置了多个 Source 对象

  • customizePropertySources(MutablePropertySources propertySources)
  • initPropertySources(ServletContext servletContext,ServletConfig servletConfig)

// Servlet上下文初始化参数属性源名称
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
// Servlet config init parameters属性源名称
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

// 加载系统属性
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";

protected void customizePropertySources(MutablePropertySources propertySources) {

    // 这2步主要添加2个空对象
    propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
    propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
    
    
    if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
        propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
    }
    
    // 伪代码 , 来自于父类 , 此处用于加载系统属性 -> PRO:0001
    propertySources.addLast(
        new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
    propertySources.addLast(
        new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}

public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
    WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}


// PS : 这里的多个Sources 将会用于优先级处理 
SERVLET_CONFIG_PROPERTY_SOURCE_NAME > SERVLET_CONTEXT_PROPERTY_SOURCE_NAME > JNDI_PROPERTY_SOURCE_NAME


// [PRO:0001]  系统属性的获取方式 ?
return (Map) System.getProperties();

[PRO:0001] 系统属性大概样式 >>> system_config.jpg

其中最主要的就是第 4 步 , 该步骤中扫描不同的配置处理类

补充 : getOrCreateEnvironment 的获取

private ConfigurableEnvironment getOrCreateEnvironment() {
   if (this.environment != null) {
      return this.environment;
   }
   switch (this.webApplicationType) {
   case SERVLET:
      return new StandardServletEnvironment();
   case REACTIVE:
      return new StandardReactiveWebEnvironment();
   default:
      return new StandardEnvironment();
   }
}

2.2 发布 listener 处理执行不同类型配置文件的处理

前面主要是获取 SystemSource , 还没有正式开始 , 我们从 prepareEnvironment 第四步开始看 , 来看看后面怎么处理 :

Step 1 : 发布 Listener , 推动 Enviroment 处理

这里不需要深入太多 , 标准的 Listener 发布方式 , 这里主要发布的是 ApplicationEnvironmentPreparedEvent

C- SpringApplicationRunListeners
void environmentPrepared(ConfigurableEnvironment environment) {
    for (SpringApplicationRunListener listener : this.listeners) {
         // PS : 此处使用的 Listener 为  ApplicationEnvironmentPreparedEvent
        listener.environmentPrepared(environment);
    }
}

Step 2 :ConfigFileApplicationListener 处理该事件

核心的处理方式就是 EnvironmentPostProcessor 的循环 , 后面所有的操作均在其中完成 :

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    postProcessors.add(this);
    AnnotationAwareOrderComparator.sort(postProcessors);
    // 可以看到 , 此处还是通过 Processors 进行处理 -> PS:0001
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }
}
     

三 . postProcessors 的处理

前面看了 , 通过调用 postProcessor.postProcessEnvironment 来实现不同的环境加载 , 可以看到 , 此处主要有5个 postProcessors :

PS:0001 ConfigFileApplicationListener 中 postProcessors 有哪些

config_process.jpg

- SystemEnvironmentPropertySourceEnvironmentPostProcessor
- SpringApplicationJsonEnvironmentPostProcessor
- CloudFoundryVcapEnvironmentPostProcessor
- ConfigFileApplicationListener(没错 , 他本身也是个 EnvironmentPostProcessor)
- DebugAgentEnvironmentPostProcessor

3.1 SystemEnvironmentPropertySourceEnvironmentPostProcessor

该类主要是对 systemEnvironment 进行处理 , 前面看到了 , 实际上前面已经拿到 SystemSource , 此处是对这些配置的二次处理 :

public void postProcessEnvironment(
    ConfigurableEnvironment environment, SpringApplication application) {
    // >>> systemEnvironment
    String sourceName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
    // 从 environment 中获取 systemEnvironment 的所有属性
    PropertySource<?> propertySource = environment.getPropertySources().get(sourceName);
    if (propertySource != null) {
        replacePropertySource(environment, sourceName, propertySource);
    }
}

// 此处主要是把 SystemEnvironmentPropertySource 封装成了 OriginAwareSystemEnvironmentPropertySource
private void replacePropertySource(ConfigurableEnvironment environment, String sourceName,
    PropertySource<?> propertySource) {
    Map<String, Object> originalSource = (Map<String, Object>) propertySource.getSource();
    SystemEnvironmentPropertySource source 
        = new OriginAwareSystemEnvironmentPropertySource(sourceName,originalSource);
    environment.getPropertySources().replace(sourceName, source);
}

[Pro] OriginAwareSystemEnvironmentPropertySource 是什么 ?

// OriginAwareSystemEnvironmentPropertySource 是SystemEnvironmentPropertySource 的子类, 
// 提供了获取Origin的方法,即返回SystemEnvironmentOrigin对象

SystemEnvironmentOrigin 提供对原始属性名的访问

3.2 SpringApplicationJsonEnvironmentPostProcessor

该类的主要使用形式是在启动 jar 包时使用 , 这种方式的优先级最高:

java -jar xxx.jar --spring.application.json={\"username\":\"ant-black\"}

image.png

// 从 spring.application.json或等价的SPRING_APPLICATION_JSON中解析JSON ,
// 并将其作为映射属性源添加到环境中

public void postProcessEnvironment(
    ConfigurableEnvironment environment, SpringApplication application) {
    MutablePropertySources propertySources = environment.getPropertySources();
    propertySources.stream().map(JsonPropertyValue::get).filter(Objects::nonNull).findFirst()
                .ifPresent((v) -> processJson(environment, v));
}

private void processJson(ConfigurableEnvironment environment, JsonPropertyValue propertyValue) {
    JsonParser parser = JsonParserFactory.getJsonParser();
    // 使用 JsonParser 将 JSON 解析为 Map 集合
    Map<String, Object> map = parser.parseMap(propertyValue.getJson());
    if (!map.isEmpty()) {
        addJsonPropertySource(environment, new JsonPropertySource(propertyValue, flatten(map)));
    }
}

// 这里可以看到 , spring.application.json 添加的优先级是最高的
private void addJsonPropertySource(ConfigurableEnvironment environment, PropertySource<?> source) {
    MutablePropertySources sources = environment.getPropertySources();
    String name = findPropertySource(sources);
    if (sources.contains(name)) {
        // 添加优先级立即高于命名相对属性源的给定属性源对象
        sources.addBefore(name, source);
    } else {
        sources.addFirst(source);
    }
}	

3.3 CloudFoundryVcapEnvironmentPostProcessor

一个环境 postprocessor,它知道在现有环境中在哪里找到VCAP(也就是云计算)元数据。它解析VCAP_APPLICATION和VCAP_SERVICES元数据,并将其转储Environment。

这一块比较玄妙 , 首先要知道 VCAP 是什么 :

  • IBM Cloud 中有一个概念 : Cloud Foundry 应用程序
  • 在 Cloud Foundry 运行的应用程序通过存储在一个名为 VCAP services 的环境变量中的凭证获得对绑定服务实例的访问
  • VCAP_APPLICATION 是指云计算的应用单体元数据
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
        Properties properties = new Properties();
        JsonParser jsonParser = JsonParserFactory.getJsonParser();
        addWithPrefix(properties, getPropertiesFromApplication(environment, jsonParser), "vcap.application.");
        addWithPrefix(properties, getPropertiesFromServices(environment, jsonParser), "vcap.services.");
        MutablePropertySources propertySources = environment.getPropertySources();
        if (propertySources.contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) {
            propertySources.addAfter(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
                    new PropertiesPropertySource("vcap", properties));
        }
        else {
            propertySources.addFirst(new PropertiesPropertySource("vcap", properties));
        }
    }
}

简单来说 , 这个类主要是为了云计算的环境准备的 , 因为没有太多涉及 , 此处就不深入了

3.4 ConfigFileApplicationListener

这一块应该就是最核心的一块了 , 这里会对 Application 文件进行扫描处理 :

Step 1 : ConfigFileApplicationListener 的入口

由于 ConfigFileApplicationListener 本身继承了 EnvironmentPostProcessor , 其本身也会加载配置环境 :

// 将配置文件属性源添加到指定的环境
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    RandomValuePropertySource.addToEnvironment(environment);
    new Loader(environment, resourceLoader).load();
}

Step 2 : 构建 Loader 对象

// 来看一看 loader 对象 , 该对象会用于处理属性 Document 并且扫描
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
    // 
    this.environment = environment;
    // 解析器 , 用于解析占位符
    this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
    // 资源加载器
    this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
    // 属性加载器 , 这里是从 SpringFactories 中加载
    this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                    getClass().getClassLoader());
}


// 我们省略其中的一些流程 , 直接关注主流程 :
void load() {
    FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
        (defaultProperties) -> {
            this.profiles = new LinkedList<>();
            this.processedProfiles = new LinkedList<>();
            this.activatedProfiles = false;
            this.loaded = new LinkedHashMap<>();
            initializeProfiles();
            // 处理 Profile
            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                if (isDefaultProfile(profile)) {
                    addProfileToEnvironment(profile.getName());
                }
                load(profile, this::getPositiveProfileFilter,
                    addToLoaded(MutablePropertySources::addLast, false));
                this.processedProfiles.add(profile);
            }
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            addLoadedPropertySources();
            applyActiveProfiles(defaultProperties);
        });
}

Step 3 : 扫描所有的路径 , 发起处理

此处通过 getSearchLocations 获得所有的根路径 , 然后依次对根路径下的文件进行扫描

PS :因为处理的顺序不同 , 更靠后的优先级更高

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    // 获取所有的路径 , 并且对路径进行扫描
    getSearchLocations().forEach((location) -> {
        boolean isFolder = location.endsWith("/");
         // 注意 , 这里 SearchName 是获取所有扫描的文件名 , 可以通过 spring.config.name 配置 
         // 默认名称 application
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
    });
}

这里主要有四个默认路径 :

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/

Step 4 : 对路径进行依次处理

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
                DocumentConsumer consumer) {
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                return;
            }
        }
        throw new IllegalStateException(".....");
    }
    Set<String> processed = new HashSet<>();
    // PropertySourceLoader -> PS:0002
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        // 此处获取额外的属性 , 此处为 yml 和 yaml
        for (String fileExtension : loader.getFileExtensions()) {
            if (processed.add(fileExtension)) {
                  // 此处拿到的值为 classpath:/application
                loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,consumer);
            }
        }
    }
}

PS:0002 PropertySourceLoader 的类型

此处提供了 2 种 PropertySourceLoader ,分别是 PropertiesPropertySourceLoader (.properties)YamlPropertySourceLoader (支持 .yml .yaml')

Step 5 : loadForFileExtension 处理

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
                Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
    DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
    if (profile != null) {
        // 尝试配置文件中特定配置文件和配置文件段(gh-340)
        String profileSpecificFile = prefix + "-" + profile + fileExtension;
        load(loader, profileSpecificFile, profile, defaultFilter, consumer);
        load(loader, profileSpecificFile, profile, profileFilter, consumer);
        // 尝试分析我们已经处理过的文件中的特定部分
        for (Profile processedProfile : this.processedProfiles) {
            if (processedProfile != null) {
                String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
                load(loader, previouslyLoaded, profile, profileFilter, consumer);
            }
        }
    }
    // Also try the profile-specific section (if any) of the normal file
    load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

Step 6 : 对 Application 文件进行读取

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
                DocumentConsumer consumer) {
    try {
        Resource resource = this.resourceLoader.getResource(location);
        if (resource == null || !resource.exists()) {
            // PS : 当依次对四个路径进行处理的时候 , 如果不存在会由此逻辑返回
            return;
        }
        if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
            return;
        }
        String name = "applicationConfig: [" + location + "]";
         // 通过 Document 加载文档 , 此处为 applicationConfig: [classpath:/application.yml]
        List<Document> documents = loadDocuments(loader, name, resource);
        if (CollectionUtils.isEmpty(documents)) {
            // PS : 如果文档为空 ,会从此逻辑返回
            return;
        }
        List<Document> loaded = new ArrayList<>();
        for (Document document : documents) {
            if (filter.match(document)) {
                // 处理 Profiles 文件
                addActiveProfiles(document.getActiveProfiles());
                addIncludedProfiles(document.getIncludeProfiles());
                loaded.add(document);
            }
        }
        Collections.reverse(loaded);
        if (!loaded.isEmpty()) {
            loaded.forEach((document) -> consumer.accept(profile, document));
        }
    }catch (Exception ex) {
        throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
    }
}

Step 7 : 调用 loader 流程发起加载

该逻辑就是对Document 的处理了 , 也不用太深入


private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
                throws IOException {
    DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
    List<Document> documents = this.loadDocumentsCache.get(cacheKey);
    if (documents == null) {
         // 此处 loader 为 org.springframework.boot.env.YamlPropertySourceLoader
        List<PropertySource<?>> loaded = loader.load(name, resource);
        documents = asDocuments(loaded);
        this.loadDocumentsCache.put(cacheKey, documents);
    }
    return documents;
}


// PS : 此处暂时就可以不用关注了 , 后续主要是Loader 的加载逻辑 , 可以参考之前的文档 :
I- PropertySourceLoader
    C- YamlPropertySourceLoader
    C- PropertiesPropertySourceLoader

3.5 DebugAgentEnvironmentPostProcessor

作用 : 启用反应堆调试代理
开启 : 调试代理默认是启用的,除非 spring.reactor.debug-agent.enabled 配置属性设置为false

// 这是个特殊的类 , 目的是为了快速加载
private static final String DEBUGAGENT_ENABLED_CONFIG_KEY = "spring.reactor.debug-agent.enabled";


public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    if (ClassUtils.isPresent(REACTOR_DEBUGAGENT_CLASS, null)) {
        Boolean agentEnabled = environment.getProperty(DEBUGAGENT_ENABLED_CONFIG_KEY, Boolean.class);
        if (agentEnabled != Boolean.FALSE) {
            try {
                Class<?> debugAgent = Class.forName(REACTOR_DEBUGAGENT_CLASS);
                // 调用init 方法初始化
                debugAgent.getMethod("init").invoke(null);
            } catch (Exception ex) {
                throw new RuntimeException("Failed to init Reactor's debug agent");
            }
        }
    }
}


这个类的主要目的就是为了初始化 REACTOR_DEBUGAGENT_CLASS 类 , 而不是为了加载配置 , 这种方式更加快 (学到了!!!!)

四 . 知识点

4.1 Profile 的处理

Step 1 : 当第一步扫描 Document 的时候 , 会从其中获得 profile 属性 , 并且标注从来

image.png

Step 2 : 处理 Profiles

起因是上文 Step 6 中调用的 addActiveProfilesaddIncludedProfiles

void addActiveProfiles(Set<Profile> profiles) {
    if (profiles.isEmpty()) {
        return;
    }
    if (this.activatedProfiles) {
        return;
    }
    this.profiles.addAll(profiles);
    this.activatedProfiles = true;
    removeUnprocessedDefaultProfiles();
}

// 此处设置了 ConfigFileApplicationListener 的 Profiles 属性
private void addIncludedProfiles(Set<Profile> includeProfiles) {
    LinkedList<Profile> existingProfiles = new LinkedList<>(this.profiles);
    this.profiles.clear();
    this.profiles.addAll(includeProfiles);
    this.profiles.removeAll(this.processedProfiles);
    this.profiles.addAll(existingProfiles);
}

// PS:此处设置完成后 ,Profiles 就会由 default 变成 test 

到这里还没完 , 还要做相关的处理 :

Step 3: 反复处理 Profiles

回顾上文 Step 5 : loadForFileExtension 处理 中 , 会发现这样一个处理

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
    // ........ 省略
    String profileSpecificFile = prefix + "-" + profile + fileExtension;
    load(loader, profileSpecificFile, profile, defaultFilter, consumer);
    // 核心 , profile 再次处理
    load(loader, profileSpecificFile, profile, profileFilter, consumer);
    //.........
}

没错 , 这里就是把 profile传进去原样再扫描一遍 , 而值就是上一步设置的 value

你以为这就完了吗 ? 这就像盗梦空间里面一样 , 你以为你醒了 ,其实还在一个梦里 , 整个逻辑中涉及多个循环处理 , 而 Profile 的循环开启是在第一层

// 当第一遍大循环执行玩抽 , profiles 中就已经有了新的 profiles , 此处会开启第二个大循环
// 此处使用 Deque<Profile> profiles , 以弹出的方式获取对象 
while (!this.profiles.isEmpty()) {
    Profile profile = this.profiles.poll();
    if (isDefaultProfile(profile)) {
        addProfileToEnvironment(profile.getName());
    }
    load(profile, this::getPositiveProfileFilter,
    addToLoaded(MutablePropertySources::addLast, false));
    this.processedProfiles.add(profile);
}

补充一下循环体系 :

  • Loader # load : while 循环 profile
  • ConfigFileApplicationListener # load(1) : foreach location
  • ConfigFileApplicationListener # load(1) : foreach names (PS : 这个循环在上个循环的内部)
  • ConfigFileApplicationListener # load(2) : for 循环 propertySourceLoaders
  • ConfigFileApplicationListener # load(2) : for 循环 FileExtensions

load(1) : load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)
load(2) : load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer)

4.2 具体的加载顺序

Step 1 : 后缀文件的加载顺序

从 ConfigFileApplicationListener # load 方法种 , 我们可以看到 , 加载顺序依次是 :

  • properties
  • xml (没错 , 他会试图加载 xml 文件)
  • yml
  • yaml

总结

本来以为篇幅不会太长, 结构又写了这么多 , 所以将属性的转换放在下一篇将 ,欢迎点赞收藏

修改记录

  • V20210612 : 添加 >> 四 . 知识点
  • V20210804 : 添加结构图