引言:配置加载的变革
在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}.yml references.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);
}