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]); }
这个静态初始化块定义了两个位置组:
类路径位置组:
classpath:/
和classpath:/config/
文件系统位置组:
file:./
(当前目录)、file:./config/
和file:./config/*/
2.2 优先级规则解析
从源码可以看出:
文件系统位置组整体优先级高于类路径位置组
文件系统组内优先级顺序:
file:./config/*/
(最高)file:./config/
file:./
类路径组内优先级顺序:
classpath:/config/
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); }
这个逆序循环意味着:
低优先级位置先加载
高优先级位置后加载
后加载的配置覆盖先加载的配置
4.2 位置解析优先级
位置解析时考虑了profile特定文件:
java
private Set<StandardConfigDataReference> getReferencesForDirectory(...) {for (String name : this.configNames) { // configNames包含"bootstrap"// 查找bootstrap-{profile}.ymlreferences.addAll(getReferencesForConfigName(name, ...));} }
加载顺序为:
bootstrap-{profile}.yml
bootstrap.yml
五、完整加载顺序总结
5.1 优先级金字塔
基于源码分析,bootstrap.yml的完整加载顺序(从高到低):
优先级 | 位置 | 说明 |
---|---|---|
1 | spring.config.import | 手动导入的最高优先级位置 |
2 | spring.config.additional-location | 额外添加的位置 |
3 | file:./config/*/ | 当前目录config下的任意子目录 |
4 | file:./config/ | 当前目录下的config目录 |
5 | file:./ | 当前目录(JAR文件所在目录) |
6 | classpath:/config/ | 类路径下的config目录 |
7 | classpath:/ | 类路径根目录(最低优先级) |
5.2 Profile处理机制
当激活特定profile(如dev)时:
优先加载
bootstrap-dev.yml
然后加载
bootstrap.yml
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'
七、最佳实践建议
生产环境配置:将
bootstrap.yml
放在JAR同级的config/
目录下敏感信息管理:外部配置文件不要提交到代码仓库
配置覆盖:需要覆盖默认配置时,使用高优先级位置
Profile管理:合理使用
bootstrap-{profile}.yml
管理环境差异调试技巧:使用
--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);}