【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-context
和spring-cloud-config
都是最新版本(当前master
分支)
主题
spring-cloud-config-old
的client
配置为什么必须在bootstrap.yaml
文件- 简单了解
spring-cloud-config-old
的server
client
代码细节 spring-cloud-config-new
的对应改动
old
old
版本下的 spring-cloud-config
, client
必须在 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
依赖(组件默认引入)后,该监听器就会被装配
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 Boot
的AutoConfigurationImportSelector
,它更像引导容器
的自动装配类
,这部分配置类只针对引导容器
可见,比如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
的启动流程,之后的应用就不再赘述了:
- 主容器启动
SpringApplication.run
- 发布
ApplicationEnvironmentPreparedEvent
事件,被BootstrapApplicationListener
监听到启动引导容器
- 启动引导容器后
PropertySourceBootstrapConfiguration
配置类被添加为主容器
的initializers
- 事实上,
spring-cloud-config-server
的引导配置类ConfigServerBootstrapConfiguration
默认无动作 主容器
继续启动,在prepareContext
阶段applyInitializers
方法执行所有initializers
,其中包括PropertySourceBootstrapConfiguration
,但此处无动作(主要针对spring-cloud-config-client
主容器
继续启动,到注册解析自动装配类ConfigServerAutoConfiguration
时,默认引入配置类为DefaultRepositoryConfiguration
,此处会注册默认的MultipleJGitEnvironmentRepository
实例,它是一个InitializingBean
,用来完成远端配置的获取,这里便是spring-cloud-config-server
功能的真正实现,点到为止不再深入- 启动完成
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