SpringBoot(五)外部化配置 - Environment

前言

        最近在学习Spring Boot相关的课程,过程中以笔记的形式记录下来,方便以后回忆,同时也在这里和大家探讨探讨,文章中有漏的或者有补充的、错误的都希望大家能够及时提出来,本人在此先谢谢了!

开始之前呢,希望大家带着几个问题去学习:
1、Spring Boot 外部化配置是什么?
2、整体流程或结构是怎样的?
3、核心部分是什么?
4、怎么实现的?
这是对自我的提问,我认为带着问题去学习,是一种更好的学习方式,有利于加深理解。好了,接下来进入主题。

1、起源

        这篇文章我们就来讨论 Spring Boot 的外部化配置功能,该功能主要是通过外部的配置资源实现与代码的相互配合,来避免硬编码,提供应用数据或行为变化的灵活性。相信小伙伴们在日常工作中都有使用过,如在 properties 或者 YAML 文件中定义好key value格式的数据后,就可在程序中通过 @Value 注解获取该value值。还可以定义一些同外部组件约定好的key,如以 springredis 等组件名为前缀的key,之后相应的组件就可读取到该value值进行工作。当然,这只是外部化配置的一小部分内容,接下来进行详细讨论。

注:本篇文章所用到的 Spring Boot版本是 2.1.6.BUILD-SNAPSHOT

2、外部化配置的资源类型

        先来看看外部化配置的几种资源类型,除了 propertiesYAML 外,还有环境变量、系统属性、启动参数等。所有的资源类型将近二十种,这里只介绍我们比较熟悉的:

  1. properties :这个应该都知道,就是在以 .properties 为后缀的文件中定义key value格式数据。
  2. YAML:文件格式是以 .yml 为后缀,文件中的数据也是key value格式,如下:
user:
  name: loong
  age: 10

这里的key就是 user.nameuser.age

  1. 环境变量:这是通过 System.getenv() 方式获取的默认配置,也是key value格式,下面列出部分配置,其它的还请自行了解,如下:
名称 Key
Java安装目录 JAVA_HOME
classpath环境变量 CLASSPATH
用户临时文件目录 TEMP
计算机名 COMPUTERNAME
用户名 USERNAME
  1. 系统属性:这是通过 System.getProperties() 方式获取的默认配置,也是key value格式,下面列出部分配置,其它的还请自行了解,如下:
名称 Key
运行时环境版本 java.version Java
Java安装目录 java.home
要使用的 JIT编译器的名称 java.compiler
操作系统的架构 os.arch
操作系统的版本 os.version
  1. 启动参数:这个在 《Spring Boot SpringApplication 启动类(二)》
    这篇文章中讨论过。一种是在 jar 包运行时行时传递的参数,如:java -jar xxx.jar name=张三 pwa=123 ,还有一种是在 IDEA 的 Program arguments 中输入数据:
image

可以看到,外部化配置中的数据都是key value 格式。这里还要注意它们的加载顺序,当key相同时,会出现覆盖的情况。

3、外部化配置的核心

        接下来,我们的重心来围绕 propertiesYAML 配置文件,这两者也是我们日常工作中常用的。首先来看取值方式,在 Spring 时代有 Environment@ValueXML 三种方式,在 Spring Boot 时代则是 @ConfigurationProperties 方式。其中,涉及到了一个核心类,它就是 Environment ,该对象不仅可以获取所有的外部化配置数据,就连另外几种取值方式的数据来源也是从该类中获取。这里,主要对 Environment@ConfigurationProperties 进行详细讨论,笔者认为 Environment@ConfigurationProperties 才是 Spring Boot 外部化配置的核心所在。

3.1 Environment

该类在 《Spring Boot SpringApplication 启动类(二)》 的 2.3 章节有简要说明过,这里我们展开详细讨论。首先回顾一下代码:

public class SpringApplication {

    ...
    
    public ConfigurableApplicationContext run(String... args) {
        ...
        
        try {
            ...
            
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        
        ...
        
    }
    ...
}

SpringApplication 运行阶段的 run 方法中通过 prepareEnvironment 方法了创建 ConfigurableEnvironment 的实现类对象,ConfigurableEnvironment 是一个接口,且继承了 Environment 。进入创建方法:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    listeners.environmentPrepared(environment);
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

首先看第一行,通过 getOrCreateEnvironment 方法创建 ConfigurableEnvironment 对象:

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

《Spring Boot SpringApplication 启动类(二)》 的 2.3 章节说过, webApplicationType 存储的是应用的类型,有 ReactiveServlet 等,是在 SpringApplication 准备阶段推导出来的,而本项目推导出来是 Servlet 类型,所以实例化的是 StandardServletEnvironment 对象:

public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {

    public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";

    public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";

    public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";

    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
        propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
        if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
            propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
        }
        super.customizePropertySources(propertySources);
    }
    
    ...

}

而该类又继承了 StandardEnvironment 类。且重写了 customizePropertySources 方法,并调用了父类的 customizePropertySources 方法。我们继续往下深入:

public class StandardEnvironment extends AbstractEnvironment {

    public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";

    public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
    
    @Override
    protected void customizePropertySources(MutablePropertySources propertySources) {
        propertySources.addLast(
                new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
        propertySources.addLast(
                new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
    }

}

继续看它的 AbstractEnvironment 父抽象类:

public abstract class AbstractEnvironment implements ConfigurableEnvironment {
    
    ...
    
    private final MutablePropertySources propertySources = new MutablePropertySources();
    
    ...
    
    public AbstractEnvironment() {
        customizePropertySources(this.propertySources);
    }
    ...
}

可以看到,最终会有一个 AbstractEnvironment 抽象类。在 StandardServletEnvironment 初始化时,会调用 AbstractEnvironment 的构造方法,里面调用了子类重写的 customizePropertySources 方法,且入参是 MutablePropertySources 对象,该对象是 Environment 的一个属性,是底层真正存储外部化配置的。之后, StandardServletEnvironmentStandardEnvironmentcustomizePropertySources 方法相继执行,主要是往 MutablePropertySources 对象中添加外部化配置。其中我们前面所说的环境变量和系统属性是在 StandardEnvironment 重写的方法中进行加载。

我们回到外面的 prepareEnvironment 方法,继续往下走。接着执行的是 configureEnvironment 方法,该方法主要是把启动参数加入到 MutablePropertySources 中。之后,我们断点看看有多少种外部化配置:

image

有五种,且真正存储数据的是 MutablePropertySources 中的 PropertySource 实现类集合。

这里简要介绍一下 PropertySource ,我们将其称之为配置源,官方定义它是外部化配置的API描述方式,是外部化配置的一个媒介。 用我们的话来说,它是一个抽象类,提供了统一存储外部化配置数据的功能,而每种外部化配置有具体的实现类,主要提供不同的基础操作,如 getcontains 等 。我们来看看 PropertySource 对象的数据格式,一般包含:

name : 外部化配置的名称
source : 存储配置中的数据,底层一般数据格式都是key value

我们继续往下走,接着调用了 SpringApplicationRunListenersenvironmentPrepared 方法。在上篇文章
《Spring Boot SpringApplication 启动类(二)》 的 2.1 小节讲过,当 Spring Boot 执行到某一阶段时,会通过 SpringSimpleApplicationEventMulticaster 事件广播器进行事件广播,之后 ,相应监听器就会监听到该事件,执行调监听器的 onApplicationEvent 方法。这里表示 Spring Boot 到了 ConfigurableEnvironment 构建完成时阶段。我们进入该方法:

class SpringApplicationRunListeners {

    ...

    private final List<SpringApplicationRunListener> listeners;

    public void environmentPrepared(ConfigurableEnvironment environment) {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.environmentPrepared(environment);
        }
    }
    ...
}

真正调用的是 SpringApplicationRunListener 集合中的 environmentPrepared 方法。 SpringApplicationRunListener 是一个接口,它具有唯一实现类 EventPublishingRunListener

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

    ...
    
    private final SimpleApplicationEventMulticaster initialMulticaster;

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

可以看到,最终通过 SimpleApplicationEventMulticastermulticastEvent 方法发布 ApplicationEnvironmentPreparedEvent 事件。上面说过, Spring Boot 监听器会监听到该事件,其中一个名为 ConfigFileApplicationListener 的监听器,监听到该事件后会进行加载 applicationYAML 配置文件的操作,接下来,我们具体的来看一看该类实现。

3.1.1、ConfigFileApplicationListener

        我们直接进入该类:

public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
    
    ...
    
    private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
    
    public static final String CONFIG_NAME_PROPERTY = "spring.config.name";

    public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
    
    public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
    
    private static final String DEFAULT_NAMES = "application";
    
    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        // 1、通过 instanceof 判断事件的类型,如果是 ApplicationEnvironmentPreparedEvent 事件,则执行 onApplicationEnvironmentPreparedEvent 方法
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
        }
        ...
    }
    
    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        // 2、调用 loadPostProcessors 方法,返回 Environment 的后置处理器集合,我们跳到 2.1 查看方法实现
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        
        // 2.2、把自己也加入该集合
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        
        // 2.3、遍历 EnvironmentPostProcessor 集合,执行它们的 postProcessEnvironment 方法,我们跳到 3 查看当前类的该方法实现
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
        }
    }
    
    // 2.1 是我们比较熟悉的 loadFactories 方法,在 Spring Boot 自动装配(二) 的 2.1.2 小节讲过,loadFactories 方法是从 spring.factories 文件中加载 key 为 EnvironmentPostProcessor 的实现类集合
    List<EnvironmentPostProcessor> loadPostProcessors() {
        return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
    }
    
    // 3、 执行到该方法时,会调用 addPropertySources 方法,入参是上文加载 ConfigurableEnvironment 对象
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
    }
    
    protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        
        // 4、 我们主要关注这里,通过 Loader 的构造方法创建该对象,并调用它的 load 方法
        new Loader(environment, resourceLoader).load();
    }
    
    private class Loader {
    
        private final ConfigurableEnvironment environment;
        
        private final List<PropertySourceLoader> propertySourceLoaders;
    
        // 4.1、 构造方法中会初始化一些属性
        Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
            ...
            this.environment = environment;
            
            // 又是我们比较熟悉的 loadFactories 方法,在 Spring Boot 自动装配(二) 的 2.1.2 小节讲过,loadFactories 方法是从 spring.factories 文件中加载 key 为 PropertySourceLoader 的实现类集合。这里加载的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 两个实现类,看类名可初步断定是处理 properties 和 YAML 文件的
            this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());
        }
        
        public void load() {
            ...
            // 5、这里会继续调用它重载的 load 方法
            load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
            ...
            
            // 9、这是最后一步,将当前类中的 MutablePropertySources 中的 PropertySource 对象,全部塞到 ConfigurableEnvironment 的 MutablePropertySources 对象中。我们跳到 9.1 进行查看
            addLoadedPropertySources();
        }
        
        private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            
            // 5.1、首先执行 getSearchLocations 方法,看方法名大致能猜出是获取搜索路径的,我们跳到 5.1.1 查看该方法的实现
            getSearchLocations().forEach((location) -> {
            
                // 5.2、开始遍历该集合,先判断该路径是否是以反斜杠结尾,是的话则该路径为文件夹;不是的话,则该路径为文件的完整路径,类似于 classPath:/application.properties 
                boolean isFolder = location.endsWith("/");
                
                // 5.3、 如果是文件夹路径,则通过 getSearchNames 获取文件的名称,不是则返回空集合,我们跳到 5.3.1 查看 getSearchNames 方法
                Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
                
                // 5.4、再调用 load 的重载方法,这里,location 是路径名,name是文件名,我们跳到 6 进行查看
                names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
            });
        }
        
        // 5.1.1、这个方法就是获取加载 application 和 YAML 文件路径的
        private Set<String> getSearchLocations() {
        
            //  可以看到 CONFIG_LOCATION_PROPERTY 的值为 spring.config.location,也就是说,先判断我们有没有手动设置搜索路径,有的话直接返回该路径。该值一般通过启动参数的方式设置
            if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
                return getSearchLocations(CONFIG_LOCATION_PROPERTY);
            }
            
            // 该 CONFIG_ADDITIONAL_LOCATION_PROPERTY 变量的值为 spring.config.additional-location,这也是用于手动设置搜索路径,不过和上面不同的是,不会覆盖 接下来默认的搜索路径
            Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
            
            // 这里就是获取默认的搜索路径,通过 DEFAULT_SEARCH_LOCATIONS 变量的值 classpath:/,classpath:/config/,file:./,file:./config/,将该值用逗号分隔,加入集合并返回。到这一步,我们至少获取到了4个加载 application 和 YAML 文件的路径
            locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
            return locations;
        }
        
        // 5.3.1 
        private Set<String> getSearchNames() {
        
            // CONFIG_LOCATION_PROPERTY 变量值为 spring.config.name ,同样先判断有没有手动设置文件名称,有的话,直接返回
            if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
                String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
                return asResolvedSet(property, null);
            }
            
            // 如果没有,则通过 DEFAULT_NAMES 变量值返回默认的文件名,变量值为 application
            return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
        }
        
        // 6、
        private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
                DocumentConsumer consumer) {
                
            // 6.1、 上面 5.2 说过 name 为空时,表示 location 是完整的文件路径。之后进入这个 if 
            if (!StringUtils.hasText(name)) {
            
                // 6.1.1、propertySourceLoaders 属性是在 4.1 处被初始化的,存储的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 两个类。这里对这两个类进行遍历
                for (PropertySourceLoader loader : this.propertySourceLoaders) {
                
                    // 我们跳到 6.1.2 查看 canLoadFileExtension 方法实现,入参 location 是文件的完整路径
                    if (canLoadFileExtension(loader, location)) {
                        
                        // 这里又是一个 load 重载方法,我们跳到 7 进行查看
                        load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
                        return;
                    }
                }
            }
            Set<String> processed = new HashSet<>();
            for (PropertySourceLoader loader : this.propertySourceLoaders) {
            
                // 6.2 这里和 6.1.3 类似,获取文件扩展名
                for (String fileExtension : loader.getFileExtensions()) {
                    if (processed.add(fileExtension)) {
                        
                        // 进入 6.3、查看该方法实现。关注重点的两个参数:一个是路径名 + 文件名,还有一个 “.” +文件扩展名
                        loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
                                consumer);
                    }
                }
            }
        }
        
        // 6.1.2、 该方法作用是 判断 name 完整路径名是否以指定的文件扩展名结尾
        private boolean canLoadFileExtension(PropertySourceLoader loader, String name) {
        
            // 6.1.3、调用 PropertySourceLoader 的 getFileExtensions 方法。当你的实现类是 PropertiesPropertySourceLoader 时,该方法返回 properties、xml;如果是 YamlPropertySourceLoader 则返回 yml、yaml。从这里可以看出,能被处理的文件格式有这四种
            return Arrays.stream(loader.getFileExtensions())
                    .anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name, fileExtension));
        }
        
        // 6.3 到了这里,prefix 和 fileExtension 都是进行拼接好的值,如 prefix = classpath:/applicarion,fileExtension = .properties
        private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
                Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
            
            ...
            
            // 这里同样调用节点 7 的重载方法,通过 prefix + fileExtension 形成完整的文件路径名,通过入参进行传递。如 classpath:/applicarion.properties
            load(loader, prefix + fileExtension, profile, profileFilter, consumer);
        }
        
        // 7、
        private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
                DocumentConsumer consumer) {
            try {
            
                // 这里调用 ResourceLoader 的 getResource 方法,通过 location 文件路径,读取获取该文件资源,之后就好办了
                Resource resource = this.resourceLoader.getResource(location);
                
                ...
                
                // 具体解析在过程 loadDocuments 中,这里就不继续跟踪了,大致是以流的方式解析文件。解析之后会生成一个 PropertySource 对象,该对象在上面说过,表示一个外部化配置源对象,存储配置中的数据。之后,会将该对象封装到 Document 中
                List<Document> documents = loadDocuments(loader, name, resource);
                
                ...
                
                if (!loaded.isEmpty()) {
                
                    // 遍历 documents 集合,当执行 consumer.accept 时会进入 addToLoaded 方法,这是 Java8 的写法。consumer 对象参数来自节点 5 。我们跳到 8 查看 addToLoaded 实现
                    loaded.forEach((document) -> consumer.accept(profile, document));
                    if (this.logger.isDebugEnabled()) {
                        StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
                        this.logger.debug(description);
                    }
                }
            }
            catch (Exception ex) {
                throw new IllegalStateException("Failed to load property " + "source from location '" + location + "'",
                        ex);
            }
        }
        
        // 8、BiConsumer 是 JAVA8 的函数接口,表示定义一个带有两个参数且不返回结果的操作,通过节点 5 我们知道,这个操作是 MutablePropertySources::addFirst 。
        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;
                        }
                    }
                }
                MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
                        (k) -> new MutablePropertySources());
                        
                // 当调用 BiConsumer 的 accept 方法时,定义的操作会执行,两个入参分别是 MutablePropertySources 对象和配置文件源对象 PropertySource。该操作会调用 MutablePropertySources 的 addFirst 方法把该配置文件源对象添加至其中。最后我们去看看前面 load 方法中的最后一步 9
                addMethod.accept(merged, document.getPropertySource());
            };
        }
        
        // 9.1
        private void addLoadedPropertySources() {
        
            // 获取当前上下文环境中的 MutablePropertySources 对象
            MutablePropertySources destination = this.environment.getPropertySources();
            
            // 获取当前类中的 MutablePropertySources 集合
            List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
            Collections.reverse(loaded);
            String lastAdded = null;
            Set<String> added = new HashSet<>();
            
            // 遍历 loaded 集合及其中所有的 PropertySource ,也就是 application 或 YAML 配置文件源对象
            for (MutablePropertySources sources : loaded) {
                for (PropertySource<?> source : sources) {
                    if (added.add(source.getName())) {
                    
                        // 我们进入 9.2 查看该方法,主要参数是上下文环境中的 MutablePropertySources 对象和配置文件源对象
                        addLoadedPropertySource(destination, lastAdded, source);
                        lastAdded = source.getName();
                    }
                }
            }
        }

        // 9.2 
        private void addLoadedPropertySource(Mutab lePropertySources destination, String lastAdded,
                PropertySource<?> source) {
            if (lastAdded == null) {
                if (destination.contains(DEFAULT_PROPERTIES)) {
                    destination.addBefore(DEFAULT_PROPERTIES, source);
                }
                else {
                    destination.addLast(source);
                }
            }
            else {
            
                // 最后通过将 source 添加到 environment 中的 MutablePropertySources 对象中。
                destination.addAfter(lastAdded, source);
            }
        }
        
        // 至此,properties 和 YAML 配置文件就被加载到了上下文环境共享的 Environment 中,之后如 @Value 等获取值都是从该对象中获取
    }
}

可以看到,ConfigFileApplicationListener 主要功能就是将 propertiesYAML 文件加载到 Environment 中。另外还存在一个 @PropertySource 注解,也是加载指定的配置文件到 Environment 中。

3.1.2、关联 SpringConfigurationPropertySources

我们回到最外面的 prepareEnvironment 方法,来看看执行完监听方法时 ConfigurableEnvironment 中加载了多少种外部化配置:

image

有七种,包括新增的 properties 配置文件。

之后还有一个操作,通过 ConfigurationPropertySources.attach 关联 SpringConfigurationPropertySources 类,该类主要是做一个适配器的工作,将 MutablePropertySources 转换为 ConfigurationPropertySource,下一小节将会用到对象。我们进入 attach 方法查看:


public final class ConfigurationPropertySources {
    
    private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties";
    
    public static void attach(Environment environment) {
        Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
        
        // 获取 ConfigurableEnvironment 中的 MutablePropertySources 
        MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
        
        // 获取名为 configurationProperties 的外部化配置源对象
        PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
        
        // 如果存在,则把该对象移除
        if (attached != null && attached.getSource() != sources) {
            sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
            attached = null;
        }
        
        // 不存在,则添加一个配置源对象,具体对象类型为 ConfigurationPropertySourcesPropertySource,源对象中的数据为 SpringConfigurationPropertySources 
        if (attached == null) {
            sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
                    new SpringConfigurationPropertySources(sources)));
        }
    }
}

到这里,ConfigurableEnvironment 又新增了一个 ConfigurationPropertySourcesPropertySource 类型的配置源对象。我们主要来关注 SpringConfigurationPropertySources 对象,可以看到,这里是通过它的带参构造器创建该对象,参数 sources 是从 ConfigurableEnvironment 中获取的 MutablePropertySources 对象。我们进入 SpringConfigurationPropertySources 类中查看:

class SpringConfigurationPropertySources implements Iterable<ConfigurationPropertySource> {
    
    ...
    
    private final Iterable<PropertySource<?>> sources;
    
    SpringConfigurationPropertySources(Iterable<PropertySource<?>> sources) {
        Assert.notNull(sources, "Sources must not be null");
        this.sources = sources;
    }
    
    ...
}

可以看到,外部 ConfigurableEnvironmentMutablePropertySources 关联到了该类中的 Iterable (继承关系) 对象,这就是在进行适配工作。

至此, Environment 的创建过程及加载外部化配置的过程就到这里结束,我们简要回顾一下该流程:

  1. 首先 Environment 是一个较为特殊的类,术语称之为应用运行时的环境。它存储了所有的外部化配置,可以通过它获取任意配置数据,并且 @Value@ConfigurationProperties 等其它获取配置数据的方式都依赖于该类。
  2. 通过判断应用的类型,来创建不同环境的 Environment ,有 ServletReactive、非 Web 类型。
  3. 之后会相继添加外部化配置到该类中,每种外部化配置都对应了一个 PropertySource 配置源对象。
  4. 重点介绍了加载 propertiesYAML 的方式。主要是通过回调 Spring Boot 的监听器 ConfigFileApplicationListener 进行处理。

加载完之后,就可以在应用中通过 @Autowired 注入该对象,获取任意外部化配置属性。

接下来要讨论的是在 Spring Boot 时代是如何通过 @ConfigurationProperties 来获取配置属性的。因篇幅过长, @ConfigurationProperties 内容另起一章。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容