当前位置: 首页 > ops >正文

Spring Boot 2.4+中bootstrap.yml加载顺序的源码深度解析

引言:配置加载的变革

在Spring Boot 2.4版本中,配置加载机制经历了重大变革,引入了全新的ConfigData API。这一变化不仅影响了application.yml/properties的加载方式,也改变了bootstrap.yml(Spring Cloud上下文引导文件)的加载机制。本文将深入源码层面,解析bootstrap.yml在Spring Boot 2.4+中的加载顺序和实现原理。

一、bootstrap.yml的特殊地位

1.1 什么是bootstrap.yml?

bootstrap.yml是Spring Cloud应用中的特殊配置文件:

  • 应用上下文创建之前加载

  • 用于加载连接到配置中心所需的配置

  • 通常包含应用名称、配置中心地址等元数据

1.2 启用要求

在Spring Boot 2.4+中,要启用bootstrap上下文,需要添加依赖:

xml

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

二、核心源码解析:加载顺序的基石

2.1 默认搜索位置定义

加载顺序的核心定义位于ConfigDataEnvironment类中:

java

// 源码位置:org.springframework.boot.context.config.ConfigDataEnvironment
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static {List<ConfigDataLocation> locations = new ArrayList<>();// 类路径位置(优先级较低)locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));// 文件系统位置(优先级较高)locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/"));DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
}

这个静态初始化块定义了两个位置组:

  1. 类路径位置组classpath:/classpath:/config/

  2. 文件系统位置组file:./(当前目录)、file:./config/file:./config/*/

2.2 优先级规则解析

从源码可以看出:

  • 文件系统位置组整体优先级高于类路径位置组

  • 文件系统组内优先级顺序:

    1. file:./config/*/(最高)

    2. file:./config/

    3. file:./

  • 类路径组内优先级顺序:

    1. classpath:/config/

    2. classpath:/(最低)

因此,完整的默认位置优先级从高到低为:

text

1. file:./config/*/
2. file:./config/
3. file:./
4. classpath:/config/
5. classpath:/

三、bootstrap.yml加载流程详解

3.1 启动入口:processAndApply()

加载流程从ConfigDataEnvironment.processAndApply()方法开始:

java

void processAndApply() {ConfigDataImporter importer = new ConfigDataImporter(...);// 初始化贡献者链ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);// 处理无profile配置contributors = processWithoutProfiles(contributors, importer, ...);// 处理带profile配置contributors = processWithProfiles(contributors, importer, ...);// 应用到环境applyToEnvironment(contributors, ...);
}

3.2 核心处理逻辑:withProcessedImports()

实际处理导入配置的核心方法:

java

ConfigDataEnvironmentContributors withProcessedImports(...) {while (true) {// 获取下一个待处理的贡献者ConfigDataEnvironmentContributor contributor = getNextToProcess(...);// 解析并加载配置Map<ConfigDataResolutionResult, ConfigData> imported = importer.resolveAndLoad(activationContext,locationResolverContext,loaderContext,imports // 包含bootstrap.yml位置);// 将新配置插入贡献者链result = new ConfigDataEnvironmentContributors(...,result.getRoot().withReplacement(contributor, contributorAndChildren));}
}

3.3 文件加载实现:resolveAndLoad()

配置文件的解析和加载过程:

java

Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(...) {// 解析位置为具体资源List<ConfigDataResolutionResult> resolved = resolve(...);// 实际加载文件return load(loaderContext, resolved);
}private Map<ConfigDataResolutionResult, ConfigData> load(...) {// 逆序加载:确保高优先级配置后加载for (int i = candidates.size() - 1; i >= 0; i--) {ConfigDataResolutionResult candidate = candidates.get(i);// 委托给StandardConfigDataLoader加载ConfigData loaded = this.loaders.load(loaderContext, resource);result.put(candidate, loaded);}return result;
}

3.4 YAML文件解析:YamlPropertySourceLoader

最终加载和解析YAML文件的实现:

java

public List<PropertySource<?>> load(String name, Resource resource) throws IOException {// 使用SnakeYAML解析器List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();return Collections.singletonList(new OriginTrackedMapPropertySource(name, Collections.unmodifiableMap(loaded.get(0))));
}

四、关键设计:覆盖机制的实现

4.1 逆序加载的魔力

源码中最重要的覆盖机制实现:

java

for (int i = candidates.size() - 1; i >= 0; i--) {ConfigData loaded = this.loaders.load(loaderContext, resource);
}

这个逆序循环意味着:

  1. 低优先级位置先加载

  2. 高优先级位置后加载

  3. 后加载的配置覆盖先加载的配置

4.2 位置解析优先级

位置解析时考虑了profile特定文件:

java

private Set<StandardConfigDataReference> getReferencesForDirectory(...) {for (String name : this.configNames) { // configNames包含"bootstrap"// 查找bootstrap-{profile}.ymlreferences.addAll(getReferencesForConfigName(name, ...));}
}

加载顺序为:

  1. bootstrap-{profile}.yml

  2. bootstrap.yml

五、完整加载顺序总结

5.1 优先级金字塔

基于源码分析,bootstrap.yml的完整加载顺序(从高到低):

优先级位置说明
1spring.config.import手动导入的最高优先级位置
2spring.config.additional-location额外添加的位置
3file:./config/*/当前目录config下的任意子目录
4file:./config/当前目录下的config目录
5file:./当前目录(JAR文件所在目录)
6classpath:/config/类路径下的config目录
7classpath:/类路径根目录(最低优先级)

5.2 Profile处理机制

当激活特定profile(如dev)时:

  1. 优先加载bootstrap-dev.yml

  2. 然后加载bootstrap.yml

  3. bootstrap-dev.yml中的配置会覆盖bootstrap.yml

六、实战验证:查看加载顺序

在应用启动时添加--debug参数,可以在日志中观察加载顺序:

log

TRACE o.s.b.c.c.ConfigDataEnvironmentContributors - Processing imports [optional:file:./config/bootstrap.yml]
DEBUG o.s.b.c.c.StandardConfigDataReferenceResolver - Creating config data reference for path 'file:./bootstrap.yml'
TRACE o.s.b.c.c.ConfigDataImporter - Loaded config file 'file:./bootstrap.yml'

七、最佳实践建议

  1. 生产环境配置:将bootstrap.yml放在JAR同级的config/目录下

  2. 敏感信息管理:外部配置文件不要提交到代码仓库

  3. 配置覆盖:需要覆盖默认配置时,使用高优先级位置

  4. Profile管理:合理使用bootstrap-{profile}.yml管理环境差异

  5. 调试技巧:使用--spring.config.import参数临时覆盖配置

结语:理解本质,灵活应用

通过对Spring Boot 2.4+源码的深度解析,我们揭示了bootstrap.yml加载顺序的内在机制:

  • 优先级控制:通过DEFAULT_SEARCH_LOCATIONS定义位置顺序

  • 覆盖机制:逆序加载实现高优先级配置覆盖

  • 扩展能力:基于ConfigData API的设计支持灵活扩展

理解这些底层原理,不仅能帮助我们更好地管理Spring Boot应用配置,也能在遇到配置问题时快速定位原因。Spring Boot通过精妙的设计,在保持灵活性的同时提供了强大的配置管理能力,这正是它成为Java生态首选框架的重要原因之一。

##源码

static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;static {List<ConfigDataLocation> locations = new ArrayList<>();locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/"));DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);}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 ConfigDataEnvironmentContributors createContributors(Binder binder) {this.logger.trace("Building config data environment contributors");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.add(ConfigDataEnvironmentContributor.ofExisting(propertySource));}}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);}protected ConfigDataEnvironmentContributors createContributors(List<ConfigDataEnvironmentContributor> contributors) {return new ConfigDataEnvironmentContributors(this.logFactory, this.bootstrapContext, contributors);}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;this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);this.additionalProfiles = additionalProfiles;this.environmentUpdateListener = (environmentUpdateListener != null) ? environmentUpdateListener: ConfigDataEnvironmentUpdateListener.NONE;this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, resourceLoader.getClassLoader());this.contributors = createContributors(binder);}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) {this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]",ex.getConfigurationProperty()));configureAdditionalProfiles(environment, additionalProfiles);postProcessUsingLegacyApplicationListener(environment, resourceLoader);}}void processAndApply() {ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,this.loaders);registerBootstrapBinder(this.contributors, null, DENY_INACTIVE_BINDING);ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);ConfigDataActivationContext activationContext = createActivationContext(contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE));contributors = processWithoutProfiles(contributors, importer, activationContext);activationContext = withProfiles(contributors, activationContext);contributors = processWithProfiles(contributors, importer, activationContext);applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(),importer.getOptionalLocations());}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;}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) {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) {Iterable<ConfigurationPropertySource> sources = Collections.singleton(contributor.getConfigurationPropertySource());PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(result, activationContext, true);Binder binder = new Binder(sources, placeholdersResolver, null, null, null);ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);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);List<ConfigDataLocation> imports = contributor.getImports();this.logger.trace(LogMessage.format("Processing imports %s", imports));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++;}}private ConfigDataEnvironmentContributor getNextToProcess(ConfigDataEnvironmentContributors contributors,ConfigDataActivationContext activationContext, ImportPhase importPhase) {for (ConfigDataEnvironmentContributor contributor : contributors.getRoot()) {if (contributor.getKind() == Kind.UNBOUND_IMPORT|| isActiveWithUnprocessedImports(activationContext, importPhase, contributor)) {return contributor;}}return null;}Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,List<ConfigDataLocation> locations) {try {Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);return load(loaderContext, resolved);}catch (IOException ex) {throw new IllegalStateException("IO error on loading imports from " + locations, ex);}}private Deque<StandardConfigDataReference> getReferencesForConfigName(String name,ConfigDataLocation configDataLocation, String directory, String profile) {Deque<StandardConfigDataReference> references = new ArrayDeque<>();for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {for (String extension : propertySourceLoader.getFileExtensions()) {StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory,directory + name, profile, extension, propertySourceLoader);if (!references.contains(reference)) {references.addFirst(reference);}}}return references;}private Set<StandardConfigDataReference> getReferencesForDirectory(ConfigDataLocation configDataLocation,String directory, String profile) {Set<StandardConfigDataReference> references = new LinkedHashSet<>();for (String name : this.configNames) {Deque<StandardConfigDataReference> referencesForName = getReferencesForConfigName(name, configDataLocation,directory, profile);references.addAll(referencesForName);}return references;}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) {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) {Iterable<ConfigurationPropertySource> sources = Collections.singleton(contributor.getConfigurationPropertySource());PlaceholdersResolver placeholdersResolver = new ConfigDataEnvironmentContributorPlaceholdersResolver(result, activationContext, true);Binder binder = new Binder(sources, placeholdersResolver, null, null, null);ConfigDataEnvironmentContributor bound = contributor.withBoundProperties(binder);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);List<ConfigDataLocation> imports = contributor.getImports();this.logger.trace(LogMessage.format("Processing imports %s", imports));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++;}}Map<ConfigDataResolutionResult, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,List<ConfigDataLocation> locations) {try {Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);return load(loaderContext, resolved);}catch (IOException ex) {throw new IllegalStateException("IO error on loading imports from " + locations, ex);}}private Map<ConfigDataResolutionResult, ConfigData> load(ConfigDataLoaderContext loaderContext,List<ConfigDataResolutionResult> candidates) throws IOException {Map<ConfigDataResolutionResult, ConfigData> result = new LinkedHashMap<>();for (int i = candidates.size() - 1; i >= 0; i--) {ConfigDataResolutionResult candidate = candidates.get(i);ConfigDataLocation location = candidate.getLocation();ConfigDataResource resource = candidate.getResource();if (resource.isOptional()) {this.optionalLocations.add(location);}if (this.loaded.contains(resource)) {this.loadedLocations.add(location);}else {try {ConfigData loaded = this.loaders.load(loaderContext, resource);if (loaded != null) {this.loaded.add(resource);this.loadedLocations.add(location);result.put(candidate, loaded);}}catch (ConfigDataNotFoundException ex) {handle(ex, location, resource);}}}return Collections.unmodifiableMap(result);}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);}

http://www.xdnf.cn/news/15200.html

相关文章:

  • NLP:RNN文本生成案例分享
  • 常用控件QWidget
  • 第10讲——一元函数积分学的几何应用
  • 关于解决win 11安装mathtype报错的问题(toolbar.eql)
  • 计算机毕业设计ssm基于Web的高校食堂管理系统 基于SSM框架的大学智慧餐饮服务平台 JavaWeb校园食堂一站式订餐与供应链系统
  • 【kubernetes】--controller(DaemonSet)
  • SD卡初始化、命令及响应命令格式(详细)讲解
  • 分层架构的C++高并发内存池性能优化
  • 无法打开windows安全中心解决方案
  • DirectX Repair修复工具下载,.NET修复,DirectX修复
  • 2025 全球酒店用品厂家竞争力排行榜发布:扬州卓韵领衔,布草工厂实力重塑行业格局
  • 关于 验证码系统 详解
  • Android音视频探索之旅 | C++层使用OpenGL ES实现音频渲染
  • Python数据容器-集合set
  • 《硬件产品经理》第八章:产品生产制造
  • Android 系统默认Launcher3 菜单模式双层改成单层-3
  • 【设计模式】适配器模式(包装器模式),缺省适配器模式,双向适配器模式
  • 带货视频评论洞察 Baseline 学习笔记 (Datawhale Al夏令营)
  • Ntfs!LfsFlushLfcb函数分析之while的循环条件NextLbcb的确定和FirstLbcb->LbcbFlags的几种情况
  • OpenVela之模拟器调试
  • Go内存分配
  • vite如何生成gzip,并在服务器上如何设置开启
  • 如何在 Windows 10 上安装 RabbitMQ
  • 如何在 Visual Studio Code 中使用 Cursor AI
  • 【嵌入式硬件实例】-555定时器实现倍压电路
  • C语言:20250712笔记
  • 系统学习Python——并发模型和异步编程:基础实例-[使用线程实现旋转指针]
  • Ruby如何采集直播数据源地址
  • tiktok 弹幕 逆向分析
  • 后端定时过期方案选型