【Spring Cloud】SpringBoot 2.4 前后 Spring Cloud Config 的变化

前言

SpringBoot 2.4 版本对 外部化配置 的机制做了 不向前兼容(若干版本内可回退)的调整:

  • 于整体,配置文件的优先级有调整,比如 config 子文件配置优先级提高,配置文件位置优先级大于 profile 优先级,optional 前缀、import 关键字 的引入 等
  • 于细节,提供了新的 ApplicationEnvironmentPreparedEvent 事件监听器以及新的 EnvironmentPostProcessor,对应新的配置文件机制 等
更多细节可见下文

【SpringBoot】对比 SpringBoot 2.4.0 版本前后配置文件机制改动

外部化配置 机制的修改,势必会影响到 Spring Cloud Config 配置中心组件的使用,本人在较深入地学习 Spring Cloud Config 时正好接触到这一部分内容,就自己的了解进行记录与分享

约定

  • old:SpringBoot 版本 2.3.x,对应 Spring Cloud 版本为 Hoxton.SRx,对应 spring-cloud-context 版本 2.2.x,对应 spring-cloud-config 版本 2.2.x
  • new:SpringBoot 版本 2.4.x,对应 Spring Cloud 版本为 2020.x,对应的 spring-cloud-contextspring-cloud-config 都是最新版本(当前 master 分支)

主题

  • spring-cloud-config-oldclient 配置为什么必须在 bootstrap.yaml 文件
  • 简单了解 spring-cloud-config-oldserver client 代码细节
  • spring-cloud-config-new 的对应改动

old

old 版本下的 spring-cloud-configclient 必须在 bootstrap.yaml 配置文件中指定诸如 spring.application.name spring.cloud.config.uri 等属性,如 bootstrap.yaml

spring:
  application:
    name: foo
  cloud:
    config:
      uri: http://localhost:8888
  profiles:
    active: dev

BootstrapApplicationListener

当我们引入 spring-cloud-context 依赖(组件默认引入)后,该监听器就会被装配
BootstrapApplicationListener

SpringApplication 启动前期,创建对应的 Environment 实例并简单配置后会发布 ApplicationEnvironmentPreparedEvent 事件,该事件就会被 BootstrapApplicationListener 监听到

	@Override
	public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
		ConfigurableEnvironment environment = event.getEnvironment();

		// 可配置关闭,2.4 前默认开
		if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
				true)) {
			return;
		}
		// 引导容器内不处理该事件
		if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
			return;
		}
		ConfigurableApplicationContext context = null;

		// 引导配置文件名默认 bootstrap
		String configName = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");

		// 执行 ParentContextApplicationContextInitializer
		// ...

		// 创建引导容器(Spring Cloud Bootstrap ApplicationContext)
		if (context == null) {
			context = bootstrapServiceContext(environment, event.getSpringApplication(),
					configName);
			event.getSpringApplication()
					.addListeners(new CloseContextOnFailureApplicationListener(context));
		}

		/**
		 * 这个地方会把 引导容器 中所有的 ApplicationContextInitializer
		 * 		类型的 bean 组件添加为 主容器 的 initializers
		 * 比如核心的 PropertySourceBootstrapConfiguration
		 */
		apply(context, event.getSpringApplication(), environment);
	}

====================== bootstrapServiceContext

	private ConfigurableApplicationContext bootstrapServiceContext(
			ConfigurableEnvironment environment, final SpringApplication application,
			String configName) {

		// 创建新的 StandardEnvironment
		StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
		MutablePropertySources bootstrapProperties = bootstrapEnvironment
				.getPropertySources();
		for (PropertySource<?> source : bootstrapProperties) {
			bootstrapProperties.remove(source.getName());
		}

		/**
		 * 基础引导属性配置
		 */
		String configLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
		String configAdditionalLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
		Map<String, Object> bootstrapMap = new HashMap<>();
		bootstrapMap.put("spring.config.name", configName);
		bootstrapMap.put("spring.main.web-application-type", "none");
		if (StringUtils.hasText(configLocation)) {
			bootstrapMap.put("spring.config.location", configLocation);
		}
		if (StringUtils.hasText(configAdditionalLocation)) {
			bootstrapMap.put("spring.config.additional-location",
					configAdditionalLocation);
		}
		bootstrapProperties.addFirst(
				new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
		for (PropertySource<?> source : environment.getPropertySources()) {
			if (source instanceof StubPropertySource) {
				continue;
			}
			bootstrapProperties.addLast(source);
		}
		
		/**
		 * 构建对应的 SpringApplication,启动引导容器
		 */
		SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
				.environment(bootstrapEnvironment)
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
		final SpringApplication builderApplication = builder.application();
		if (builderApplication.getMainApplicationClass() == null) {
			builder.main(application.getMainApplicationClass());
		}
		if (environment.getPropertySources().contains("refreshArgs")) {
			builderApplication
					.setListeners(filterListeners(builderApplication.getListeners()));
		}

		/**
		 * 引导容器的 自动装配类,其机制类似于 SpringBoot 的自动装配
		 * 		只是针对 引导容器,因此其中的 配置 对 SB 容器不可见
		 * 对应地,上述装配类中的 bean组件,也只可见引导容器中的
		 * 		Environment,这也是为什么 Spring Cloud Config Client
		 * 		必须通过 bootstrap.yaml 配置
		 */
		builder.sources(BootstrapImportSelectorConfiguration.class);

		// 启动引导容器
		final ConfigurableApplicationContext context = builder.run();
		context.setId("bootstrap");
		
		// 指定为当前容器(SpringBoot)的父容器
		addAncestorInitializer(application, context);
		bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
		mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
		return context;
	}
  • Spring Boot 2.4 以前,Spring Cloud 应用默认都以 引导容器 的方式启动
  • 引导容器 会是 当前主容器(即 SpringBoot 应用的容器)的 父容器,它拥有自己的 Environment,且默认配置文件名为 bootstrap,因此它是完全不感知 主容器 的配置的。换句话说,在 引导容器 内创建的 bean 组件是无法使用 主容器 的配置属性的,这就是为什么 spring-cloud-config-client-old 的属性要配置在 bootstrap.yaml 文件中的一部分原因
  • 引导容器 中会引入资源类 BootstrapImportSelectorConfiguration,可类比于 Spring BootAutoConfigurationImportSelector,它更像 引导容器自动装配类,这部分配置类只针对 引导容器 可见,比如 spring-cloud-config-client-old 下的 ConfigServiceBootstrapConfiguration,这就是为什么 spring-cloud-config-client-old 的属性要配置在 bootstrap.yaml 文件中的另一部分原因
  • 启动 引导容器 后会把 引导容器 中所有的 ApplicationContextInitializer 类型的 bean 组件添加为 主容器initializers,比如核心的 PropertySourceBootstrapConfiguration

spring-cloud-config-server

简单梳理下 spring-cloud-config-server-old 的启动流程,之后的应用就不再赘述了:

  1. 主容器启动 SpringApplication.run
  2. 发布 ApplicationEnvironmentPreparedEvent 事件,被 BootstrapApplicationListener 监听到启动 引导容器
  3. 启动引导容器后 PropertySourceBootstrapConfiguration 配置类被添加为 主容器initializers
  4. 事实上,spring-cloud-config-server 的引导配置类 ConfigServerBootstrapConfiguration 默认无动作
  5. 主容器 继续启动,在 prepareContext 阶段 applyInitializers 方法执行所有 initializers,其中包括 PropertySourceBootstrapConfiguration,但此处无动作(主要针对 spring-cloud-config-client
  6. 主容器 继续启动,到注册解析自动装配类 ConfigServerAutoConfiguration 时,默认引入配置类为 DefaultRepositoryConfiguration,此处会注册默认的 MultipleJGitEnvironmentRepository 实例,它是一个 InitializingBean,用来完成远端配置的获取,这里便是 spring-cloud-config-server 功能的真正实现,点到为止不再深入
  7. 启动完成

spring-cloud-config-client

忽略上述步骤,此处重点关注引导自动装配类 ConfigServiceBootstrapConfiguration
自动装配类

ConfigServiceBootstrapConfiguration

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
public class ConfigServiceBootstrapConfiguration {

	@Autowired
	private ConfigurableEnvironment environment;

	/**
	 * 基于引导容器的 environment 创建 ConfigClientProperties
	 * 即 bootstrap.yaml 配置文件
	 */
	@Bean
	public ConfigClientProperties configClientProperties() {
		ConfigClientProperties client = new ConfigClientProperties(this.environment);
		return client;
	}

	/**
	 * 注册 PropertySourceLocator 来加载配置
	 */
	@Bean
	@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
	@ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true)
	public ConfigServicePropertySourceLocator configServicePropertySource(
			ConfigClientProperties properties) {
		ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(
				properties);
		return locator;
	}

	// ...

}
  • ConfigClientProperties 实例是基于 引导容器environment 创建的,这就解释了为什么 old 版本的 spring-cloud-config-client 要用 bootstrap.yaml 配置才生效
  • 注册了一个 ConfigServicePropertySourceLocator 实例,之前提到 引导容器 启动后期会添加 PropertySourceBootstrapConfiguration主容器initliazier,在对应时机被执行
之前一直听到的说法是,application.yaml 对 client 配置不生效是因为优先级
低于 bootstrap.yaml,我认为这句话并不完全对:因为在 client 对应的 引导容
器中,根本都看不到主容器(即 application.yaml)对应的配置,跟优先级已经没
什么关系了

PropertySourceBootstrapConfiguration


	/**
	 * 容器中所有的 PropertySourceLocator
	 */
	@Autowired(required = false)
	private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
	
	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		List<PropertySource<?>> composite = new ArrayList<>();
		AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
		boolean empty = true;
		ConfigurableEnvironment environment = applicationContext.getEnvironment();

		/**
		 * 遍历所有 propertySourceLocators 收集配置
		 */
		for (PropertySourceLocator locator : this.propertySourceLocators) {
			
			// ...
			
		}
		
		// ...
		
	}
  • 收集容器中所有的 PropertySourceLocator,其中就包括刚才的 ConfigServicePropertySourceLocator
  • 在执行 initialize 方法时从 spring-cloud-server 获取配置,不再深入

new

到了 new 版本的 spring-cloud-config,变化主要体现在 client 的配置,使用关键字 spring.config.import: [optional:]configserver: 来配置,且无须配置在 bootstrap.yaml 文件中,如 application.yaml

spring:
  application:
    name: foo
  profiles:
    active: dev
  config:
    import: optional:configserver:http://localhost:8931

BootstrapApplicationListener

	@Override
	public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
		ConfigurableEnvironment environment = event.getEnvironment();

		/**
		 * 默认关闭
		 * 可以手动开启或者通过 spring.config.use-legacy-processing 开启
		 */
		if (!bootstrapEnabled(environment) && !useLegacyProcessing(environment)) {
			return;
		}
		
		// ...
		
	}
  • Spring Boot 2.4 后,Spring Cloud 应用不再默认基于 引导容器 启动了, 当然也可以通过手动指定属性或者回退到之前的 外部化配置 模式来实现
  • 其实究竟为什么会有 引导容器 机制以及为什么又取消了这种机制,背后的设计理念我也是不清楚的(反正肯定不是因为 spring-cloud-config …)

spring-cloud-config-server

取消 引导容器 机制并没有影响 spring-cloud-config-server 的整体链路,依旧是在 主容器 解析 自动装配类 的阶段,实现 远程配置 的获取

spring-cloud-config-client

首先,既然不基于 引导容器 启动,那么必然就不强依赖于 bootstrap.yaml 了,它的配置处理完全基于一种新的机制:

  • 同样是 SpringApplication 启动前期发布 ApplicationEnvironmentPreparedEvent 事件,对应的监听器 EnvironmentPostProcessorApplicationListener 调用所有 EnvironmentPostProcessor 进行配置处理
  • 其中,ConfigDataEnvironmentPostProcessor 处理对应的配置信息(ConfigData),这又会调用 ConfigDataLoader ConfigDataLocationResolver 对不同配置进行 加载、解析
  • 上述 ConfigDataLoader ConfigDataLocationResolver 都是以类似的 SPI 机制去获取的(即配置在 spring.factories 中)
  • spring-cloud-config-client 分别提供了 ConfigServerConfigDataLoader ConfigServerConfigDataLocationResolver 进行配置中心服务端的配置获取和解析
  • 不再深入

总结

Spring Boot 2.4 以前,其对应版本 Spring Cloud Config

  • 无论 server client 端都会基于 引导容器 启动
  • server 端在 主容器 解析 自动装配类 阶段时获取 远程配置
  • client 端在 主容器 prepareContext 阶段 applyInitializers 方法加载 server 端配置,因为对应的 bean实例 依赖于 引导容器Environment,所以强依赖于 bootstrap.yaml

Spring Boot 2.4 以后,其对应版本 Spring Cloud Config

  • Spring Cloud 组件不再默认基于 引导容器 启动
  • server 端在 主容器 解析 自动装配类 阶段时获取 远程配置
  • client 端的配置文件解析提前到 ApplicationEnvironmentPreparedEvent 事件的监听器中,基于 Spring Boot 2.4 的新配置文件加载机制实现,不再强依赖于 bootstrap.yaml
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值