前言
开发过程中必然使用到的多环境案例,通过简单的案例分析多环境配置的实现过程。
一、案例
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_ACTIVATIONImportPhase 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,供应用运行时使用。