SpringCloud-配置中心源码分析

为什么需要配置中心?

分布式微服务架构,当一个服务存在多个实例分布在不同的服务器上,需要对该服务进行配置变更时,需要逐个服务器进行配置,并需要重启服务。配置中心就是为了解决这个问题,让配置一处变更,处处生效。

开源的配置中心:

  • Diamond(super)

  • Apoll 携程

  • spring cloud config

  • nacos(alibaba) (服务注册、 配置中心)

  • zookeeper

差异化对比:

  • 权限管理

  • 高可用特性

  • 通信协议

  • 数据更新的方式(pull/push)
  • 是否支持多语言
  • 是否支持灰度

spring-cloud-config

配置中心源码分析要解决的问题:

  1. 我在持久化层存储的配置,config server可以通过一系列的接口直接访问,那么server拿到配置后是通过什么方式给到client?
  2. client拿到server 给的配置后,又是怎样塞给Environment对象的,为什么不用重启服务配置就能生效?

1. bootstrap.yml的加载

在对springboot的启动过程有过一定了解之后,我已经了解到对项目进行的配置最终都会加载到Environment对象中,该对象中包含了很对配置来源:

  • 系统属性
  • 系统环境变量
  • Servlet配置能
  • 命令行配置属性源
  • 配置文件属性源

而springboot应用在启动过程中Environment对象创建完成之后,已经确定了5个属性源,其中没有配置文件以及配置中心

Environment创建完会广播一个事件ApplicationEnvironmentPreparedEvent,这时候监听事件的监听器如下图:

监听器们

BootstrapApplicationListener
1
2
public class BootstrapApplicationListener
implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {...}

从类声明就能看出该监听器将响应ApplicationEnvironmentPreparedEvent事件,找到事件处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
true)) { //spring.cloud.bootstrap.enabled是Bootstrap配置开关,为false表示不允许使用Bootstrap配置文件进行配置
return;
}
// don't listen to events in a bootstrap context
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return; //
}
ConfigurableApplicationContext context = null;
String configName = environment //spring.cloud.bootstrap.name:bootstrap 冒号后面是默认值
.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
.getInitializers()) {
if (initializer instanceof ParentContextApplicationContextInitializer) {
context = findBootstrapContext(
(ParentContextApplicationContextInitializer) initializer,
configName);
}
}
if (context == null) {
//启动会到这里,父context为null 这里会先创建一个父context
context = bootstrapServiceContext(environment, event.getSpringApplication(),
configName);
event.getSpringApplication()
.addListeners(new CloseContextOnFailureApplicationListener(context));
}

apply(context, event.getSpringApplication(), environment);
}

重点是创建父context。bootstrapServiceContext方法源码较长但不复杂,简单分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private ConfigurableApplicationContext bootstrapServiceContext(
ConfigurableEnvironment environment, final SpringApplication application,
String configName) {
// 创建一个Environment对象
StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
// 拿到Environment对象中的初始属性源集合
MutablePropertySources bootstrapProperties = bootstrapEnvironment
.getPropertySources();
//清空操作,完成之后bootstrapProperties没有元素
for (PropertySource<?> source : bootstrapProperties) {
bootstrapProperties.remove(source.getName());
}
String configLocation = environment //默认为null
.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
String configAdditionalLocation = environment //默认为null
.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
Map<String, Object> bootstrapMap = new HashMap<>();
bootstrapMap.put("spring.config.name", configName); // configName = bootstrap
// 如果spring.main.web-application-type=reactive, bootstrap会失败
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);
}
// 配置SpringApplicationBuilder,将通过这个对象创建SpringApplication
SpringApplicationBuilder builder = new SpringApplicationBuilder()
.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
.environment(bootstrapEnvironment)
// Don't use the default properties in this builder
.registerShutdownHook(false).logStartupInfo(false)
.web(WebApplicationType.NONE);
// 拿到创建好的SpringApplication对象
final SpringApplication builderApplication = builder.application();
if (builderApplication.getMainApplicationClass() == null) {
builder.main(application.getMainApplicationClass());
}
if (environment.getPropertySources().contains("refreshArgs")) {
builderApplication
.setListeners(filterListeners(builderApplication.getListeners()));
}
builder.sources(BootstrapImportSelectorConfiguration.class);
final ConfigurableApplicationContext context = builder.run();
// gh-214 using spring.application.name=bootstrap to set the context id via
// `ContextIdApplicationContextInitializer` prevents apps from getting the actual
// spring.application.name
// during the bootstrap phase.
context.setId("bootstrap");
// Make the bootstrap context a parent of the app context
addAncestorInitializer(application, context);
// It only has properties in it now that we don't want in the parent so remove
// it (and it will be added back later)
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}

所以,我们启动应用的时候,run函数创建完第一的Environment对象之后,广播ApplitionEnvironmentPreparedEvent事件,BootstrapApplicationListener响应该事件时会创建一个父context对象,父级别context创建方式也是使用SpringApplication.run()的方式,只是SpringApplication对象的配置创建完全由程序控制,创建出父context之后,会将其引用交给一个叫AncestorInitializer的初始化类,该类在之后的逻辑中会将应用级别的context的父亲引用设置为当前创建的context。

因此第一次创建context的过程只会读取bootstrap.yml配置文件,并且因为父context中并没有加载相关的bean,配置中心中配置的加载也没有执行。

2. Config处理流程

在Spring Cloud Config中,我们通过@Value注解注入了一个属性,但是这个属性不存在于本地配置 中,那么Config是如何将远程配置信息加载到Environment中的呢?这里我们需要思考几个问题

  • 如何将配置加载到 Environment

  • 配置变更时,如何控制 Bean 是否需要 create,重新触发一次 Bean 的初始化,才能将 @Value 注 解指定的字段从 Environment 中重新注入。

  • 配置变更时,如何控制新的配置会更新到 Environment 中,才能保证配置变更时可注入最新的 值。

为了解决这三个问题,Spring Cloud Config规范中定义了三个核心的接口

  • PropertySourceLocator:抽象出这个接口,就是让用户可定制化的将一些配置加载到 Environment。这部分的配置获取遵循了 Spring Cloud Config 的理念,即希望能从外部储存介质 中来 loacte。

  • RefreshScope: Spring Cloud 定义这个注解,是扩展了 Spring 原有的 Scope 类型。用来标识当前 这个 Bean 是一个refresh 类型的 Scope。其主要作用就是可以控制 Bean 的整个生命周期。

  • ContextRefresher:抽象出这个 Class,是让用户自己按需来刷新上下文(比如当有配置刷新时, 希望可以刷新上下文,将最新的配置更新到 Environment,重新创建 Bean 时,就可以从 Environment 中注入最新的配置)。

2.1 服务启动时

Environment是如何在启动过程中从远程服务器上加载配置的呢?

从前面的代码分析过程中我们知道,Environment中所有外部化配置,针对不同类型的配置都会有与之 对应的PropertySource,比如(SystemEnvironmentPropertySource、 CommandLinePropertySource)。以及PropertySourcesPropertyResolver来进行解析。

那Config Client在启动的时候,必然也会需要从远程服务器上获取配置加载到Environment中,这样才 能使得应用程序通过@value进行属性的注入,而且我们一定可以猜测到的是,这块的工作一定又和 spring中某个机制有关系。

PropertySourceBootstrapConfiguration

这个类已经处于spring-config包中,该类是用于在启动过程中加载一些外部配置,配置中心的配置加载就是在此处调用完成的。

它实现了 ApplicationContextInitializer 接口,其目的就是在应 用程序上下文初始化的时候做一些额外的操作.

根据默认的 AnnotationAwareOrderComparator 排序规则对propertySourceLocators数组进行排序 获取运行的环境上下文ConfigurableEnvironment
遍历propertySourceLocators时

  • 调用 locate 方法,传入获取的上下文environment
  • 将source添加到PropertySource的链表中
  • 设置source是否为空的标识标量empty

source不为空的情况,才会设置到environment中

  • 返回Environment的可变形式,可进行的操作如addFirst、addLast

  • 移除propertySources中的bootstrapProperties

  • 根据config server覆写的规则,设置propertySources

  • 处理多个active profiles的配置信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
//排序 this.propertySourceLocators是自动注入的所有Locator
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
// 逐一调用locator的locateCollection方法,获取属性源集合
Collection<PropertySource<?>> source = locator.locateCollection(environment);
...
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) { // 将属性源包装成BootstrapPropertySource
sourceList.add(new BootstrapPropertySource<>(p));
}
composite.addAll(sourceList); // 添加到composite中
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
for (PropertySource<?> p : environment.getPropertySources()) {
// BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrapProperties"
if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
// 环境对象中若存在属性源bootstrapProperties,移除,一般不会有
propertySources.remove(p.getName());
}
}
// 插入属性源bootstrapProperties >>
insertPropertySources(propertySources, composite);
...
}
}

insertPropertySources(propertySources, composite)方法

该方法将远程配置添加到Environment对象中,其中涉及到重载属性的优先级控制,跟进去一看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private void insertPropertySources(MutablePropertySources propertySources,
List<PropertySource<?>> composite) {
MutablePropertySources incoming = new MutablePropertySources();
List<PropertySource<?>> reversedComposite = new ArrayList<>(composite); //逆序compsite
Collections.reverse(reversedComposite);
for (PropertySource<?> p : reversedComposite) {
incoming.addFirst(p);
}
PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
Binder.get(environment(inming)).bind("spring.cloud.config",
Bindable.ofInstance(remoteProperties));
if (!remoteProperties.isAllowOverride() || (!remoteProperties.isOverrideNone()
&& remoteProperties.isOverrideSystemProperties())) {
for (PropertySource<?> p : reversedComposite) {
propertySources.addFirst(p);
}
return;
}
if (remoteProperties.isOverrideNone()) {
for (PropertySource<?> p : composite) {
propertySources.addLast(p);
}
return;
}
if (propertySources.contains(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
if (!remoteProperties.isOverrideSystemProperties()) {
for (PropertySource<?> p : reversedComposite) {
propertySources.addAfter(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, p);
}
}
else {
for (PropertySource<?> p : composite) {
propertySources.addBefore(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, p);
}
}
}
else {
for (PropertySource<?> p : composite) {
propertySources.addLast(p);
}
}
}

一张图看清楚变量间的关系

根据重载的具体属性,将远程属性源插入到不同的位置,由此我可以猜测出属性的优先级是由属性源在列表中的位置决定的,排在前面的优先级高,get属性是从前往后遍历,get到就返回。

  • 若发生普通属性重载,将远程属性源添加到列表最前面,addfirst 优先级最高

  • 若远程配置没有重载本地属性,将远程属性源添加到最后面位置 addlast

  • 若是重载了系统环境变量属性,则根据是否能重载环境变量属性的配置将该属性添加到元属性源之前或者之后

ConfigServicePropertySourceLocator

下面分析一下远程属性的获取流程,ConfigServicePropertySourceLocator类就是用来定位远程属性的

重点看locate方法:它会通过RestTemplate调用一个远程地址获得配置信息, getRemoteEnvironment 。然后把这个配置PropertySources,然后将这个信息包装成一个OriginTrackedMapPropertySource,设置到 Composite 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Override
@Retryable(interceptor = "configServerRetryInterceptor")
public org.springframework.core.env.PropertySource<?> locate(
org.springframework.core.env.Environment environment) {
ConfigClientProperties properties = this.defaultProperties.override(environment);
CompositePropertySource composite = new OriginTrackedCompositePropertySource(
"configService");
// 使用restTemplate发起远程调用
RestTemplate restTemplate = this.restTemplate == null
? getSecureRestTemplate(properties) : this.restTemplate;
Exception error = null;
String errorBody = null;
try {
String[] labels = new String[] { "" };
...
for (String label : labels) {
//获取远程Environment对象,重点 >>
Environment result = getRemoteEnvironment(restTemplate, properties,
label.trim(), state);
if (result != null) {
log(result);
// result.getPropertySources() can be null if using xml
if (result.getPropertySources() != null) {
for (PropertySource source : result.getPropertySources()) {
@SuppressWarnings("unchecked")
Map<String, Object> map = translateOrigins(source.getName(),
(Map<String, Object>) source.getSource());
composite.addPropertySource(
new OriginTrackedMapPropertySource(source.getName(),
map));
}
}

if (StringUtils.hasText(result.getState())
|| StringUtils.hasText(result.getVersion())) {
HashMap<String, Object> map = new HashMap<>();
putValue(map, "config.client.state", result.getState());
putValue(map, "config.client.version", result.getVersion());
composite.addFirstPropertySource(
new MapPropertySource("configClient", map));
}
return composite;
}
}
errorBody = String.format("None of labels %s found", Arrays.toString(labels));
}
...
return null;
}

OriginTrackedMapPropertySource 表示可追踪源的属性源

2.2. 服务运行中

配置中心需要解决的另一个问题是,服务运行中如何将配置中心配置变更在不重启服务的前提下同步到服务端。

解决方案是利用回调actuator接口+消息总线广播的方式。

它提供了一个刷新机制,但是需要我们主动触发。那就是 @RefreshScope 注解并结合 actuator ,注意要引入 spring-boot-starter-actuator 包。

1、在 config client 端配置中增加 actuator 配置,上面大家可能就注意到了。

1
2
3
4
5
6
7
8
management:
endpoint:
shutdown:
enabled: false
endpoints:
web:
exposure:
include: "*"

其实这里主要用到的是 refresh 这个接口

2、在需要读取配置的类上增加 @RefreshScope 注解,我们是 controller 中使用配置,所以加在 controller 中。

注意,以上都是在 client 端做的修改。

之后,重启 client 端,重启后,我们修改 github 上的配置文件内容,并提交更改,再次刷新页面,没有反应。没有问题。

接下来,我们发送 POST 请求到 http://localhost:XXXX/actuator/refresh 这个接口,用 postman 之类的工具即可,此接口就是用来触发加载新配置的,返回内容如下:

1
2
3
4
[    
"config.client.version",
"data.env"
]

这就结束了吗,并没有,总不能每次改了配置后,就用 postman 访问一下 refresh 接口吧,还是不够方便呀。 github 提供了一种 webhook 的方式,当有代码变更的时候,会调用我们设置的地址,来实现我们想达到的目的。

1、进入 github 仓库配置页面,选择 Webhooks ,并点击 add webhook;

2、之后填上回调的地址,也就是上面提到的 actuator/refresh 这个地址,但是必须保证这个地址是可以被 github 访问到的。如果是内网就没办法了。这也仅仅是个演示,一般公司内的项目都会有自己的代码管理工具,例如自建的 gitlab,gitlab 也有 webhook 的功能,这样就可以调用到内网的地址了。

那么当服务部署多个实例的情况下怎么办呢,总不能在git上设置多个webhook呀,这也不被允许。此时可是使用Spring Cloud Bus 来自动刷新多个端

Spring Cloud Bus 将分布式系统的节点与轻量级消息代理链接。这可以用于广播状态更改(例如配置更改)或其他管理指令。一个关键的想法是,Bus 就像一个扩展的 Spring Boot 应用程序的分布式执行器,但也可以用作应用程序之间的通信渠道。

—— Spring Cloud Bus 官方解释

如果只有一个 client 端的话,那我们用 webhook ,设置手动刷新都不算太费事,但是如果端比较多的话呢,一个一个去手动刷新未免有点复杂。这样的话,我们可以借助 Spring Cloud Bus 的广播功能,让 client 端都订阅配置更新事件,当配置更新时,触发其中一个端的更新事件,Spring Cloud Bus 就把此事件广播到其他订阅端,以此来达到批量更新。

1、Spring Cloud Bus 核心原理其实就是利用消息队列做广播,所以要先有个消息队列,目前官方支持 RabbitMQ 和 kafka。

这里用的是 RabbitMQ, 所以先要搭一套 RabbitMQ 环境

2、在 client 端增加相关的包,注意,只在 client 端引入就可以。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

3、在配置文件中增加 RabbitMQ 相关配置,默认的端口应该是 5672 ,因为我是用 docker 创建的,所以有所不同。

1
2
3
4
5
6
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest

4、启动两个或多个 client 端,准备来做个测试

在启动的时候分别加上 vm option:-Dserver.port=3302 和 -Dserver.port=3303 ,然后分别启动就可以了。

访问其中一个的 actuator/bus-refresh 地址,注意还是要用 POST 方式访问。之后查看控制台输出,会看到这两个端都有一条这样的日志输出