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}.yml
        references.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);
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值