Spring Boot源码阅读——配置文件加载


Spring Boot 源码阅读——配置文件加载

开头

在 SpringBoot 中,配置文件是环境的一部分

“环境”,有一个专属的类—— Environment

里面包含了各种应用程序中重要的角色:配置文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数等等

这篇文章只对其中配置文件的部分进行解读,一些关系不大的代码用注释 // .. 替代,节省篇幅

找到 Environment

SpringBoot 的启动,是从一个 run 方法开始的

@SpringBootApplication
public class SpringbootTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootTestApplication.class, args);
    }
}

从这个 run 方法,一步一步点进去,就来到了下面这个方法

为了方便阅读,我省略了与本文无关的代码


SpringApplication.java

public ConfigurableApplicationContext run(String... args) {
    // ...
    try {
        // ...
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        // ...
    }
    catch (Throwable ex) {
        // ...
    }
    // ...
    return context;
}

上面这段代码中,我只保留了一行代码

这是 run 方法中 Environment 对象的第一次出现,同时,也是环境初始化的地方:prepareEnvironment

环境初始化

创建实例

SpringApplication.java

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
                                                   DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    // 根据应用程序类型,创建对应的 Environment 实例,并进行一些基本配置
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    // 这里开始,正式对环境进行初始化
    listeners.environmentPrepared(bootstrapContext, environment);
    // ...
    return environment;
}

初始化——广播事件

这里的调用链比较长,满满往深处走,我们要找到事件广播到最后,被处理的地方

SpringApplicationRunListeners.java

void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
    doWithListeners("spring.boot.application.environment-prepared",
                    (listener) -> listener.environmentPrepared(bootstrapContext, environment));
}

EventPublishingRunListener.java

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

SimpleApplicationEventMulticaster.java

@Override
public void multicastEvent(ApplicationEvent event) {
    multicastEvent(event, resolveDefaultEventType(event));
}

SimpleApplicationEventMulticaster.java

@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

SimpleApplicationEventMulticaster.java

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
    ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
        try {
            doInvokeListener(listener, event);
        }
        catch (Throwable err) {
            errorHandler.handleError(err);
        }
    }
    else {
        doInvokeListener(listener, event);
    }
}

SimpleApplicationEventMulticaster.java

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
    try {
        // 走到这就到底了,由各个事件监听器来执行相关操作
        listener.onApplicationEvent(event);
    }
    catch (ClassCastException ex) {
        // ..
    }
}

SpringBoot 自带的事件监听器有非常非常多,我们这里触发的时间是环境准备(environment-prepared),那么理所当然的,相关操作肯定在 EnvironmentPostProcessorApplicationListener 里面

EnvironmentPostProcessorApplicationListener.java

@Override
public void onApplicationEvent(ApplicationEvent event) {
    // 可以看到这个监听器只对三个事件进行了处理,正好有我们触发的这个事件
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent();
    }
    if (event instanceof ApplicationFailedEvent) {
        onApplicationFailedEvent();
    }
}

EnvironmentPostProcessorApplicationListener.java

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    SpringApplication application = event.getSpringApplication();
    // 这个循环就是环境初始化的关键了,程序中有很多的后置处理器,包括我们的配置文件加载也在其中
    for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(),
                                                                               event.getBootstrapContext())) {
        postProcessor.postProcessEnvironment(environment, application);
    }
}

后置处理器类图

SpringBoot 提供了两个配置文件处理器,其中 ConfigFileApplicationListener 是旧版本中的处理器,在新版本已废弃,并被 ConfigDataEnvironmentPostProcessor 取代

找到了配置文件的处理器,那么接下来就可以开始阅读源码了

ConfigDataEnvironmentPostProcessor.java

void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
    try {
        this.logger.trace("Post-processing environment to add config data");
        resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
        // getConfigDataEnvironment 是对环境的一层封装,对配置文件的读取是后面的那个方法:processAndApply()
        getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
    }
    catch (UseLegacyConfigProcessingException ex) {
        // ...
    }
}

ConfigDataEnvironment.java

void processAndApply() {
    // importer 是一个很关键的对象,包含了扫描的目录、扫描到的文件
    // 当然这里只是进行了实例化,还没有任何数据
    ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
                                                         this.loaders);
    registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
    // processInitial 就是最核心的方法了,里面对所有 SpringBoot 约定的配置进行了扫描和加载
    // 下面会着重看这一部分代码
    ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
    // 创建上下文环境
    ConfigDataActivationContext activationContext = createActivationContext(
        contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
    // process 开头的方法,内部逻辑与 processInitial 相似,所以下面几行就不讲了
    // 区别就像方法名一样,processInitial 是初始化,这两个与激活相关
    contributors = processWithoutProfiles(contributors, importer, activationContext);
    activationContext = withProfiles(contributors, activationContext);
    contributors = processWithProfiles(contributors, importer, activationContext);
    // 这一步将配置文件应用进环境
    applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
                       importer.getOptionalLocations());
}

ConfigDataEnvironment.java

private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors, ConfigDataImporter importer) {
    // ..
    // 调用处理器的方法
    contributors = contributors.withProcessedImports(importer, null);
    // ..
    return contributors;
}

从 processAndApply 方法可以看出,SpringBoot 的配置文件环境初始化包含三个步骤:

  1. 扫描
  2. 加载
  3. 应用

其中,扫描和加载由配置文件处理器来完成,而应用在这个方法的最后

配置文件处理器

ConfigDataEnvironmentContributors.java

ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
                                                       ConfigDataActivationContext activationContext) {
    // 日志以及对象创建
    // ..
    while (true) {
        // 依次获取所有的 contributor
        ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
        if (contributor == null) {
            this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
            return result;
        }
        // 一些对象的创建
       	// ..
        // 从 contributor 获取扫描目录
        List<ConfigDataLocation> imports = contributor.getImports();
        this.logger.trace(LogMessage.format("Processing imports %s", imports));
        // resolveAndload 就是加载配置文件
        // resolve 是扫描,需要知道这里是一个循环,不同的 contributor 内含的扫描目录是不同的,其也有各自的含义,debug 时我们只需要看配置文件的扫描即可
        // load 是加载,不同类型的配置文件有不同的加载器,后面会看到的
        Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
		// ...
    }
}

ConfigDataImporter.java

Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext, ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext, List<ConfigDataLocation> locations) {
    try {
        Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
        // 扫描
        List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
        // 加载
        return load(loaderContext, resolved);
		}
		catch (IOException ex) {
			// ..
		}
	}

扫描

扫描——in

我们先来看扫描,扫描进行了非常多层的封装,要有耐心的一步步深入

ConfigDataImporter.java

/** 
 * 扫描代码片段1
 */
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext, Profiles profiles, List<ConfigDataLocation> locations) {
    List<ConfigDataResolutionResult> resolved = new ArrayList<>(locations.size());
    for (ConfigDataLocation location : locations) {
        // 将所有扫描结果加入集合,从这里的 resolve 方法去看扫描代码片段2
        resolved.addAll(resolve(locationResolverContext, profiles, location));
    }
    return Collections.unmodifiableList(resolved);
}

ConfigDataImporter.java

/** 
 * 扫描代码片段2
 */
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext, Profiles profiles, ConfigDataLocation location) {
    try {
        // 进入扫描代码片段3
        return this.resolvers.resolve(locationResolverContext, location, profiles);
    }
    catch (ConfigDataNotFoundException ex) {
        // ..
    }
}

ConfigDataImporter.java

/** 
 * 扫描代码片段3
 */
List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
    if (location == null) {
        return Collections.emptyList();
    }
    for (ConfigDataLocationResolver<?> resolver : getResolvers()) {
        if (resolver.isResolvable(context, location)) {
            // 进入扫描代码片段4
            return resolve(resolver, context, location, profiles);
        }
    }
    throw new UnsupportedConfigDataLocationException(location);
}

ConfigDataImporter.java

/** 
 * 扫描代码片段4
 */
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver, ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
    // 这里的 debug 会复杂一点,因为传入了一个 Supplier 函数
    // 如下面的扫描代码片段5,内部执行了 Supplier 函数的 get 方法,也就是这里的 lambda 表达式中的 resolve 方法
    List<ConfigDataResolutionResult> resolved = resolve(location, false, () -> resolver.resolve(context, location));
    if (profiles == null) {
        return resolved;
    }
    List<ConfigDataResolutionResult> profileSpecific = resolve(location, true, () -> resolver.resolveProfileSpecific(context, location, profiles));
    return merge(resolved, profileSpecific);
}

/** 
 * 扫描代码片段5
 */
private List<ConfigDataResolutionResult> resolve(ConfigDataLocation location, boolean profileSpecific, Supplier<List<? extends ConfigDataResource>> resolveAction) {
    // 前面4个代码片段,不管是中间变量还是 return 的结果,都是 List<ConfigDataResolutionResult>
    // 而这里出现了 List<ConfigDataResource>,显而易见,这一句代码是真正执行扫描的入口
    // 上面的几段代码只是对结果集的封装与处理
    // 代码片段4中提到,这个 get 就是 resolve,所以我们进入扫描代码片段6
    List<ConfigDataResource> resources = nonNullList(resolveAction.get());
    List<ConfigDataResolutionResult> resolved = new ArrayList<>(resources.size());
    for (ConfigDataResource resource : resources) {
        resolved.add(new ConfigDataResolutionResult(location, resource, profileSpecific));
    }
    return resolved;
}

StandardConfigDataLocationResolver.java

/** 
 * 扫描代码片段6
 */
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataNotFoundException {
    // 上面几段代码的参数无非是 localtion、profile 等基本信息
    // 而这里的 getReferences 获取的是一个依赖集
    // 在进入下一个 resolve 方法前,我们先看看这个依赖集是什么作用
    // 这里传入了两个参数,context 就不用多说了,location.split() 需要提一句
    // SpringBoot 内置的扫描目录是以分号隔开的字符串,这里的 split 方法就是以分号来分割字符串,变成一个目录地址的数组
    // 而依赖集就是根据 SpringBoot 内置的规则获取的依赖地址
    return resolve(getReferences(context, location.split()));
}

依赖

StandardConfigDataLocationResolver.java

/** 
 * 依赖代码片段1
 */
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context, ConfigDataLocation[] configDataLocations) {
   Set<StandardConfigDataReference> references = new LinkedHashSet<>();
   for (ConfigDataLocation configDataLocation : configDataLocations) {
      // 是不是很熟悉,像极了 resolve 方法的层层嵌套,那么继续往里面看
      references.addAll(getReferences(context, configDataLocation));
   }
   return references;
}

StandardConfigDataLocationResolver.java

/** 
 * 依赖代码片段2
 */
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context, ConfigDataLocation configDataLocation) {
    String resourceLocation = getResourceLocation(context, configDataLocation);
    try {
        // 我们只看配置文件的扫描,配置文件扫描的是目录,所以这里只解析这段 if 里的代码
        if (isDirectory(resourceLocation)) {
            return getReferencesForDirectory(configDataLocation, resourceLocation, NO_PROFILE);
        }
        // ..
    }
    catch (RuntimeException ex) {
        // ..
    }
}

StandardConfigDataLocationResolver.java

/** 
 * 依赖代码片段3
 */
private Set<StandardConfigDataReference> getReferencesForDirectory(ConfigDataLocation configDataLocation, String directory, String profile) {
    Set<StandardConfigDataReference> references = new LinkedHashSet<>();
    // 配置文件的 name 为 application,这是 SpringBoot 约定好的,不是我们能更改的
    for (String name : this.configNames) {
        // 进入下一个方法 依赖代码片段4
        Deque<StandardConfigDataReference> referencesForName = getReferencesForConfigName(name, configDataLocation, directory, profile);
        references.addAll(referencesForName);
    }
    return references;
}

StandardConfigDataLocationResolver.java

/** 
 * 依赖代码片段4
 */
private Deque<StandardConfigDataReference> getReferencesForConfigName(String name, ConfigDataLocation configDataLocation, String directory, String profile) {
    Deque<StandardConfigDataReference> references = new ArrayDeque<>();
    // PropertySourceLoader 在 SpringBoot启动时已经初始化完毕
    // 在 spring.factories 文件中可以看到一共有两个 loader
    // PropertiesPropertySourceLoader 和 YamlPropertySourceLoader
    // 分别是 (properties 与 xml)加载器、(yaml 与 yml)加载器
    for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
        // 不同加载器有对应的文件扩展名
        for (String extension : propertySourceLoader.getFileExtensions()) {
            // 根据目录 + 文件名 + 激活名 + 扩展名创建一个资源依赖,并传入了对应的资源加载器
            StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory,
                                                                                    directory + name, profile, extension, propertySourceLoader);
            if (!references.contains(reference)) {
                references.addFirst(reference);
            }
        }
    }
    return references;
}

扫描——out

获取到依赖后,代码又回到了扫描的最后:扫描代码片段6

StandardConfigDataLocationResolver.java

/** 
 * 扫描代码片段6
 */
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataNotFoundException {
    // 这个时候 getReferences 已经返回了一个依赖集
    // 我们直接去看 resolve 方法怎么处理依赖
    return resolve(getReferences(context, location.split()));
}

StandardConfigDataLocationResolver.java

/** 
 * 扫描代码片段7
 * 从这个 resolve 方法开始,限定符已经变成了 private
 * 开始涉及到真正的扫描逻辑了
 */
private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
    List<StandardConfigDataResource> resolved = new ArrayList<>();
    for (StandardConfigDataReference reference : references) {
        // 继续进入下一个 resolve 方法
        resolved.addAll(resolve(reference));
    }
    if (resolved.isEmpty()) {
        resolved.addAll(resolveEmptyDirectories(references));
    }
    return resolved;
}

StandardConfigDataLocationResolver.java

/** 
 * 扫描代码片段8
 */
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
    // 这个 isPattern 是判断依赖的路径字符串是否是模式串
    // 比如 classpath:*
    // 这类带 * 号的代表目录下的所有文件,那么就需要特殊的处理
    // 我们加载配置文件的部分是确定文件地址的,所以走这个 if
    if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
        return resolveNonPattern(reference);
    }
    return resolvePattern(reference);
}

StandardConfigDataLocationResolver.java

/** 
 * 扫描代码片段9
 */
private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
    // 根据依赖地址加载资源
    // 这里的加载和后面的 load 不是一个概念
    // 这里只是将资源加载到内存,还没有开始解析
    // 而 load 的加载是加载到 SpringBoot
    Resource resource = this.resourceLoader.getResource(reference.getResourceLocation());
    // 如果资源不存在并且是可跳过的,那么返回空集合
    if (!resource.exists() && reference.isSkippable()) {
        logSkippingResource(reference);
        return Collections.emptyList();
    }
    // 资源存在的话就返回
    return Collections.singletonList(createConfigResourceLocation(reference, resource));
}

那么到这里,资源的扫描获取就结束了,在 resolve 的层层调用中,资源又会被封装为 List

在进入下一节“加载”之前,我们不妨 debug 调试看看,SpringBoot 对资源文件会扫描哪些目录

扩展知识

扫描的目录

SpringBoot 需要加载的文件很多,不过配置文件的扫描都是以 application 开头的

我们可以在 getReferences 方法内打上断点,并加上判断条件:

"application".equals(this.configNames[0])

debug

debug 后,这里会有 5 次停顿,查看 configDataLocation 的 value 就能知道扫描的目录有哪些,分别是:

value含义
file:./项目编译打包后 所在根目录
file:./config/项目编译打包后 所在根目录下的 config 目录
file:./config/*/项目编译打包后 所在根目录下的 config 目录中的任意子目录
classpath:/项目编译打包后的 classes 目录(对应开发环境的 resources 目录)
classpath:/config/项目编译打包后的 classes 目录下的 config 目录

扫描的文件

目录找到了,接下来就要找扫描的文件有哪些了

上面也提到过,SpringBoot 默认提供两种加载器

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

分别能加载:

  1. properties 和 xml
  2. yaml 和 yml

那么即使不进行 debug,也能猜到扫描的文件只有 4 个

但是,SpringBoot 并不支持 xml 类型的配置文件,之所以会扫描,是因为在加载器中 xml 是和 properties 并列

所以实际能使用的配置文件格式只有 3种

当然该 debug 还是得 debug

这次我们在 getReferencesForConfigName 方法内打上断点,条件是:

"application".equals(name)

debug

根据 debug 可以得出上面的结论,确实只扫描了这 4 个文件

而且顺序是:

  1. properties
  2. xml
  3. yml
  4. yaml

但是由于加入双端队列(Deque)的方式是 addFirst,所以在队列中的顺序刚好是相反的

看到这就引出下一个问题:如果 4 个配置文件共存,生效的是哪个?又或者是都生效,但是覆盖的顺序是什么?

这个问题就要看“加载”部分的源码才能知道了

加载

入口在 配置文件处理器 这一小节的最后一段代码

在看下面这段加载代码前,我们先通过 debug 查看参数 candidates

我在 resources 中创建了 3 种类型的配置文件,都会被扫描进来,所以这里的 candidates 有 3 个 element

顺序正如 上面 讲的:

  1. yaml
  2. yml
  3. properties

ConfigDataImporter.java

private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext, List<ConfigDataResolutionResult> candidates) throws IOException {
    Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();
    // 这里的循环是倒序的,忽略覆不覆盖的问题,加载的顺序又与 candidates 内元素的顺序相反
    // 一来一去,加载的顺序就和扫描的顺序一致了
    for (int i = candidates.size() - 1; i >= 0; i--) {
        ConfigDataResolutionResult candidate = candidates.get(i);
        ConfigDataLocation location = candidate.getLocation();
        // 前面也提到过,资源已经被加载到内存,这里直接拿
        ConfigDataResource resource = candidate.getResource();
        if (resource.isOptional()) {
            this.optionalLocations.add(location);
        }
        // 如果资源已经被加载了,那就添加路径
        // 因为是 set,所以不会重复
        if (this.loaded.contains(resource)) {
            this.loadedLocations.add(location);
        }
        else {
            try {
                // 调用对应的加载器进行配置文件的加载,说是加载,其实用解析这个词更合适一点
                // 进过两三次调用后,最终进入的就是两种类型的资源加载器的 load 方法
                // 下面就不贴代码了
                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);
}

上面这段代码,最容易产生疑惑的地方就是资源加载的 if 判断

为什么会重复加载资源呢,这就得回到最外层的代码。不过先别急,我们先把加载看完

上面已经将配置文件 load 解析完成了,但是还没绑定到我们的 SpringBoot,也就是说 SringBoot 还不能访问到配置信息

绑定并不是 load 的功能,load 是加载与解析

所以我们回到外层代码 配置文件处理器 的部分

ConfigDataEnvironmentContributors.java

ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer, ConfigDataActivationContext activationContext) {
    // ..
    while (true) {
        // 这里是对 contributor 的遍历,可以想象成一个链表或者队列,contributor 包含环境初始化中的各个生命周期(这个词可能不太合适,但是我想不到更好的词了)
        // file目录和classpath是不同的 contributor,这里以 classpath 为例
        // 第一阶段是 INITAL_IMPORT,字面意思就是初始化导入
        // 这个阶段的 contributor 中就已经包含配置文件了要扫描的路径,所以会进行一次 resolveAndLoad
        // 扫描和加载结束后,回反馈给 contributor,具体表现为其内部的 children
        // children 包含两种值:EMPTY_LOCATION 和 BOUND_IMPORT
        // EMPTY_LOCATION 是上一阶段的扫描加载中为空的目录
        // BOUND_IMPORT 是已进行绑定的配置文件
        // 下面是我的 debug 数据,只取了成功加载的那一部分
        // BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.properties] []
        // BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yml] []
        // BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yaml] []
        // 
        // 第二阶段为 UNBOUND_IMPORT,含义为未绑定的资源
        // 这一阶段的目的是绑定上一阶段获取到的资源
        // 绑定成功后,同样会反馈给 contributor,不过这次写回的就是 null 了
        ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
        // ..
        // 第二阶段:绑定已导入的资源
        // 绑定:其实就是检测下资源存不存在然后创建一个binder
        // binder 会尝试与一些东西进行绑定
        // 不过对配置文件的加载来说,没有多大影响
        if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
            ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(result, activationContext);
            // 将绑定的结果回写入 contributor,并放回 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);
        List<ConfigDataLocation> imports = contributor.getImports();
        this.logger.trace(LogMessage.format("Processing imports %s", imports));
        Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext, locationResolverContext, loaderContext, imports);
        ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase, asContributors(imported));
        // 将本次扫描加载的结果回写入 contributor,并放回 contributor 链
        result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext, result.getRoot().withReplacement(contributor, contributorAndChildren));
        // ..
    }
}

下面是完整代码配上执行步骤的图

经过上面这些花里胡哨的步骤后,result 终于完成了:

{BEFORE_PROFILE_ACTIVATION=[EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, EXISTING null null []
, INITIAL_IMPORT null null []
    EMPTY_LOCATION optional:file:./;optional:file:./config/;optional:file:./config/*/ null [IGNORE_IMPORTS]
, INITIAL_IMPORT null null []
    BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.properties] []
    BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yml] []
    BOUND_IMPORT optional:classpath:/;optional:classpath:/config/ class path resource [config/application.yaml] []
]}

这些数据看着很抽象,其实每一行都是一个 contributor 对象

下面几个 BOUND_IMPORT 的就包含了 resource,有兴趣可以去 debug 看看最终结果,我找了 application.properties 的截了个图如下

image-20221124140640239

应用

配置文件处理器进行完扫描与加载后,将 result 返回到了 processAndApply 方法,看到 processAndApply,就能理解为什么上面会出现资源重复加载的问题了

  • processInitial
  • processWithoutProfiles
  • processWithProfiles

这几个方法在内部调用的都是 withProcessedImports,只是参数不同,他们的差别就是配置文件的激活筛选

ConfigDataEnvironment.java

void processAndApply() {
    // importer 是一个很关键的对象,包含了扫描的目录、扫描到的文件
    // 当然这里只是进行了实例化,还没有任何数据
    ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
                                                         this.loaders);
    registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
    // processInitial 就是最核心的方法了,里面对所有 SpringBoot 约定的配置进行了扫描和加载
    ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
    // 创建上下文环境
    ConfigDataActivationContext activationContext = createActivationContext(
        contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
    // process 开头的方法,内部逻辑与 processInitial 相似,所以下面几行就不讲了
    // 区别就像方法名一样,processInitial 是初始化,这两个与激活相关
    contributors = processWithoutProfiles(contributors, importer, activationContext);
    activationContext = withProfiles(contributors, activationContext);
    contributors = processWithProfiles(contributors, importer, activationContext);
    // 这一步将配置文件应用进环境
    applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
                       importer.getOptionalLocations());
}

ConfigDataEnvironment.java

private void applyToEnvironment(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, Set<ConfigDataLocation> loadedLocations, Set<ConfigDataLocation> optionalLocations) {
    // 进行一些合法性检验
    checkForInvalidProperties(contributors);
    checkMandatoryLocations(contributors, activationContext, loadedLocations, optionalLocations);
    // 获取环境的资源集合
    MutablePropertySources propertySources = this.environment.getPropertySources();
    // 应用
    // 这里传入了环境的资源集合 propertySources
    applyContributor(contributors, activationContext, propertySources);
    // ..
}

ConfigDataEnvironment.java

private void applyContributor(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, MutablePropertySources propertySources) {
    // 遍历加载过程返回的 result,也就是一堆 contributor
    for (ConfigDataEnvironmentContributor contributor : contributors) {
        PropertySource<?> propertySource = contributor.getPropertySource();
        // 找到 BOUND_IMPORT 型的 contributor
        if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT && propertySource != null) {
            if (!contributor.isActive(activationContext)) {
                // ..
            }
            else {
                // ..
                // 加入环境的资源集合,并且是放在最后
                // 还记得我们加载配置文件的顺序吗,和扫描顺序一致
                // properties > xml > yml > yaml
                // 所以环境的资源集合中,几个配置文件的相对顺序也是如此
                propertySources.addLast(propertySource);
                // ..
            }
        }
    }
}

到这里,配置文件的应用就结束了,已经被写入环境 Environment

在启动类写个测试

public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(SpringbootTestApplication.class, args);
    ConfigurableEnvironment environment = context.getEnvironment();
    System.out.println(environment.getProperty("server.port"));
}

ConfigurationPropertySourcesPropertyResolver.java

@Override
@Nullable
public String getProperty(String key) {
    return this.propertyResolver.getProperty(key);
}

ConfigurationPropertySourcesPropertyResolver.java

@Override
public String getProperty(String key) {
    return getProperty(key, String.class, true);
}

ConfigurationPropertySourcesPropertyResolver.java

private <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
    Object value = findPropertyValue(key);
    if (value == null) {
        return null;
    }
    if (resolveNestedPlaceholders && value instanceof String) {
        value = resolveNestedPlaceholders((String) value);
    }
    return convertValueIfNecessary(value, targetValueType);
}

ConfigurationPropertySourcesPropertyResolver.java

private Object findPropertyValue(String key) {
    ConfigurationPropertySourcesPropertySource attached = getAttached();
    if (attached != null) {
        ConfigurationPropertyName name = ConfigurationPropertyName.of(key, true);
        if (name != null) {
            try {
                ConfigurationProperty configurationProperty = attached.findConfigurationProperty(name);
                return (configurationProperty != null) ? configurationProperty.getValue() : null;
            }
            catch (Exception ex) {
            }
        }
    }
    return this.defaultResolver.getProperty(key, Object.class, false);
}

ConfigurationPropertySourcesPropertySource.java

ConfigurationProperty findConfigurationProperty(ConfigurationPropertyName name) {
    if (name == null) {
        return null;
    }
    // 这个类中的 source 就是 Environment 中的 propertySources
    // 从前往后遍历
    for (ConfigurationPropertySource configurationPropertySource : getSource()) {
        ConfigurationProperty configurationProperty = configurationPropertySource.getConfigurationProperty(name);
        // 直到某个 source 有这个属性,那就返回
        // 而我们的 3 个配置文件在 source 中的顺序为
        // properties > yml > yaml
        // 所以配置文件都是能被读取进去的,也都能被应用,但是他们的属性有优先级
        if (configurationProperty != null) {
            return configurationProperty;
        }
    }
    return null;
}

同目录下的配置文件优先级问题解决了,那不同目录呢

扫描的目录 中按扫描顺序,列举了 5 个目录

每一个目录被扫描后,会在 resolve 方法被 addAll 进集合

也就是先扫描的在前,后扫描的在后

而在加载中,for 循环是逆序的,所以目录的加载顺序刚好与扫描顺序相反

总结

  1. 扫描目录的顺序
    • file:./
    • file:./config/
    • file:./config/*/
    • classpath:/
    • classpath:/config/
  2. 扫描文件的顺序
    • properties
    • xml
    • yml
    • yaml
  3. 加载文件的顺序
    • properties
    • xml
    • yml
    • yaml
  4. 加载目录的顺序
    • classpath:/config/
    • classpath:/
    • file:./config/*/
    • file:./config/
    • file:./
  5. 越先被加载的配置文件,在环境中被应用的优先级越高

文章作者: ❤纱雾
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ❤纱雾 !
评论
  目录