源码层面分析Springboot 配置文件加载顺序


前言
新旧版本对比
springboot配置文件加载
postProcessEnvironment
总结


前言

    

    在使用Springboot项目时,我们或多或少会使用到spring.profiles.active,spring.config.location等配置。那你知道两者的区别吗,如果项目中配置文件很多时,如下面这种情况,配置文件的加载顺序又是怎样?


源码层面分析Springboot 配置文件加载顺序


先说结论:

1.如果我们没有使用spring.config.location


优先级从高到低会是:inside2/>inside1/>项目根目录/>classpath:/config/>classpath:/;所有位置的文件都会被加载,高优先级配置内容会覆盖低优先级配置的内容,并形成互补配置

2.如果使用了spring.config.location

    则不会再加载其他application.properties或application.yaml,这就要求我们用spring.config.location指定的配置必须包含项目需要的所有配置

1.新旧版本对比

1.1 Springboot<2.4    ConfigFileApplicationListener

    我们打开ConfigFileApplicationListener类


源码层面分析Springboot 配置文件加载顺序

源码层面分析Springboot 配置文件加载顺序


上面有说默认加载的路径,然后2.4版本后被ConfigDataEnvironmentPostProcessor替代。


1.2 Springboot>2.4    ConfigDataEnvironmentPostProcessor

源码层面分析Springboot 配置文件加载顺序


描述说ConfigDataEnvironment会包含我们想要的property,然后看ConfigDataEnvironment


源码层面分析Springboot 配置文件加载顺序


这里的顺序倒过来则是最终配置文件的优先级顺序。

接下里我们从源码角度分析springboot是如何加载配置文件的。

2.Springboot 配置文件加载


本篇基于springboot 2.5.14版本分析。


最近看Spring源码发现,Spring最近几个版本源码改动挺多的,大多基于jdk的新特性对以前的代码进行了重构。如函数式编程等。如果对jdk新特性不是很熟,读起来还有点生硬。
        我们继续从Springboot启动源码入手。
#SpringApplication.class
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    //❶    
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    //
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    //
    listeners.environmentPrepared(bootstrapContext, environment);
    DefaultPropertiesPropertySource.moveToEnd(environment);
    Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
            "Environment prefix cannot be set via properties.");
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = convertEnvironment(environment);
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}


Springboot启动后,首先是准备Springboot启动所需要的环境配置。

2.1 getOrCreateEnvironment

创建并且配置环境

#SpringApplication.class
private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    }
    switch (this.webApplicationType) {
    case SERVLET:
        return new ApplicationServletEnvironment();
    case REACTIVE:
        return new ApplicationReactiveWebEnvironment();
    default:
        return new ApplicationEnvironment();
    }
}


我们使用springboot来构建项目一般都是web环境,目前主流还是用的servlet,所以会创建ApplicationServletEnvironment


#SpringApplication.class
private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;
    }
    switch (this.webApplicationType) {
    case SERVLET:
        return new ApplicationServletEnvironment();
    case REACTIVE:
        return new ApplicationReactiveWebEnvironment();
    default:
        return new ApplicationEnvironment();
    }
}


先看下它的依赖关系



源码层面分析Springboot 配置文件加载顺序


然后我们再follow无参构造,依次往父类跟踪,最终我们在AbstractEnvironment中发现了无参构造,隐藏的还算比较深。


#AbstractEnvironment

  public AbstractEnvironment() {
        this(new MutablePropertySources());
    }
    
  protected AbstractEnvironment(MutablePropertySources propertySources) {
    this.propertySources = propertySources;
    this.propertyResolver = createPropertyResolver(propertySources);
    customizePropertySources(propertySources);
}

主要是创建了MutablePropertySources对象,然后调用可以重写的方法customizePropertySources来个性化配置PropertySources,


先看MutablePropertySources,内部包含一个可变的集合,用来存储我们配置文件加载的优先级。


源码层面分析Springboot 配置文件加载顺序

后面解析出来的配置都会通过addFirst

源码层面分析Springboot 配置文件加载顺序


或者addLast


源码层面分析Springboot 配置文件加载顺序


来调整配置的优先级


接着我们看customizePropertySources,我们发现

ApplicationServletEnvironment的父类StandardServletEnvironment实现了该方法,如果你看过springboot以前的源码会发现

ApplicationServletEnvironment是后面新增的一个类。


#StandardServletEnvironment
@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 (jndiPresent && JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
        propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
    }
    super.customizePropertySources(propertySources);
}

可以看到它主要是添加servletConfigInitParams和servletContextInitParams的PropertySources配置

完事后它调了父类的customizePropertySources方法,我们跟踪代码会发现父类StandardEnvironment中也实现了该方法。

#StandardEnvironment
@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()));
}

可以看到它主要添加了systemProperties和systemEnvironmen系统配置,并且此时同时读取了系统的配置属性getSystemProperties(),getSystemEnvironment()

从上面也可以看出spring的抽象做的很好,职责也很单一,不同的类加载不同的配置。

2.2 configureEnvironment

顾名思义,这个也是继续配置环境

#SpringApplication
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    if (this.addConversionService) {
        environment.setConversionService(new ApplicationConversionService());
    }
    configurePropertySources(environment, args);
    configureProfiles(environment, args);
}     

继续看configurePropertySources方法

#SpringApplication
protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
        MutablePropertySources sources = environment.getPropertySources();
        if (!CollectionUtils.isEmpty(this.defaultProperties)) {
            DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
        }
        if (this.addCommandLineProperties && args.length > 0) {
            String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
            if (sources.contains(name)) {
                PropertySource<?> source = sources.get(name);
                CompositePropertySource composite = new CompositePropertySource(name);
                composite.addPropertySource(
                        new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
                composite.addPropertySource(source);
                sources.replace(name, composite);
            }
            else {
                sources.addFirst(new SimpleCommandLinePropertySource(args));
            }
        }
    }

如果我们有在启动命令里面添加启动参数,那么就会使用addFirst创建SimpleCommandLinePropertySource,优先级最高。

经过前面两步的初始化配置,目前我们的environment主要有如下几个配置(因为没有在命令行添加启动参数,所以没有SimpleCommandLinePropertySource)


源码层面分析Springboot 配置文件加载顺序

2.3 listeners.environmentPrepared

前面已经初始化了一些基础配置后,这步开始发布环境准备好的事件。这里面也用了一系列的抽象以及函数式编程,我们直接到关键类

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

发布ApplicationEnvironmentPreparedEvent事件,然后调用SimpleApplicationEventMulticasterdoInvokeListener方法,

#SimpleApplicationEventMulticaster

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
        try {
            listener.onApplicationEvent(event);
        }
        catch (ClassCastException ex) {
            xxx
        }
    }

这里其实就是spring event的源码了。(下次单独分析这块,很多人对event肯定存在误解,event其实不一定是异步,主要是解耦。)

最后调用EnvironmentPostProcessorApplicationListener的方法onApplicationEvent


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

继续跟踪onApplicationEnvironmentPreparedEvent

#EnvironmentPostProcessorApplicationListener
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    ConfigurableEnvironment environment = event.getEnvironment();
    SpringApplication application = event.getSpringApplication();
    for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(),
            event.getBootstrapContext())) {
        postProcessor.postProcessEnvironment(environment, application);
    }
}


然后通过processor开始正式的加工环境变量了。


以上步骤总结:

源码层面分析Springboot 配置文件加载顺序


3.postProcessEnvironment


#ConfigDataEnvironmentPostProcessor
void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
        Collection<String> additionalProfiles) {
    try {
        this.logger.trace("Post-processing environment to add config data");
        resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
        getConfigDataEnvironment(environment, resourceLoader, additionalProfiles).processAndApply();
    }
    catch (UseLegacyConfigProcessingException ex) {
        xxx
    }
}

3.1 getConfigDataEnvironment

#ConfigDataEnvironmentPostProcessor
ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader,
        Collection<String> additionalProfiles) {
    return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader,
            additionalProfiles, this.environmentUpdateListener);
}

这里创建了ConfigDataEnvironment,我们在开头有介绍,这里面有包含我们默认的配置路径。


#ConfigDataEnvironment
ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
            ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles,
            ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
        //获取绑定对象    
        Binder binder = Binder.get(environment);
        UseLegacyConfigProcessingException.throwIfRequested(binder);
        this.logFactory = logFactory;
        this.logger = logFactory.getLog(getClass());
        this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
                .orElse(ConfigDataNotFoundAction.FAIL);
        this.bootstrapContext = bootstrapContext;
        this.environment = environment;
        //初始化resolver
        this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
        this.additionalProfiles = additionalProfiles;
        this.environmentUpdateListener = (environmentUpdateListener != null) ? environmentUpdateListener
                : ConfigDataEnvironmentUpdateListener.NONE;
        //初始化loaders        
        this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, resourceLoader.getClassLoader());
        //创建contributors
        this.contributors = createContributors(binder);
    }


❶Binder.get(environment),配置绑定类Binder的功能非常强大。是Springboot2.0提供的数据绑定新特性,比Environment类好用很多,可以非常方便地进行类型转换,可以将属性绑定到对象,Map,List等类型上。虽然我们可以从environment中拿到很多配置,但有些时候比如我们通过map注入的属性,key值是无法确定的,这个时候就可以用Binder来获取map对象,然后再做其他判断操作了。(后面再单独讲解Binder如何使用)。


❷createConfigDataLocationResolvers,初始化resolvers

protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
        ConfigurableBootstrapContext bootstrapContext, Binder binder, ResourceLoader resourceLoader) {
    return new ConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
}

    ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
        Binder binder, ResourceLoader resourceLoader) {
    this(logFactory, bootstrapContext, binder, resourceLoader, SpringFactoriesLoader
            .loadFactoryNames
(ConfigDataLocationResolver.class, resourceLoader.getClassLoader()));
}

看到SpringFactoriesLoader.loadFactoryNames我们就知道它是从WEB-INF目录下的spring.facotries文件中寻找对应的配置了。如果我们自定义resolvers,直接在spring.factories文件中配置即可,这也是另外一种扩展方式。最终ConfigTreeConfigDataLocationResolver和StandardConfigDataLocationResolver会被扫描注入IOC。


❸new ConfigDataLoaders,初始化loaders,和初始化resolver一样,最终会创建ConfigTreeConfigDataLoader和StandardConfigDataLoader


❹createContributors,创建contributors


#ConfigDataEnvironment
private ConfigDataEnvironmentContributors createContributors(Binder binder) {
        this.logger.trace("Building config data environment contributors");
        //取出propertySources准备继续装配
        MutablePropertySources propertySources = this.environment.getPropertySources();
        List<ConfigDataEnvironmentContributor> contributors = new ArrayList<>(propertySources.size() + 10);
        PropertySource<?> defaultPropertySource = null;
        for (PropertySource<?> propertySource : propertySources) {
            if (DefaultPropertiesPropertySource.hasMatchingName(propertySource)) {
                defaultPropertySource = propertySource;
            }
            else {
                this.logger.trace(LogMessage.format("Creating wrapped config data contributor for '%s'",
                        propertySource.getName()));
                //填充contributors    
                contributors.add(ConfigDataEnvironmentContributor.ofExisting(propertySource));
            }
        }
        
        //获取初始化importer配置
        contributors.addAll(getInitialImportContributors(binder));
        if (defaultPropertySource != null) {
            this.logger.trace("Creating wrapped config data contributor for default property source");
            contributors.add(ConfigDataEnvironmentContributor.ofExisting(defaultPropertySource));
        }
        return createContributors(contributors);
    }

❶this.environment.getPropertySources(),在最开始prepareEnvrionment那步已经预先填充了一些propertySources,这里取出来继续填充。

❷ConfigDataEnvironmentContributor,主要是通过propertySources来构建ConfigDataEnvironmentContributor对象,这个对象很重要,在后面配置的解析中主要靠它来做判断的。

❸getInitialImportContributors(binder)获取初始化import的配置

#ConfigDataEnvironment

private List<ConfigDataEnvironmentContributor> getInitialImportContributors(Binder binder) {
    List<ConfigDataEnvironmentContributor> initialContributors = new ArrayList<>();
    addInitialImportContributors(initialContributors, bindLocations(binder, IMPORT_PROPERTY, EMPTY_LOCATIONS));
    addInitialImportContributors(initialContributors,
            bindLocations(binder, ADDITIONAL_LOCATION_PROPERTY, EMPTY_LOCATIONS));
    addInitialImportContributors(initialContributors,
            bindLocations(binder, LOCATION_PROPERTY, DEFAULT_SEARCH_LOCATIONS));
    return initialContributors;
}
private ConfigDataLocation[] bindLocations(Binder binder, String propertyName, ConfigDataLocation[] other) {
    return binder.bind(propertyName, CONFIG_DATA_LOCATION_ARRAY).orElse(other);
}
这里就用到了binder了,分别绑定了spring.config.import,spring.config.additional-location和spring.config.location。前面两个,如果没有对应的配置,则为空;spring.config.location如果没有,则是DEFAULT_SEARCH_LOCATIONS,对应的具体路径如下:

源码层面分析Springboot 配置文件加载顺序


这就回到了我们开篇那里,Springboot默认扫描的配置路径。这里要注意,如果我们配置了spring.config.location,那么就不会再扫描默认配置了。原理就在这里。

到这里并没有结束,只是将我们的默认路径放入了ConfigDataEnvironmentContributor中,只有最终放入到propertySources中才能最终生效。


同样来个总结。

源码层面分析Springboot 配置文件加载顺序


3.2 processAndApply

收集并加载配置文件


#ConfigDataEnvironment

/**
 * Process all contributions and apply any newly imported property sources to the
 * {@link Environment}.
 */
void processAndApply() {
    ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
            this.loaders);
    registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);
    //初始化process
    ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);
    ConfigDataActivationContext activationContext = createActivationContext(
            contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));
    //激活cloudPlatform        
    contributors =processWithoutProfiles(contributors, importer, activationContext);
    //添加additionalProfiles和include_profile
    activationContext = withProfiles(contributors, activationContext);
    //最终处理所有profile,更新Kind类型
    contributors =processWithProfiles(contributors, importer, activationContext);
// 应用到环境中
   
applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),
            importer.getOptionalLocations());
}

3.2.1 processInitial

#ConfigDataEnvironment

private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors,
        ConfigDataImporter importer) {
    this.logger.trace("Processing initial config data environment contributors without activation context");
    contributors = contributors.withProcessedImports(importer, null);
    registerBootstrapBinder(contributors, null, DENY_INACTIVE_BINDING);
    return contributors;
}


这步最重要的方法就是contributors.withProcessedImports(importer, null);后面几步都要调这个方法,springboot将我们的配置的绑定状态类型Kind分成以下几类:


/**
 * The various kinds of contributor.
 */
enum Kind {

    /**
     * A root contributor used contain the initial set of children.
     */
    ROOT,

    /**
     * An initial import that needs to be processed.
     */
    INITIAL_IMPORT,

    /**
     * An existing property source that contributes properties but no imports.
     */
    EXISTING,

    /**
     * A contributor with {@link ConfigData} imported from another contributor but not
     * yet bound.
     */
    UNBOUND_IMPORT,

    /**
     * A contributor with {@link ConfigData} imported from another contributor that
     * has been.
     */
    BOUND_IMPORT,

    /**
     * A valid location that contained nothing to load.
     */
    EMPTY_LOCATION;

}

正常情况下一个存在的配置的状态迁移过程就是INITIAL_IMPORT –>UNBOUND_IMPORT –> BOUND_IMPORT

3.2.2 processWithoutProfiles

#ConfigDataEnvironment

private ConfigDataEnvironmentContributors processWithoutProfiles(ConfigDataEnvironmentContributors contributors,
        ConfigDataImporter importer, ConfigDataActivationContext activationContext) {
    this.logger.trace("Processing config data environment contributors with initial activation context");
    contributors = contributors.withProcessedImports(importer, activationContext);
    registerBootstrapBinder(contributors, activationContext, DENY_INACTIVE_BINDING);
    return contributors;
}
这步也调用到contributors.withProcessedImports(importer, activationContext),不过多了一个activationContext参数。
#ConfigDataActivationContext
ConfigDataActivationContext(Environment environment, Binder binder) {
    this.cloudPlatform = deduceCloudPlatform(environment, binder);
    this.profiles = null;
}

如果涉及到云平台,这里就会处理云平台相关配置。

3.2.3 withProfiles

#ConfigDataEnvironment
private ConfigDataActivationContext withProfiles(ConfigDataEnvironmentContributors contributors,
            ConfigDataActivationContext activationContext) {
    Binder binder = contributors.getBinder(activationContext,
            (contributor) -> !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES),
            BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
    try {
        Set<String> additionalProfiles = new LinkedHashSet<>(this.additionalProfiles);
        additionalProfiles.addAll(getIncludedProfiles(contributors, activationContext));
        Profiles profiles = new Profiles(this.environment, binder, additionalProfiles);
        return activationContext.withProfiles(profiles);
    }
    
}

这步主要是添加additionalProfiles和include_profile配置

3.2.4 processWithProfiles

最后一步处理contributors配置的状态

#ConfigDataEnvironment
private ConfigDataEnvironmentContributors processWithProfiles(ConfigDataEnvironmentContributors contributors,
        ConfigDataImporter importer, ConfigDataActivationContext activationContext) {
    this.logger.trace("Processing config data environment contributors with profile activation context");
    contributors = contributors.withProcessedImports(importer, activationContext);
    registerBootstrapBinder(contributors, activationContext, ALLOW_INACTIVE_BINDING);
    return contributors;
}
最后也是调用contributors.withProcessedImports(importer, activationContext)。接下来分析该方法

3.2.5 withProcessedImports

#ConfigDataEnvironmentContributors
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
            ConfigDataActivationContext activationContext) {
        ImportPhase importPhase = ImportPhase.get(activationContext);
        this.logger.trace(LogMessage.format("Processing imports for phase %s. %s", importPhase,
                (activationContext != null) ? activationContext : "no activation context"));
        ConfigDataEnvironmentContributors result = this;
        int processed = 0;
        while (true) {
            //获取nextToProcess
            ConfigDataEnvironmentContributor contributor = getNextToProcess(result, activationContext, importPhase);
            if (contributor == null) {
                this.logger.trace(LogMessage.format("Processed imports for of %d contributors", processed));
                return result;
            }
            if (contributor.getKind() == Kind.UNBOUND_IMPORT) {
                //如果是UNBOUND_IMPORT,绑定配置
                ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(result, activationContext);
                result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                        result.getRoot().withReplacement(contributor, bound));
                continue;
            }
            ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
                    result, contributor, activationContext);
            ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
            //获取ConfigDataLocation
            List<ConfigDataLocation> imports = contributor.getImports();
            this.logger.trace(LogMessage.format("Processing imports %s", imports));
            //解析configDataLocation并加载为configData
            Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,
                    locationResolverContext, loaderContext, imports);
            this.logger.trace(LogMessage.of(() -> getImportedMessage(imported.keySet())));
            ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
                    asContributors(imported));
            result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
                    result.getRoot().withReplacement(contributor, contributorAndChildren));
            processed++;
        }
    }
前面几步就是将配置的状态迁移INITIAL_IMPORT –>UNBOUND_IMPORT –> BOUND_IMPORT

❶contributor.getImports() 获取ConfigDataLocation,即默认配置等的路径。

❷importer.resolveAndLoad,解析configDataLocation并加载为configData,继续跟踪代码


#ConfigDataImporter
Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,
            ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,
            List<ConfigDataLocation> locations) {
    Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
    //解析配置
    List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
    //加载配置
    return load(loaderContext, resolved);
    
}
先看❶resolve(locationResolverContext, profiles, locations);解析配置,resolver是StandardConfigDataLocationResolver,前面有介绍resolvers和loaders
#StandardConfigDataLocationResolver
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
        ConfigDataLocation location) throws ConfigDataNotFoundException {
    return resolve(getReferences(context, location.split()));
}

其中getReferences就是获取配置文件可能存在的所有路径


如classpath路径下

源码层面分析Springboot 配置文件加载顺序

项目同级目录下

源码层面分析Springboot 配置文件加载顺序

接下来调用reslove方法来获取有效的地址

#StandardConfigDataLocationResolver
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
    if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
        return resolveNonPattern(reference);
    }
    return resolvePattern(reference);
}

接着

private List<StandardConfigDataResource> resolvePattern(StandardConfigDataReference reference) {
    List<StandardConfigDataResource> resolved = new ArrayList<>();
    for (Resource resource : this.resourceLoader.getResources(reference.getResourceLocation(), ResourceType.FILE)) {
        if (!resource.exists() && reference.isSkippable()) {
            logSkippingResource(reference);
        }
        else {
            resolved.add(createConfigResourceLocation(reference, resource));
        }
    }
    return resolved;
}

通过resourceLoader去加载指定路径的配置,这样就可以判断是否匹配了。


接下来再回到❷load(loaderContext, resolved),加载指定配置


#StandardConfigDataLoader
public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResource resource)
        throws IOException, ConfigDataNotFoundException {
    if (resource.isEmptyDirectory()) {
        return ConfigData.EMPTY;
    }
    ConfigDataResourceNotFoundException.throwIfDoesNotExist(resource, resource.getResource());
    StandardConfigDataReference reference = resource.getReference();
    Resource originTrackedResource = OriginTrackedResource.of(resource.getResource(),
            Origin.from(reference.getConfigDataLocation()));
    String name = String.format("Config resource '%s' via location '%s'", resource,
            reference.getConfigDataLocation());
    List<PropertySource<?>> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource);
    PropertySourceOptions options = (resource.getProfile() != null) ? PROFILE_SPECIFIC : NON_PROFILE_SPECIFIC;
    return new ConfigData(propertySources, options);
}
其中reference.getPropertySourceLoader()获取的loader可能是PropertiesPropertySourceLoader或者YamlPropertySourceLoader

前置主要是针对xml,properties文件解析,后者主要是yaml文件解析。

最终返回的是PropertySource


源码层面分析Springboot 配置文件加载顺序


总结一下


源码层面分析Springboot 配置文件加载顺序

3.2.6 applyToEnvironment

最后一步就是将前面加载到的配置应用到环境中去。


#ConfigDataEnvironment
private void applyToEnvironment(ConfigDataEnvironmentContributors contributors,
            ConfigDataActivationContext activationContext, Set<ConfigDataLocation> loadedLocations,
            Set<ConfigDataLocation> optionalLocations) {
    checkForInvalidProperties(contributors);
    checkMandatoryLocations(contributors, activationContext, loadedLocations, optionalLocations);
    MutablePropertySources propertySources = this.environment.getPropertySources();
    //将contributor中的propertySource添加到environment中的propertySource
    applyContributor(contributors, activationContext, propertySources);
    DefaultPropertiesPropertySource.moveToEnd(propertySources);
    Profiles profiles = activationContext.getProfiles();
    this.logger.trace(LogMessage.format("Setting default profiles: %s", profiles.getDefault()));
    //设置DefaultProfiles
    this.environment.setDefaultProfiles(StringUtils.toStringArray(profiles.getDefault()));
    this.logger.trace(LogMessage.format("Setting active profiles: %s", profiles.getActive()));
    //设置ActiveProfiles
    this.environment.setActiveProfiles(StringUtils.toStringArray(profiles.getActive()));
    this.environmentUpdateListener.onSetProfiles(profiles);
}


❶applyContributor,将contributor中的propertySource添加到environment中的propertySource


#ConfigDataEnvironment
private void applyContributor(ConfigDataEnvironmentContributors contributors,
            ConfigDataActivationContext activationContext, MutablePropertySources propertySources) {
    this.logger.trace("Applying config data environment contributions");
    for (ConfigDataEnvironmentContributor contributor : contributors) {
        PropertySource<?> propertySource = contributor.getPropertySource();
        if (contributor.getKind() == ConfigDataEnvironmentContributor.Kind.BOUND_IMPORT && propertySource != null) {
            if (!contributor.isActive(activationContext)) {
                this.logger.trace(
                        LogMessage.format("Skipping inactive property source '%s'", propertySource.getName()));
            }
            else {
                this.logger
                        .trace(LogMessage.format("Adding imported property source '%s'", propertySource.getName()));
                propertySources.addLast(propertySource);
                this.environmentUpdateListener.onPropertySourceAdded(propertySource, contributor.getLocation(),
                        contributor.getResource());
            }
        }
    }
}

此时的排序如下。

源码层面分析Springboot 配置文件加载顺序

可以看到项目同级目录下config下的有优级要高于其他配置。


如果我们加上了-Dspring.config.location=config/,指定加载项目同级目录config目录下


源码层面分析Springboot 配置文件加载顺序

此时之后加载指定路径下的配置。前面也已经解释二者的区别。


❷setDefaultProfiles, 设置DefaultProfiles

❸setActiveProfiles,设置ActiveProfiles,可以看到它接收的是一个集合,说明我们可以同时设置多个。


4.总结


属性文件优先级
1.SimpleCommandLinePropertySource 命令行参数
2.java的系统属性,可以通过System.getProperties()获得的内容
3.systemEvironment 操作系统的环境变量
4.application-{profile}.properties (jar包外优先于jar包内)
5.applicaiton.properties (jar包外优先于jar包内)
6.在@Configuration注解修改的类中,通过@PropertySource注解定义的属性。

所有位置的文件都会被加载,高优先级配置内容会覆盖低优先级配置的内容,并形成互补配置。

如果使用了spring.config.location配置,则不会再加载其他application.properties或application.yaml。

同级下,application.properties文件优先级大于application.yml,前者会覆盖后者。
springboot的默认加载路径有5个,分别是classpath:/、classpath:/config/、项目根路径、项目根路径/config/*/、项目根路径/config/,会从这5个路径下加载application.properties或application.yml

5个默认的加载路径的优先级为:项目根路径下的config/*/ > 项目根路径下的config > 项目根路径 > classpath:/config > classpath:/,前者会覆盖后者。(项目根路径可以理解成jar外。)

源码层面分析Springboot 配置文件加载顺序





源码层面分析Springboot 配置文件加载顺序
关注我的你,是最香哒!


原文始发于微信公众号(小李的源码图):源码层面分析Springboot 配置文件加载顺序

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145354.html

(1)
青莲明月的头像青莲明月

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!