抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

SpringBoot加载配置文件

读过SpringBoot源码的同学应该都知道它会在启动过程中根据spring.factories加载监听器,而其中有一个名叫ConfigFileApplicationListener的监听器,它的作用为加载配置信息,即application.xml、application.yml。

常量值说明

ConfigFileApplicationListener定义了一批常量,他们主要为加载配置文件服务,现在就总体地看看这些常量吧。

名称类型默认值说明
DEFAULT_SEARCH_LOCATIONSStringclasspath:/,classpath:/config/,file:./,file:./config/配置文件加载顺序
DEFAULT_NAMESStringapplication默认配置文件名称
ACTIVE_PROFILES_PROPERTYStringspring.profiles.active“活动配置文件”属性名称
INCLUDE_PROFILES_PROPERTYStringspring.profiles.include“包含配置文件”属性名称。
CONFIG_NAME_PROPERTYStringspring.config.name“配置名称”属性名称。
CONFIG_LOCATION_PROPERTYStringspring.config.location“配置位置”属性名称。
DEFAULT_ORDERintInteger.MIN_VALUE+10处理器的默认顺序。

配置文件加载

监听器入口

阅读SpringBoot启动源码时,我们都知道监听器真实生效的方法是onApplicationEventConfigFileApplicationListener中的onApplicationEvent的源码如下:

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

通过onApplicationEnvironmentPreparedEvent=>postProcessEnvironment方法

public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        //将配置文件属性源添加到指定的环境
        addPropertySources(environment, application.getResourceLoader());
        //设置spring.beaninfo.ignore变量
        configureIgnoreBeanInfo(environment);
        //把环境绑定到SpringApplication。
        bindToSpringApplication(environment, application);
    }

addPropertySources源码如下:

protected void addPropertySources(ConfigurableEnvironment environment,
            ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
        new Loader(environment, resourceLoader).load();
    }

通过上述代码,可以看出配置文件的接在就在load方法中。

文件加载

load方法源码:

public void load() {
            this.propertiesLoader = new PropertySourcesLoader();
            this.activatedProfiles = false;
            this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
            this.processedProfiles = new LinkedList<Profile>();

            // Pre-existing active profiles set via Environment.setActiveProfiles()
            // are additional profiles and config files are allowed to add more if
            // they want to, so don't call addActiveProfiles() here.
            Set<Profile> initialActiveProfiles = initializeActiveProfiles();
            this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
            if (this.profiles.isEmpty()) {
                for (String defaultProfileName : this.environment.getDefaultProfiles()) {
                    Profile defaultProfile = new Profile(defaultProfileName, true);
                    if (!this.profiles.contains(defaultProfile)) {
                        this.profiles.add(defaultProfile);
                    }
                }
            }

            // The default profile for these purposes is represented as null. We add it
            // last so that it is first out of the queue (active profiles will then
            // override any settings in the defaults when the list is reversed later).
            //翻译:下面一行代码的目的是将默认配置文件被表示为null。将null添加到最后,以便它首先从队列中取出(主动概要文件将在稍后颠倒列表时覆盖缺省情况下的任何设置)。
            this.profiles.add(null);

            while (!this.profiles.isEmpty()) {
                Profile profile = this.profiles.poll();
                 //查找配置文件
                for (String location : getSearchLocations()) {
                    if (!location.endsWith("/")) {
                        // location is a filename already, so don't search for more
                        // filenames
                        load(location, null, profile);
                    }
                    else {
                        for (String name : getSearchNames()) {
                            load(location, name, profile);
                        }
                    }
                }
                this.processedProfiles.add(profile);
            }

            addConfigurationProperties(this.propertiesLoader.getPropertySources());
        }

查找配置文件路径

值得注意的是getSearchLocations方法,其源码如下:

private Set<String> getSearchLocations() {
            Set<String> locations = new LinkedHashSet<String>();
            // User-configured settings take precedence, so we do them first
            // 判断当前环境中是否有spring.config.location属性,如果有的话,则加载spring.config.location指定的配置文件
            if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
                for (String path : asResolvedSet(
                        this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
                    if (!path.contains("$")) {
                        path = StringUtils.cleanPath(path);
                        if (!ResourceUtils.isUrl(path)) {
                            path = ResourceUtils.FILE_URL_PREFIX + path;
                        }
                    }
                    locations.add(path);
                }
            }
            //添加默认的配置文件,按照类中定义的顺序加载文件:
            //其顺序为:classpath:/,classpath:/config/,file:./,file:./config/
            locations.addAll(
                    asResolvedSet(ConfigFileApplicationListener.this.searchLocations,
                            DEFAULT_SEARCH_LOCATIONS));
            return locations;
        }

DEFAULT_SEARCH_LOCATIONS为文件默认加载顺序,其值为classpath:/,classpath:/config/,file:./,file:./config/,然后springboot会按照这几个位置的由后到前的顺序去加载。

通过查看asResolvedSet的源码:

private Set<String> asResolvedSet(String value, String fallback) {
            List<String> list = Arrays.asList(StringUtils.trimArrayElements(
                    StringUtils.commaDelimitedListToStringArray(value != null
                            ? this.environment.resolvePlaceholders(value) : fallback)));
            Collections.reverse(list);
            return new LinkedHashSet<String>(list);
        }

不难看出,这里对list进行了Collections.reverse(反转处理)。

即配置文件真正的加载顺序为:

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/
  • spring.config.location

正式加载配置文件

load(String location, String name, Profile profile)方法源码:

private void load(String location, String name, Profile profile) {
            String group = "profile=" + (profile == null ? "" : profile);
            if (!StringUtils.hasText(name)) {
                // Try to load directly from the location
                loadIntoGroup(group, location, profile);
            }
            else {
                // Search for a file with the given name
                for (String ext : this.propertiesLoader.getAllFileExtensions()) {
                    if (profile != null) {
                        // Try the profile-specific file
                        loadIntoGroup(group, location + name + "-" + profile + "." + ext,
                                null);
                        for (Profile processedProfile : this.processedProfiles) {
                            if (processedProfile != null) {
                                loadIntoGroup(group, location + name + "-"
                                        + processedProfile + "." + ext, profile);
                            }
                        }
                        // Sometimes people put "spring.profiles: dev" in
                        // application-dev.yml (gh-340). Arguably we should try and error
                        // out on that, but we can be kind and load it anyway.
                        loadIntoGroup(group, location + name + "-" + profile + "." + ext,
                                profile);
                    }
                    // Also try the profile-specific section (if any) of the normal file
                    loadIntoGroup(group, location + name + "." + ext, profile);
                }
            }
        }

在Load方法中,首先会通过getAllFileExtensions方法去组装所有可加载文件的扩展名,然后在通过loadIntoGroup方法加载配置文件,而我们跟读到loadIntoGroup中会发现其只执行了 doLoadIntoGroup方法, doLoadIntoGroup源码如下:

private PropertySource<?> doLoadIntoGroup(String identifier, String location,
                Profile profile) throws IOException {
            Resource resource = this.resourceLoader.getResource(location);
            PropertySource<?> propertySource = null;
            StringBuilder msg = new StringBuilder();
            if (resource != null && resource.exists()) {
                String name = "applicationConfig: [" + location + "]";
                String group = "applicationConfig: [" + identifier + "]";
                propertySource = this.propertiesLoader.load(resource, group, name,
                        (profile == null ? null : profile.getName()));
                if (propertySource != null) {
                    msg.append("Loaded ");
                    handleProfileProperties(propertySource);
                }
                else {
                    msg.append("Skipped (empty) ");
                }
            }
            else {
                msg.append("Skipped ");
            }
            msg.append("config file ");
            msg.append(getResourceDescription(location, resource));
            if (profile != null) {
                msg.append(" for profile ").append(profile);
            }
            if (resource == null || !resource.exists()) {
                msg.append(" resource not found");
                this.logger.trace(msg);
            }
            else {
                this.logger.debug(msg);
            }
            return propertySource;
        }

上诉源码中有一段核心的方法

propertySource = this.propertiesLoader.load(resource, group, name,
                        (profile == null ? null : profile.getName()));
                if (propertySource != null) {
                    msg.append("Loaded ");
                    handleProfileProperties(propertySource);
                }

如果propertySource存在,则调用handleProfileProperties方法。

handleProfileProperties源码:

private void handleProfileProperties(PropertySource<?> propertySource) {
            SpringProfiles springProfiles = bindSpringProfiles(propertySource);
            maybeActivateProfiles(springProfiles.getActiveProfiles());
            addProfiles(springProfiles.getIncludeProfiles());
        }

bindSpringProfiles源码:

private SpringProfiles bindSpringProfiles(PropertySource<?> propertySource) {
            MutablePropertySources propertySources = new MutablePropertySources();
            propertySources.addFirst(propertySource);
            return bindSpringProfiles(propertySources);
        }

bindSpringProfiles源码:

private SpringProfiles bindSpringProfiles(PropertySources propertySources) {
            SpringProfiles springProfiles = new SpringProfiles();
            RelaxedDataBinder dataBinder = new RelaxedDataBinder(springProfiles,
                    "spring.profiles");
            dataBinder.bind(new PropertySourcesPropertyValues(propertySources, false));
            springProfiles.setActive(resolvePlaceholders(springProfiles.getActive()));
            springProfiles.setInclude(resolvePlaceholders(springProfiles.getInclude()));
            return springProfiles;
        }

通过这样一步一步的跟读,我们能发现配置文件的装载主要是通过new PropertySourcesPropertyValues来完成。

查看PropertySourcesPropertyValues的构造方法可以发现,它在装载值时调用了这样一个方法:

private void processPropertySource(PropertySource<?> source,
            PropertySourcesPropertyResolver resolver) {
        if (source instanceof CompositePropertySource) {
            //加载复杂的属性
            processCompositePropertySource((CompositePropertySource) source, resolver);
        }
        else if (source instanceof EnumerablePropertySource) {
            //加载枚举类型
            processEnumerablePropertySource((EnumerablePropertySource<?>) source,
                    resolver, this.includes);
        }
        else {
            //加载非枚举类型,即简单类型
            processNonEnumerablePropertySource(source, resolver);
        }
    }

配置文件键值对加载方法

此处以yml文件加载为例

配置文件键值对加载方法,org.springframework.beans.factory.config.YamlProcessor#process(MatchCallback callback, Yaml yaml, Resource resource)

在该方法中通过yaml.loadAll(reader)去加载文件的属性,然后在下方process(asMap(object), callback)通过callback进行键值对组装。

关键代码如下:

try {
    for (Object object : yaml.loadAll(reader)) {
        if (object != null && process(asMap(object), callback)) {
            count++;
            if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
                break;
            }
        }
    }
    if (logger.isDebugEnabled()) {
        logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
                     " from YAML resource: " + resource);
    }
}
finally {
    reader.close();
}

该代码中的callback为org.springframework.boot.env.YamlPropertySourceLoader#process()中new的一个,源码如下:

public Map<String, Object> process() {
            final Map<String, Object> result = new LinkedHashMap<String, Object>();
            process(new MatchCallback() {
                @Override
                public void process(Properties properties, Map<String, Object> map) {
                    result.putAll(getFlattenedMap(map));
                }
            });
            return result;
        }

总结

配置文件的加载主要分为两个步骤:

  1. 查找配置文件
  2. 加载配置文件中的值

其中查找配置文件加载顺序为:

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/
  • spring.config.location

spring.config.location为在启动SpringBoot时,为其指定的配置文件路径,

设置方式为:

java -jar demo.jar --Dspring.config.location=application.yml

评论