Spring Boot 源码分析 - 配置加载

参考 知识星球芋道源码 星球的源码解析,一个活跃度非常高的 Java 技术社群,感兴趣的小伙伴可以加入 芋道源码 星球,一起学习😄

该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读

Spring Boot 版本:2.2.x

最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章导读》 系列文章

如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~

该系列其他文章请查看:《精尽 Spring Boot 源码分析 - 文章导读》

概述

在我们的 Spring Boot 应用中,可以很方便的在 application.ymlapplication.properties 文件中添加需要的配置信息,并应用于当前应用。那么,对于 Spring Boot 是如何加载配置文件,如何按需使指定环境的配置生效的呢?接下来,我们一起来看看 Spring Boot 是如何加载配置文件的。

提示:Spring Boot 加载配置文件的过程有点绕,本篇文章有点长,可选择性的跳过 Loader 这一小节

回顾

回到前面的 《SpringApplication 启动类的启动过程》 这篇文章,Spring Boot 启动应用的入口和主流程都是在 SpringApplication#run(String.. args) 方法中。

在这篇文章的 6. prepareEnvironment 方法 小节中可以讲到,会对所有的 SpringApplicationRunListener 广播 应用环境已准备好 的事件,如下:

// SpringApplicationRunListeners.java
void environmentPrepared(ConfigurableEnvironment environment) {
    // 只有一个 EventPublishingRunListener 对象
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.environmentPrepared(environment);
    }
}

只有一个 EventPublishingRunListener 事件发布器,里面有一个事件广播器,封装了几个 ApplicationListener 事件监听器,如下:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

其中有一个 ConfigFileApplicationListener 对象,监听到上面这个事件,会去解析 application.yml 等应用配置文件的配置信息

在 Spring Cloud 还会配置一个 BootstrapApplicationListener 对象,监听到上面的这个事件会创建一个 ApplicationContext 作为当前 Spring 应用上下文的父容器,同时会读取 bootstrap.yml 文件的信息

ConfigFileApplicationListener

org.springframework.boot.context.config.ConfigFileApplicationListener,Spring Boot 的事件监听器,主要用于加载配置文件到 Spring 应用中

相关属性

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {

	/** 默认值的 PropertySource 在 Environment 中的 key */
	private static final String DEFAULT_PROPERTIES = "defaultProperties";

	// Note the order is from least to most specific (last one wins)
	/** 支持的配置文件的路径 */
	private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

	/** 配置文件名称(不包含后缀) */
	private static final String DEFAULT_NAMES = "application";

	private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);

	private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);

	private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);

	/** 需要过滤的配置项 */
	private static final Set<String> LOAD_FILTERED_PROPERTY;

	static {
		Set<String> filteredProperties = new HashSet<>();
		filteredProperties.add("spring.profiles.active");
		filteredProperties.add("spring.profiles.include");
		LOAD_FILTERED_PROPERTY = Collections.unmodifiableSet(filteredProperties);
	}

	/**
	 * The "active profiles" property name.
	 * 可通过该属性指定配置需要激活的环境配置
	 */
	public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";

	/**
	 * The "includes profiles" property name.
	 */
	public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";

	/**
	 * The "config name" property name.
	 * 可通过该属性指定配置文件的名称
	 */
	public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

	/**
	 * The "config location" property name.
	 */
	public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";

	/**
	 * The "config additional location" property name.
	 */
	public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";

	/**
	 * The default order for the processor.
	 */
	public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;

	private final DeferredLog logger = new DeferredLog();

	private String searchLocations;

	private String names;

	private int order = DEFAULT_ORDER;
    
    @Override
	public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
		return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
				|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
	}
}

属性不多,几个关键的属性都有注释,同时支持处理的事件有 ApplicationEnvironmentPreparedEvent 和 ApplicationPreparedEvent

我们看到它实现了 EnvironmentPostProcessor 这个接口,用于对 Environment 进行后置处理,在刷新 Spring 应用上下文之前

1. onApplicationEvent 方法

onApplicationEvent(ApplicationEvent) 方法,ApplicationListener 处理事件的方法,如下:

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

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    // <1> 通过类加载器从 `META-INF/spring.factories` 文件中获取 EnvironmentPostProcessor 类型的类名称,并进行实例化
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    // <2> 当前对象也是 EnvironmentPostProcessor 实现类,添加进去
    postProcessors.add(this);
    // <3> 将这些 EnvironmentPostProcessor 进行排序
    AnnotationAwareOrderComparator.sort(postProcessors);
    // <4> 遍历这些 EnvironmentPostProcessor 依次对 Environment 进行处理
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        // <4.1> 依次对当前 Environment 进行处理,上面第 `2` 步添加了当前对象,我们直接看到当前类的这个方法
        postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
    }
}

我们在前面 回顾 中讲到,会广播一个 应用环境已准备好 的事件,也就是 ApplicationEnvironmentPreparedEvent 事件

处理该事件的过程如下:

  1. 通过类加载器从 META-INF/spring.factories 文件中获取 EnvironmentPostProcessor 类型的类名称,并进行实例化

    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
    }
    
  2. 当前对象也是 EnvironmentPostProcessor 实现类,添加进去

  3. 将这些 EnvironmentPostProcessor 进行排序

  4. 遍历这些 EnvironmentPostProcessor 依次对 Environment 进行处理

    1. 依次对当前 Environment 进行处理,上面第 2 步添加了当前对象,我们直接看到当前类的这个方法

2. postProcessEnvironment 方法

postProcessEnvironment(ConfigurableEnvironment, SpringApplication) 方法,实现 EnvironmentPostProcessor 接口的方法,对 Environment 进行后置处理

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    // 为 Spring 应用的 Environment 环境对象添加属性(包括 `application.yml`)
    addPropertySources(environment, application.getResourceLoader());
}

直接调用 addPropertySources(..) 方法,为当前 Spring 应用的 Environment 环境对象添加属性(包括 application.yml 配置文件的解析)

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
   // <1> 往 Spring 应用的 Environment 环境对象添加随机值的 RandomValuePropertySource 属性源
   // 这样就可直接通过 `@Value(random.uuid)` 随机获取一个 UUID
   RandomValuePropertySource.addToEnvironment(environment);
   // <2> 创建一个 Loader 对象,设置占位符处理器,资源加载器,PropertySourceLoader 配置文件加载器
   // <3> 加载配置信息,并放入 Environment 环境对象中
   // 整个处理过程有点绕,嵌套有点深,你可以理解为会将你的 Spring Boot 或者 Spring Cloud 的配置文件加载到 Environment 中,并激活对应的环境
   new Loader(environment, resourceLoader).load();
}

过程如下:

  1. 往 Spring 应用的 Environment 环境对象添加随机值的 RandomValuePropertySource 属性源,这样就可直接通过 @Value(random.uuid) 随机获取一个 UUID

    // RandomValuePropertySource.java
    public static void addToEnvironment(ConfigurableEnvironment environment) {
        environment.getPropertySources().addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME));
        logger.trace("RandomValuePropertySource add to Environment");
    }
    

    逻辑很简单,感兴趣的可以去看看

  2. 创建一个 Loader 对象,设置占位符处理器,资源加载器,PropertySourceLoader 配置文件加载器

  3. 调用这个 Loader 的 load() 方法,加载配置信息,并放入 Environment 环境对象中

加载配置信息的过程有点绕嵌套有点深,你可以先理解为,将你的 Spring Boot 或者 Spring Cloud 的配置文件加载到 Environment 中,并激活对应的环境

Loader

org.springframework.boot.context.config.ConfigFileApplicationListener.Loader,私有内部类,配置文件的加载器

构造方法

private class Loader {
    
    /** 环境对象 */
    private final ConfigurableEnvironment environment;
    
    /** 占位符处理器 */
    private final PropertySourcesPlaceholdersResolver placeholdersResolver;
    
    /** 资源加载器 */
    private final ResourceLoader resourceLoader;
    
    /** 属性的资源加载器 */
    private final List<PropertySourceLoader> propertySourceLoaders;
    
    /** 待加载的 Profile 队列 */
    private Deque<Profile> profiles;
    
    /** 已加载的 Profile 队列 */
    private List<Profile> processedProfiles;
    
    /** 是否有激活的 Profile */
    private boolean activatedProfiles;
    
    /** 保存每个 Profile 对应的属性信息 */
    private Map<Profile, MutablePropertySources> loaded;

    private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();

    Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        this.environment = environment;
        // 占位符处理器
        this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
        // 设置默认的资源加载器
        this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
        /**
         * 通过 ClassLoader 从所有的 `META-INF/spring.factories` 文件中加载出 PropertySourceLoader
         * Spring Boot 配置了两个属性资源加载器:
         * {@link PropertiesPropertySourceLoader} 加载 `properties` 和 `xml` 文件
         * {@link YamlPropertySourceLoader} 加载 `yml` 和 `yaml` 文件
         */
        this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
                getClass().getClassLoader());
    }
}

属性不多,上面都已经注释了,在构造器中会通过 ClassLoader 从所有的 META-INF/spring.factories 文件中加载出 PropertySourceLoader,如下:

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

PropertiesPropertySourceLoader:加载 propertiesxml 文件

YamlPropertySourceLoader:加载 ymlyaml 文件

3. load 方法

load() 方法,加载配置信息,并放入 Environment 环境对象中,如下:

void load() {
    // 借助 FilteredPropertySource 执行入参中的这个 Consumer 函数
    // 目的就是获取 `defaultProperties` 默认值的 PropertySource,通常我们没有设置,所以为空对象
    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<>();
            // <1> 初始化 Profile 对象,也就是我们需要加载的 Spring 配置,例如配置的 JVM 变量:`dev`、`sit`、`uat`、`prod`
            // 1. `java -jar xxx.jar --spring.profiles.active=dev` or `java -jar -Dspring.profiles.active=dev xxx.jar`,那么这里的 `profiles` 就会有一个 `null` 和一个 `dev`
            // 2. `java -jar xxx.jar`,那么这里的 `profiles` 就会有一个 `null` 和一个 `default`
            initializeProfiles();
            // <2> 依次加载 `profiles` 对应的配置信息
            // 这里先解析 `null` 对应的配置信息,也就是公共配置
            // 针对上面第 `2` 种情况,如果公共配置指定了 `spring.profiles.active`,那么添加至 `profiles` 中,并移除 `default` 默认 Profile
            // 所以后续和上面第 `1` 种情况一样的处理
            while (!this.profiles.isEmpty()) {
                // <2.1> 将接下来的准备加载的 Profile 从队列中移除
                Profile profile = this.profiles.poll();
                // <2.2> 如果不为 `null` 且不是默认的 Profile,这个方法名不试试取错了??
                if (isDefaultProfile(profile)) {
                    // 则将其添加至 Environment 的 `activeProfiles`(有效的配置)中,已存在不会添加
                    addProfileToEnvironment(profile.getName());
                }
                /**
                 * <2.3> 尝试加载配置文件,并解析出配置信息,会根据 Profile 归类,最终保存至 {@link this#loaded} 集合
                 * 例如会去加载 `classpath:/application.yml` 或者 `classpath:/application-dev.yml` 文件,并解析
                 * 如果 `profile` 为 `null`,则会解析出 `classpath:/application.yml` 中的公共配置
                 * 因为这里是第一次去加载,所以不需要检查 `profile` 对应的配置信息是否存在
                 */
                load(profile, this::getPositiveProfileFilter,
                        addToLoaded(MutablePropertySources::addLast, false));
                // <2.4> 将已加载的 Profile 保存
                this.processedProfiles.add(profile);
            }
            /**
             * <3> 如果没有指定 `profile`,那么这里尝试解析所有需要的环境的配置信息,也会根据 Profile 归类,最终保存至 {@link this#loaded} 集合
             * 例如会去加载 `classpath:/application.yml` 文件并解析出各个 Profile 的配置信息
             * 因为上面可能尝试加载过,所以这里需要检查 `profile` 对应的配置信息是否存在,已存在则不再添加
             * 至于这一步的用途暂时还没搞懂~
             */
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            /** <4> 将上面加载出来的所有配置信息从 {@link this#loaded} 集合添加至 Environment 中 */
            addLoadedPropertySources();
            // <5> 设置被激活的 Profile 环境
            applyActiveProfiles(defaultProperties);
        });
}

方法内部借助 FilteredPropertySource 执行入参中的这个 Consumer 函数,目的就是获取 defaultProperties 默认值的 PropertySource,通常我们没有设置,所以为空对象,如下:

static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
        Consumer<PropertySource<?>> operation) {
    MutablePropertySources propertySources = environment.getPropertySources();
    // 先获取当前环境中 `defaultProperties` 的 PropertySource 对象,默认没有,通常我们也不会配置
    PropertySource<?> original = propertySources.get(propertySourceName);
    if (original == null) {
        // 直接调用 `operation` 函数
        operation.accept(null);
        return;
    }
    // 将这个当前环境中 `defaultProperties` 的 PropertySource 对象进行替换
    // 也就是封装成一个 FilteredPropertySource 对象,设置了几个需要过滤的属性
    propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
    try {
        // 调用 `operation` 函数,入参是默认值的 PropertySource
        operation.accept(original);
    }
    finally {
        // 将当前环境中 `defaultProperties` 的 PropertySource 对象还原
        propertySources.replace(propertySourceName, original);
    }
}

所以我们直接来看到 load() 方法中的 Consumer 函数,整个处理过程如下:

  1. 调用 initializeProfiles() 方法,初始化 Profile 对象,也就是我们需要加载的 Spring 配置,例如配置的 JVM 变量:devsituatprod

    1. java -jar xxx.jar --spring.profiles.active=dev or java -jar -Dspring.profiles.active=dev xxx.jar,那么这里的 profiles 就会有一个 null 和一个 dev

    2. java -jar xxx.jar,那么这里的 profiles 就会有一个 null 和一个 default

  2. 依次加载上一步得到的 profiles 对应的配置信息,这里先解析 null 对应的配置信息,也就是公共配置

    针对上面第 1.2 种情况,如果公共配置指定了 spring.profiles.active,那么添加至 profiles 中,并移除 default 默认 Profile,所以后续和上面第 1.1 种情况一样的处理,后面会讲到

    1. 将接下来的准备加载的 Profile 从队列中移除

    2. 如果不为 null 且不是默认的 Profile,这个方法名不试试取错了??则将其添加至 Environment 的 activeProfiles(有效的配置)中,已存在不会添加

      也就是保存激活的 Profile 环境

    3. 调用 load(..) 重载方法,尝试加载配置文件,并解析出配置信息,会根据 Profile 归类,最终保存至 this#loaded 集合

      例如会去加载 classpath:/application.yml 或者 classpath:/application-dev.yml 文件,并解析;如果 profilenull,则会解析出 classpath:/application.yml 中的公共配置,因为这里是第一次去加载,所以不需要检查 profile 对应的配置信息是否存在

    4. 将已加载的 Profile 保存

  3. 继续调用 load(..) 重载方法,如果没有指定 profile,那么这里尝试解析所有需要的环境的配置信息,也会根据 Profile 归类,最终保存至 this#loaded 集合

    例如会去加载 classpath:/application.yml 文件并解析出各个 Profile 的配置信息;因为上面可能尝试加载过,所以这里需要检查 profile 对应的配置信息是否存在,已存在则不再添加,至于这一步的用途暂时还没搞懂~

  4. 调用 addLoadedPropertySources() 方法,将上面加载出来的所有配置信息从 this#loaded 集合添加至 Environment 中

上面的的 load(..) 重载方法中有一个 Consumer 函数,它的入参又有一个 Consumer 函数,第 2.33 步的入参不同,注意一下⏩

上面的整个过程有点绕,有点难懂,建议各位小伙伴自己调试代码⏩

3.1 initializeProfiles 方法

initializeProfiles() 方法,初始化 Profile 对象,也就是我们需要加载的 Spring 配置,如下:

private void initializeProfiles() {
    // The default profile for these purposes is represented as null. We add it
    // first so that it is processed first and has lowest priority.
    // <1> 先添加一个空的 Profile
    this.profiles.add(null);
    // <2> 从 Environment 中获取 `spring.profiles.active` 配置
    // 此时还没有加载配置文件,所以这里获取到的就是你启动 `jar` 包时设置的 JVM 变量,例如 `-Dspring.profiles.active`
    // 或者启动 `jar` 包时添加的启动参数,例如 `--spring.profiles.active=dev`
    Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
    // <3> 从 Environment 中获取 `spring.profiles.include` 配置
    Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
    // <4> 从 Environment 配置的需要激活的 Profile 们,不在上面两个范围内则属于其他
    List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
    // <5> 将上面找到的所有 Profile 都添加至 `profiles` 中(通常我们只在上面的第 `2` 步可能有返回结果)
    this.profiles.addAll(otherActiveProfiles);
    // Any pre-existing active profiles set via property sources (e.g.
    // System properties) take precedence over those added in config files.
    this.profiles.addAll(includedViaProperty);
    // 这里主要设置 `activatedProfiles`,表示已有需要激活的 Profile 环境
    addActiveProfiles(activatedViaProperty);
    // <6> 如果只有一个 Profile,也就是第 `1` 步添加的一个空对象,那么这里再创建一个默认的
    if (this.profiles.size() == 1) { // only has null profile
        for (String defaultProfileName : this.environment.getDefaultProfiles()) {
            Profile defaultProfile = new Profile(defaultProfileName, true);
            this.profiles.add(defaultProfile);
        }
    }
}

过程如下:

  1. 先往 profile 集合添加一个空的 Profile

  2. 从 Environment 中获取 spring.profiles.active 配置,此时还没有加载配置文件,所以这里获取到的就是你启动 jar 包时设置的 JVM 变量,例如 -Dspring.profiles.active,或者启动 jar 包时添加的启动参数,例如 --spring.profiles.active=dev

    在前面的 《SpringApplication 启动类的启动过程》 这篇文章的 6. prepareEnvironment 方法 小节的第 2 步讲过

  3. 从 Environment 中获取 spring.profiles.include 配置

  4. 从 Environment 配置的需要激活的 Profile 们,不在上面两个范围内则属于其他

  5. 将上面找到的所有 Profile 都添加至 profiles 中(通常我们只在上面的第 2 步可能有返回结果)

  6. 如果只有一个 Profile,也就是第 1 步添加的一个空对象,那么这里再创建一个默认的

3.2 load 重载方法1

load(Profile, DocumentFilterFactory, DocumentConsumer) 方法,加载指定 Profile 的配置信息,如果为空则解析出公共的配置

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    // <1> 先获取 `classpath:/`、`classpath:/config/`、`file:./`、`file:./config/` 四个路径
    // <2> 然后依次遍历,从该路径下找到对应的配置文件,找到了则通过 `consumer` 进行解析,并添加至 `loaded` 中
    getSearchLocations().forEach((location) -> {
        // <2.1> 判断是否是文件夹,这里好像都是
        boolean isFolder = location.endsWith("/");
        // <2.2> 是文件夹的话找到应用配置文件的名称,可以通过 `spring.config.name` 配置进行设置
        // Spring Cloud 中默认为 `bootstrap`,Spring Boot 中默认为 `application`
        Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
        // <2.3> 那么这里开始解析 `application` 配置文件了
        names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
    });
}

过程如下:

  1. 调用 getSearchLocations() 方法,获取 classpath:/classpath:/config/file:./file:./config/ 四个路径

    private Set<String> getSearchLocations() {
        Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
        if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
            locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY));
        }
        else {
            // 这里会得到 `classpath:/`、`classpath:/config/`、`file:./`、`file:./config/` 四个路径
            locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
        }
        return locations;
    }
    
  2. 然后依次遍历,从该路径下找到对应的配置文件,找到了则通过 consumer 进行解析,并添加至 loaded

    1. 判断是否是文件夹,这里好像都是

    2. 是文件夹的话找到应用配置文件的名称,默认就是 application 名称

      private Set<String> getSearchNames() {
          // 如果通过 `spring.config.name` 指定了配置文件名称
          if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
              String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
              // 进行占位符处理,并返回设置的配置文件名称
              return asResolvedSet(property, null);
          }
          // 如果指定了 `names` 配置文件的名称,则对其进行处理(占位符)
          // 没有指定的话则去 `application` 默认名称
          return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
      }
      
    3. 遍历上一步获取到 names,默认只有一个application,那么这里开始解析 application 配置文件了,调用的还是一个 load(..) 重载方法

总结下来就是这里会尝试从 classpath:/classpath:/config/file:./file:./config/ 四个文件夹下面解析 application 名称的配置文件

3.3 load 重载方法2

load(String, String, Profile, DocumentFilterFactory, DocumentConsumer) 方法,加载 application 配置文件,加载指定 Profile 的配置信息,如果为空则解析出公共的配置

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
        DocumentConsumer consumer) {
    // <1> 如果没有应用的配置文件名称,则尝试根据 `location` 进行解析,暂时忽略
    if (!StringUtils.hasText(name)) {
        for (PropertySourceLoader loader : this.propertySourceLoaders) {
            if (canLoadFileExtension(loader, location)) {
                load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                return;
            }
        }
        // 抛出异常
    }
    Set<String> processed = new HashSet<>();
    /**
     * <2> 遍历 PropertySourceLoader 对配置文件进行加载,这里有以下两个:
     * {@link PropertiesPropertySourceLoader} 加载 `properties` 和 `xml` 文件
     * {@link YamlPropertySourceLoader} 加载 `yml` 和 `yaml` 文件
     */
    for (PropertySourceLoader loader : this.propertySourceLoaders) {
        // 先获取 `loader` 的后缀,也就是说这里会总共会遍历 4 次,分别处理不同后缀的文件
        // 加上前面 4 种 `location`(文件夹),这里会进行 16 次加载
        for (String fileExtension : loader.getFileExtensions()) {
            // 避免重复加载
            if (processed.add(fileExtension)) {
                // 例如尝试加载 `classpath:/application.yml` 文件
                loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                        consumer);
            }
        }
    }
}

过程如下:

  1. 如果没有应用的配置文件名称,则尝试根据 location 进行解析,暂时忽略

  2. 遍历 PropertySourceLoader 对配置文件进行加载,回到 Loader 的构造方法中,会有 PropertiesPropertySourceLoaderYamlPropertySourceLoader 两个对象,前者支持 propertiesxml 后缀,后者支持 ymlyaml

    1. 获取 PropertySourceLoader 支持的后缀,然后依次加载对应的配置文件

      也就是说四种后缀,加上前面四个文件夹,那么接下来每次 3.load 方法 都会调用十六次 loadForFileExtension(..) 方法

3.4 loadForFileExtension 方法

loadForFileExtension(PropertySourceLoader, String, String, Profile, DocumentFilterFactory, DocumentConsumer) 方法,尝试加载 classpath:/application.yml 配置文件,加载指定 Profile 的配置信息,如果为空则解析出公共的配置

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
        Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
    // <1> 创建一个默认的 DocumentFilter 过滤器 `defaultFilter`
    DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
    // <2> 创建一个指定 Profile 的 DocumentFilter 过滤器 `profileFilter`
    DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
    // <3> 如果传入了 `profile`,那么尝试加载 `application-${profile}.yml`对应的配置文件
    if (profile != null) {
        // Try profile-specific file & profile section in profile file (gh-340)
        // <3.1> 获取 `profile` 对应的名称,例如 `application-dev.yml`
        String profileSpecificFile = prefix + "-" + profile + fileExtension;
        // <3.2> 尝试对该文件进行加载,公共配置
        load(loader, profileSpecificFile, profile, defaultFilter, consumer);
        // <3.3> 尝试对该文件进行加载,环境对应的配置
        load(loader, profileSpecificFile, profile, profileFilter, consumer);
        // Try profile specific sections in files we've already processed
        // <3.4> 也尝试从该文件中加载已经加载过的环境所对应的配置
        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
    // <4> 正常逻辑,这里尝试加载 `application.yml` 文件中对应 Profile 环境的配置
    // 当然,如果 Profile 为空也就加载公共配置
    load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

过程如下:

  1. 创建一个默认的 DocumentFilter 过滤器 defaultFilter

    private DocumentFilter getPositiveProfileFilter(Profile profile) {
        return (Document document) -> {
            // 如果没有指定 Profile,那么 Document 中的 `profiles` 也得为空
            // 也就是不能有 `spring.profiles` 配置,就是公共配置咯
            if (profile == null) {
                return ObjectUtils.isEmpty(document.getProfiles());
            }
            // 如果指定了 Profile,那么 Document 中的 `profiles` 需要包含这个 Profile
            // 同时,Environment 中也要接受这个 Document 中的 `profiles`
            return ObjectUtils.containsElement(document.getProfiles(), profile.getName())
                    && this.environment.acceptsProfiles(Profiles.of(document.getProfiles()));
        };
    }
    
  2. 创建一个指定 Profile 的 DocumentFilter 过滤器 profileFilter

  3. 如果传入了 profile,那么尝试加载 application-${profile}.yml对应的配置文件

    1. 获取 profile 对应的名称,例如 application-dev.yml
    2. 又调用 load(..) 重载方法加载 3.1 步的配置文件,这里使用 defaultFilter 过滤器,找到公共的配置信息
    3. 又调用 load(..) 重载方法加载 3.1 步的配置文件,这里使用 profileFilter 过滤器,找到指定 profile 的配置信息
    4. 也尝试从该文件中加载已经加载过的环境所对应的配置,也就是说 dev 的配置信息,也能在其他的 application-prod.yml 中读取
  4. 正常逻辑,继续调用 load(..) 重载方法,尝试加载 application.yml 文件中对应 Profile 环境的配置,当然,如果 Profile 为空也就加载公共配置

没有什么复杂的逻辑,继续调用重载方法

3.5 load 重载方法3

load(PropertySourceLoader, String, Profile, DocumentFilter,DocumentConsumer) 方法,尝试加载配置文件,加载指定 Profile 的配置信息,如果为空则解析出公共的配置

private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
        DocumentConsumer consumer) {
    try {
        // <1> 通过资源加载器获取这个文件资源
        Resource resource = this.resourceLoader.getResource(location);
        // <2> 如果文件资源不存在,那直接返回了
        if (resource == null || !resource.exists()) {
            return;
        }
        // <3> 否则,如果文件资源的后缀为空,跳过,直接返回
        if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
            return;
        }
        String name = "applicationConfig: [" + location + "]";
        // <4> 使用 PropertySourceLoader 加载器加载出该文件资源中的所有属性,并将其封装成 Document 对象
        // Document 对象中包含了配置文件的 `spring.profiles` 和 `spring.profiles.active` 属性
        // 一个文件不是对应一个 Document,因为在一个 `yml` 文件可以通过 `---` 来配置多个环境的配置,这里也就会有多个 Document
        List<Document> documents = loadDocuments(loader, name, resource);
        // <5> 如果没有解析出 Document,表明该文件资源无效,跳过,直接返回
        if (CollectionUtils.isEmpty(documents)) {
            return;
        }
        List<Document> loaded = new ArrayList<>();
        // <6> 通过 DocumentFilter 对 `document` 进行过滤,过滤出想要的 Profile 对应的 Document
        // 例如入参的 Profile 为 `dev` 那么这里只要 `dev` 对应 Document
        // 如果 Profile 为空,那么找到没有 `spring.profiles` 配置 Document,也就是我们的公共配置
        for (Document document : documents) {
            if (filter.match(document)) {
                // 如果前面还没有激活的 Profile
                // 那么这里尝试将 Document 中的 `spring.profiles.active` 添加至 `profiles` 中,同时删除 `default` 默认的 Profile
                addActiveProfiles(document.getActiveProfiles());
                addIncludedProfiles(document.getIncludeProfiles());
                loaded.add(document);
            }
        }
        // <7> 将需要的 Document 们进行倒序,因为配置在后面优先级越高,所以需要反转一下
        Collections.reverse(loaded);
        // <8> 如果有需要的 Document
        if (!loaded.isEmpty()) {
            /**
             * 借助 Lambda 表达式调用 {@link #addToLoaded} 方法
             * 将这些 Document 转换成 MutablePropertySources 保存至 {@link this#loaded} 集合中
             */
            loaded.forEach((document) -> consumer.accept(profile, document));
        }
    } catch (Exception ex) {
        throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
    }
}

过程如下:

  1. 通过资源加载器获取这个文件资源,例如 classpath:/application.yml

  2. 如果文件资源不存在,那直接返回了

  3. 否则,如果文件资源的后缀为空,跳过,直接返回

  4. 调用 loadDocuments(..) 方法,使用 PropertySourceLoader 加载器加载出该文件资源中的所有属性,并将其封装成 Document 对象

    Document 对象中包含了配置文件的 spring.profilesspring.profiles.active 属性,一个文件不是对应一个 Document,因为在一个 yml 文件可以通过 --- 来配置多个环境的配置,这里也就会有多个 Document

  5. 如果没有解析出 Document,表明该文件资源无效,跳过,直接返回

  6. 通过 DocumentFilter 对 document 进行过滤,过滤出想要的 Profile 对应的 Document

    例如入参的 Profile 为 dev 那么这里只要 dev 对应 Document,如果 Profile 为空,那么找到没有 spring.profiles 配置 Document,也就是我们的公共配置

    1. 如果前面还没有激活的 Profile,那么这里尝试将 Document 中的 spring.profiles.active 添加至 profiles 中,同时删除 default 默认的 Profile
  7. 将需要的 Document 们进行倒序,因为配置在后面优先级越高,所以需要反转一下

  8. 如果有需要的 Document,借助 Lambda 表达式调用 addToLoaded(..) 方法,将这些 Document 转换成 MutablePropertySources 保存至 this#loaded 集合中

逻辑没有很复杂,找到对应的 application.yml 文件资源,解析出所有的配置,找到指定 Profile 对应的配置信息,然后添加到集合中

你要知道的是上面第 4 步得到的 Document 对象,例如 application.yml 中设置 dev 环境激活,有两个 devprod 不同的配置,那么这里会得到三个 Document 对象

3.6 loadDocuments 方法

loadDocuments(PropertySourceLoader, String, Resource) 方法,从文件资源中加载出 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) {
        // 使用 PropertySourceLoader 加载器进行加载
        List<PropertySource<?>> loaded = loader.load(name, resource);
        // 将 PropertySource 转换成 Document
        documents = asDocuments(loaded);
        // 放入缓存
        this.loadDocumentsCache.put(cacheKey, documents);
    }
    return documents;
}

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) {
        // 使用 PropertySourceLoader 加载器进行加载
        List<PropertySource<?>> loaded = loader.load(name, resource);
        // 将 PropertySource 转换成 Document
        documents = asDocuments(loaded);
        // 放入缓存
        this.loadDocumentsCache.put(cacheKey, documents);
    }
    return documents;
}

逻辑比较简单,先通过 PropertySourceLoader 加载配置文件,例如 YamlPropertySourceLoader 加载 application.yml 配置文件

然后将加载出来的 PropertySource 属性源对象们一一封装成 Document 对象,同时放入缓存中

YamlPropertySourceLoader

org.springframework.boot.env.YamlPropertySourceLoaderymlyaml 配置文件的加载器

public class YamlPropertySourceLoader implements PropertySourceLoader {

	@Override
	public String[] getFileExtensions() {
		return new String[] { "yml", "yaml" };
	}

	@Override
	public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
		// 如果不存在 `org.yaml.snakeyaml.Yaml` 这个 Class 对象,则抛出异常
		if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
			throw new IllegalStateException(
					"Attempted to load " + name + " but snakeyaml was not found on the classpath");
		}
		// 通过 Yaml 解析该文件资源
		List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
		if (loaded.isEmpty()) {
			return Collections.emptyList();
		}
		// 将上面获取到的 Map 集合们一一封装成 OriginTrackedMapPropertySource 对象
		List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
		for (int i = 0; i < loaded.size(); i++) {
			String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";
			propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
					Collections.unmodifiableMap(loaded.get(i)), true));
		}
		return propertySources;
	}

}

可以看到,主要就是通过 org.yaml.snakeyaml.Yaml 解析配置文件

3.7 addToLoaded 方法

addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>>, boolean) 方法,将加载出来的配置信息保存起来,如下:

private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
        boolean checkForExisting) {
    return (profile, document) -> {
        // 如果需要检查是否存在,存在的话直接返回
        if (checkForExisting) {
            for (MutablePropertySources merged : this.loaded.values()) {
                if (merged.contains(document.getPropertySource().getName())) {
                    return;
                }
            }
        }
        // 获取 `loaded` 中该 Profile 对应的 MutablePropertySources 对象
        MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                (k) -> new MutablePropertySources());
        // 往这个 MutablePropertySources 对象中添加 Document 对应的 PropertySource
        addMethod.accept(merged, document.getPropertySource());
    };
}

loaded 中添加该 Profile 对应的 PropertySource 属性源们

4. addLoadedPropertySources 方法

addLoadedPropertySources() 方法,将前面加载出来的所有 PropertySource 配置信息们添加到 Environment 环境中

private void addLoadedPropertySources() {
    // 获取当前 Spring 应用的 Environment 环境中的配置信息
    MutablePropertySources destination = this.environment.getPropertySources();
    // 将上面已加载的每个 Profile 对应的属性信息放入一个 List 集合中 `loaded`
    List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
    // 将 `loaded` 进行翻转,因为写在后面的环境优先级更高
    Collections.reverse(loaded);
    String lastAdded = null;
    Set<String> added = new HashSet<>();
    // 遍历 `loaded`,将每个 Profile 对应的属性信息按序添加到 Environment 环境中
    for (MutablePropertySources sources : loaded) {
        for (PropertySource<?> source : sources) {
            if (added.add(source.getName())) {
                // 放入上一个 PropertySource 的后面,优先默认配置
                addLoadedPropertySource(destination, lastAdded, source);
                lastAdded = source.getName();
            }
        }
    }
}

过程如下:

  1. 获取当前 Spring 应用的 Environment 环境中的配置信息 destination
  2. 将上面已加载的每个 Profile 对应的属性信息放入一个 List 集合中 loaded
  3. loaded 进行翻转,因为写在后面的环境优先级更高❓❓❓前面不是翻转过一次吗?好吧,暂时忽略
  4. 遍历 loaded,将每个 Profile 对应的 PropertySources 属性信息按序添加到 Environment 环境中

5. applyActiveProfiles 方法

applyActiveProfiles(PropertySource<?> defaultProperties) 方法,设置被激活的 Profile 环境

private void applyActiveProfiles(PropertySource<?> defaultProperties) {
    List<String> activeProfiles = new ArrayList<>();
    // 如果默认的配置信息不为空,通常为 `null`
    if (defaultProperties != null) {
        Binder binder = new Binder(ConfigurationPropertySources.from(defaultProperties),
                new PropertySourcesPlaceholdersResolver(this.environment));
        activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.include"));
        if (!this.activatedProfiles) {
            activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.active"));
        }
    }
    // 遍历已加载的 Profile 对象,如果它不为 `null` 且不是默认的,那么添加到需要 `activeProfiles` 激活的队列中
    this.processedProfiles.stream().filter(this::isDefaultProfile).map(Profile::getName)
            .forEach(activeProfiles::add);
    // 设置 Environment 需要激活的环境名称
    this.environment.setActiveProfiles(activeProfiles.toArray(new String[0]));
}

逻辑比较简单,例如我们配置了 spring.profiles.active=dev,那么这里将设置 Environment 被激活的 Profile 为 dev

总结

本文分析了 Spring Boot 加载 application.yml 配置文件并应用于 Spring 应用的 Environment 环境对象的整个过程,主要是借助于 Spring 的 ApplicationListener 事件监听器机制,在启动 Spring 应用的过程中,准备好 Environment 的时候会广播 应用环境已准备好 事件,然后 ConfigFileApplicationListener 监听到该事件会进行处理。

加载 application 配置文件的整个过程有点绕,嵌套有点深,想深入了解的话查看上面的内容,每个小节都进行了编号。

大致流程就是先加载出 application.yml 文件资源,然后找到需要的 Profile 对应的 PropertySource 属性信息,包括公告配置,最后将这些 PropertySource 应用于 Environment。

posted @ 2021-07-08 09:00  月圆吖  阅读(1523)  评论(5编辑  收藏  举报