Bootstrap

SpringBoot多环境配置的实现

前言

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

一、案例

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

特性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,供应用运行时使用。

;