学一学SpringBoot多环境配置

61 阅读4分钟

前言

开发过程中必然使用到的多环境案例,通过简单的案例分析多环境配置的实现过程。

一、案例

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测试结果

  • 开发环境

image-20241117213950534

  • 测试环境

image-20241117214023941

  • 生产环境

image-20241117214142326

二、配置文件解析过程

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);
     }
 }
  • 遍历环境后置处理器

image-20241117215028418

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/*/

image-20241117223025218

classpath:/;optional:classpath:/config/

image-20241117223149610

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);
     }
 }

两个解析器

image-20241117223422419.png

特性ConfigTreeConfigDataLocationResolverStandardConfigDataLocationResolver
主要用途解析配置树格式文件(文件名-文件内容映射)。解析传统配置文件(.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解析主配置文件

image-20241117223953518

  • 加载配置文件
 // 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);
 }
  • 选择对应的加载器加载文件

image-20241117224512235

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;
     }
 }
 ​
 ​

image-20241117230715095

  • 处理激活环境中的配置信息

image-20241117230817184

  • 调用withProcessedImports对application-profiles.yml进行解析加载

image-20241117231313192

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);
 }

image-20241117231718974

三、总结

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,供应用运行时使用。学一学SpringBoot多环境配置