前言
开发过程中必然使用到的多环境案例,通过简单的案例分析多环境配置的实现过程。
一、案例
1.1主配置文件
spring:
profiles:
active: prod
server:
port: 8080
1.2多环境配置文件
- 开发环境
blog:
domain: http://localhost:8080
- 测试环境
blog:
domain: https://test.lazysnailstudio.com
- 生产环境
blog:
domain: https://lazysnailstudio.com
1.3测试源码
package com.lazy.snail.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* @ClassName BlogInfoService
* @Description TODO
* @Author lazysnail
* @Date 2024/11/15 14:30
* @Version 1.0
*/
@Service
public class BlogInfoService {
@Value("${blog.domain}")
private String domain;
public String getDomain() {
return domain;
}
}
package com.lazy.snail.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* @ClassName BlogInfoService
* @Description TODO
* @Author lazysnail
* @Date 2024/11/15 14:30
* @Version 1.0
*/
@Service
public class BlogInfoService {
@Value("${blog.domain}")
private String domain;
public String getDomain() {
return domain;
}
}
1.4测试结果
- 开发环境
- 测试环境
- 生产环境
二、配置文件解析过程
2.1SpringBoot启动过程,环境准备阶段
// SpringApplication
public ConfigurableApplicationContext run(String... args) {
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
}
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
listeners.environmentPrepared(bootstrapContext, environment);
}
2.2事件处理
-
应用环境准备事件:ApplicationEnvironmentPreparedEvent
-
事件监听(监听器:EnvironmentPostProcessorApplicationListener)
// 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);
}
}
- 遍历环境后置处理器
2.3配置数据环境后置处理
- 核心方法processAndApply
// ConfigDataEnvironmentPostProcessor
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
postProcessEnvironment(environment, application.getResourceLoader(), application.getAdditionalProfiles());
}
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);
}
}
- processInitial方法解析和加载初始配置文件(如application.yml或application.properties)的内容,封装为contributors对象,解析出来的配置没有立即应用到Spring的Environment中。
- processWithoutProfiles在基础的多环境中基本没有额外操作。
- withProfiles主要是确定激活的profile
- processWithProfiles处理带有profile的配置
- applyToEnvironment将配置信息应用到Spring的环境中
// ConfigDataEnvironment
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;
}
2.4配置文件路径搜索
找到需要处理的导入,加载相关配置,将结果合并到当前的配置贡献者集合(ConfigDataEnvironmentContributors
)中。
// ConfigDataEnvironmentContributors
ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter importer,
ConfigDataActivationContext activationContext) {
// BEFORE_PROFILE_ACTIVATION、AFTER_PROFILE_ACTIVATION
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) {
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);
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++;
}
}
指定配置文件的搜索路径表达式
optional:file:./;optional:file:./config/;optional:file:./config/*/
classpath:/;optional:classpath:/config/
2.5配置文件解析加载
// ConfigDataImporter
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);
}
}
两个解析器
特性 | ConfigTreeConfigDataLocationResolver | StandardConfigDataLocationResolver |
---|---|---|
主要用途 | 解析配置树格式文件(文件名-文件内容映射)。 | 解析传统配置文件(.properties 和 .yml )。 |
典型场景 | 容器化环境,如 Kubernetes ConfigMap 或 Secrets。 | 通常的文件或类路径中的配置文件。 |
数据来源 | 挂载的目录结构,例如 /etc/config 。 | 本地文件系统或类路径,例如 application.properties 。 |
配置导入方式 | spring.config.import=configtree:/path/to/config/ 。 | 默认加载机制或 spring.config.import=file:/path/to/file/ 。 |
2.5.1解析主配置文件
- 加载配置文件
// 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);
}
- 选择对应的加载器加载文件
2.5.2解析激活环境配置
- 获取激活环境
// ConfigDataEnvironment
private ConfigDataActivationContext withProfiles(ConfigDataEnvironmentContributors contributors,
ConfigDataActivationContext activationContext) {
this.logger.trace("Deducing profiles from current config data environment contributors");
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);
} catch (BindException ex) {
if (ex.getCause() instanceof InactiveConfigDataAccessException) {
throw (InactiveConfigDataAccessException) ex.getCause();
}
throw ex;
}
}
- 处理激活环境中的配置信息
- 调用withProcessedImports对application-profiles.yml进行解析加载
2.6环境应用
- 将所有解析的配置信息应用到Spring的环境中
// 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();
applyContributor(contributors, activationContext, propertySources);
DefaultPropertiesPropertySource.moveToEnd(propertySources);
Profiles profiles = activationContext.getProfiles();
this.logger.trace(LogMessage.format("Setting default profiles: %s", profiles.getDefault()));
this.environment.setDefaultProfiles(StringUtils.toStringArray(profiles.getDefault()));
this.logger.trace(LogMessage.format("Setting active profiles: %s", profiles.getActive()));
this.environment.setActiveProfiles(StringUtils.toStringArray(profiles.getActive()));
this.environmentUpdateListener.onSetProfiles(profiles);
}
三、总结
3.1实现的底层流程
(1)processInitial阶段
- 首先加载默认配置文件application.yml。
- 如果配置中存在动态导入 (spring.config.import),会解析导入源,但此时不会解析 spring.profiles.active。
(2)processWithoutProfiles阶段
- 执行额外的静态配置绑定,如处理动态导入的配置源。
- 此阶段仍未激活Profiles,仅为后续处理提供基础环境。
(3)withProfiles阶段
- 确定当前激活的Profile:
- 根据spring.profiles.active获取激活的Profiles。
- 如果没有设置,则使用spring.profiles.default或回退到默认Profile。
- 动态调整配置上下文,为接下来的加载提供Profile信息。
(4)processWithProfiles阶段
- 基于激活的Profiles,加载对应的配置文件(如application-dev.yml)。
- 合并所有配置源,按优先级覆盖默认配置。
(5)applyToEnvironment阶段
- 将解析后的所有配置应用到Spring的Environment对象中。
- Spring的容器在运行时可以直接从Environment中读取合并后的配置值。
3.2实现机制
配置文件分层:支持默认和环境特定配置文件。
动态激活:通过spring.profiles.active指定激活的环境。
加载优先级:先加载默认配置,再加载特定环境配置,按优先级覆盖。
合并与应用:所有配置合并后统一注入到Environment,供应用运行时使用。