跳至主要內容

7.配置加载

Java突击队大约 25 分钟

7.配置加载

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

Spring Boot 版本:2.2.x

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

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

该系列其他文章请查看:《死磕 Spring Boot 源码分析 - 文章导读》open in new window

概述

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

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

回顾

回到前面的 《SpringApplication 启动类的启动过程》open in new window 这篇文章,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-jarxxx.jar--spring.profiles.active=devorjava-jar-Dspring.profiles.active=devxxx.jar,那么这里的profiles就会有一个null和一个dev
2、 java-jarxxx.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` 文件,并解析;如果 `profile` 为 `null`,则会解析出 `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名称;

    ```java 
    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 的配置信息,如果为空则解析出公共的配置

```java 
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。

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址:open in new window