我们知道 Spring Cloud Config 是 Spring Cloud 提供的配置中心实现工具,我们可以通过它把配置信息存放在 Git 等第三方配置仓库中。每当 Spring Cloud Config 客户端启动时,就会发送 HTTP 请求到服务器端获取配置信息,这点比较好理解。但事实上,在 Git 中更改了配置信息之后,客户端并不会主动再次请求最新配置,而是使用缓存到本地的原有配置信息。如图 1。

图 1 配置信息自动更新问题
那么问题就来了,在这种情况下, **Spring Cloud Config 是如何实时获取到更改后的配置呢?**这就是今天我们要讨论的内容。通过理解 Spring Cloud Config 配置信息自动更新的执行过程,有助于我们深入把握框架的底层原理。
在对底层原理进行详细展开之前,我们先来给出 Spring Cloud Config 应对这一问题的具体做法。事实上,Spring Cloud Config 能够做到配置信息的自动更新,是依赖于 Spring Cloud 中的另一个组件,即 Spring Cloud Bus。
Spring Cloud Bus 是 Spring Cloud 中用于实现消息总线的专用组件,它集成了 RabbitMQ、Kafka 等主流消息中间件。当我们在 Spring Cloud Config 服务器端代码工程的类路径中添加 Spring Cloud Bus 的引用并启动应用程序之后,Spring Boot Actuator 就为我们提供了/actuator/bus-refresh 端点, 通过访问该端点就可以达到对客户端所有服务实例的配置信息进行自动更新的效果。在这种方案中,服务端会主动通知所有客户端进行配置信息的更新,这样我们就无需关注各个客户端,而只对服务端进行操作即可。
是不是听起来有点神奇?整个实现过程我们至少要搞清楚三大问题,如图 2 所示。

图 2 实现配置信息自动更新的三个问题
针对这三个问题,接下来我们将结合源码逐一展开讨论。
问题一:如何自动调用服务器端所暴露的/actuator/bus-refresh 端点?
在现代软件开发过程中,开放式平台是一种常见的软件服务形态。我们可以把 Spring Cloud Config Server 所提供的 HTTP 端点视为一种开放式的接口,以供 Git 等第三方工具进行访问和集成。
基于这种思想,我们可以把服务器端/actuator/bus-refresh 端点对外进行暴露,然后第三方工具通过这个暴露的端点进行集成。例如,在 Github 中就设计了一种 Webhook 机制,并提供了用户界面供我们配置所需要集成的端点以及对应的操作,操作方法如图 3 所示。

图 3 Github 的 Webhook 配置界面(来自 Github 官网)
我们可以在上图的 Payload URL 中设置/actuator/bus-refresh 端点地址。所谓的 Webhook,实际上就是一种回调。通过 Webhook,当我们提交代码时,Github 就会自动调用所配置的 HTTP 端点。也就是说,我们可以根据配置项信息的更新情况自动实现对/actuator/bus-refresh 端点的访问。基于 Github 的配置仓库实现方案,我们可以得到如下图所示的系统结构图。

图 4 Github Webhook 机制执行效果图
现在,配置信息一旦有更新,Spring Cloud Config Server 就能从 Github 中获取最新的配置信息了。
问题二:客户端如何得知服务器端的配置信息已经更新?
接下来我们关注第二个问题,即客户端如何得知服务器端的配置信息已经更新?
我们首先需要明确,调用了/actuator/bus-refresh 端点之后,系统内部会发生了什么。这里我们快速浏览 Spring Cloud Bus 中的代码工程,发现存在一个 RefreshBusEndpoint 端点类,如下所示:
typescript
@Endpoint(id = "bus-refresh")
public class RefreshBusEndpoint extends AbstractBusEndpoint {
@WriteOperation
public void busRefreshWithDestination(@Selector String destination) {
//发布 RefreshRemoteApplicationEvent 事件
publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), destination));
}
@WriteOperation
public void busRefresh() {
//发布 RefreshRemoteApplicationEvent 事件
publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null));
}
}
显然,RefreshBusEndpoint 类对应于我们前面访问的/bus-refresh 端点。可以看到,Spring Cloud Bus 在这里做的事情仅仅只是发布了一个新的 RefreshRemoteApplicationEvent 事件。
既然发送了事件,我们就需要寻找该事件的监听者。我们在 Spring Cloud Bus 中找到了 RefreshRemoteApplicationEvent 事件的监听器 RefreshListener,如下所示:
typescript
public class RefreshListener implements ApplicationListener<RefreshRemoteApplicationEvent> {
...
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
//执行配置属性的刷新操作
Set<String> keys = contextRefresher.refresh();
}
}
从类的定义中不难看出该监听器就是用来处理 RefreshRemoteApplicationEvent 事件。可以看到,在它的 onApplicationEvent 方法中同样也是调用了 ContextRefresher 中的 refresh 方法进行配置属性的刷新。
请注意,RefreshRemoteApplicationEvent 是一个远程事件,将通过消息中间件进行发送,并被 Spring Cloud Config 客户端所监听,处理流程如下图所示:

图 5 基于 Spring Cloud Bus 的事件传播机制
问题三:客户端如何实时获取服务器端所更新的配置信息?
最后需要明确的第三个问题是,客户端如何获取服务器端所更新的配置信息,这就需要梳理 Spring Cloud Config Server 与注册中心之间的关系。
我们知道配置中心作为整个微服务架构运行所需的基础服务,需要确保其可用性。因为配置服务本身也是一个独立的微服务,所以 Spring Cloud Config 实现高可用的方式很简单。跟其他微服务一样,它把自己注册到注册中心上,让其他服务提供者或消费者通过注册中心进行服务发现和获取。

图 6 Spring Cloud Config 与注册中心之间的关系
显然,在这种方式下,注册中心的服务治理机制同时提供了服务器端的负载均衡和客户端的配置功能,从而也就间接实现了高可用性。从另一个角度,我们也可以理解为,可以通过注册中心获取所有 Spring Cloud Config 客户端服务的实例,从而在分布式环境下为获取配置信息提供了一种简便的手段。
Spring Cloud Config 提供了一个工具类 ConfigServerInstanceProvider 来完成与注册中心之间的交互,代码如下所示:
kotlin
public class ConfigServerInstanceProvider {
private final DiscoveryClient client;
@Retryable(interceptor = "configServerRetryInterceptor")
public List<ServiceInstance> getConfigServerInstances(String serviceId) {
List<ServiceInstance> instances = this.client.getInstances(serviceId);
if (instances.isEmpty()) {
//抛出异常
}
return instances;
}
}
在这里,我们看到了熟悉的 DiscoveryClient,DiscoveryClient 通过同样熟悉的 getInstances 方法从注册中心中获取 Spring Cloud Config 服务器实例,如下所示:
ini
List<ServiceInstance> instances = this.client.getInstances(serviceId);
ConfigServerInstanceProvider 的调用者是 DiscoveryClientConfigServiceBootstrapConfiguration。现在我们来看这个 Spring Boot 自动配置类的定义,如下所示:
typescript
public class DiscoveryClientConfigServiceBootstrapConfiguration
implements SmartApplicationListener {
public void startup(ContextRefreshedEvent event) {
refresh();
}
}
可以看到,如果系统中生成了 ContextRefreshedEvent 事件就会触发如下所示的 refresh 方法。
ini
private void refresh() {
try {
////获取 Spring Cloud Config 客户端服务实例
String serviceId = this.config.getDiscovery().getServiceId();
List<String> listOfUrls = new ArrayList<>();
List<ServiceInstance> serviceInstances = this.instanceProvider.getConfigServerInstances(serviceId);
//遍历服务实例列表
for (int i = 0; i < serviceInstances.size(); i++) {
ServiceInstance server = serviceInstances.get(i);
String url = getHomePage(server);
//获取配置路径
if (server.getMetadata().containsKey("configPath")) {
String path = server.getMetadata().get("configPath");
if (url.endsWith("/") && path.startsWith("/")) {
url = url.substring(0, url.length() - 1);
}
url = url + path;
}
//填充配置路径
listOfUrls.add(url);
}
String[] uri = new String[listOfUrls.size()];
uri = listOfUrls.toArray(uri);
this.config.setUri(uri);
}
}
在上述 refresh 方法中,Spring Cloud Config 首先会获取配置文件中配置项 spring.cloud.config.discovery.serviceId 所指定的服务实例 id,然后根据 serviceId 从 ConfigServerInstanceProvider 中获取注册服务的实例对象集合 serviceInstances,最后循环遍历 serviceInstances 来更新存储在内存中的配置属性值。
至此,我们通过解答三个问题,引出了 Spring Cloud Config 中实现配置信息自动更新的三个步骤,并基于框架内部一系列组件之间的交互过程剖析了底层实现原理。
总结
今天基于 Spring Cloud Config 框架剖析了实现配置信息自动更新的工作原理。抛出了三个与这个主题相关的核心问题,然后基于源码对这些问题做了一一解答。
事实上,Spring Cloud Config 作为 Spring 自研的配置中心框架,其内部大量使用了 Spring 现有的功能特性,比方说这节课中提到的 Spring 容器的事件发布和监听机制,又比方说 Spring Boot Acuator 中的端点机制以及 Spring Cloud Bus 所具备的消息通信总线机制。这点与我们学习 Netflix 旗下的 Eureka、Zuul 等框架不同。我们需要首先对 Spring 容器相关的知识体系有足够的了解,才能更好地理解 Spring Cloud Config 的设计和实现方式。