一文带你搞懂Apollo配置中心动态刷新配置的原理
背景介绍
相信应该有不少小伙伴在工作中接触过Apollo配置中心,毕竟在国内用的最多的两款配置中心就是Apollo和nacos,记得我之前从事过的某家大公司就用到了它。我们都知道像Apollo和nacos这种现代化的配置中心是支持配置动态刷新的,整个过程中不需要重启服务器、接下来,我要问大家一个问题:这种动态刷新配置,而不需要重启服务器的能力是如何实现的?
接下来我将带大家解决这个问题,话不多话,咱们开始吧!需要注意的是:本篇文章基于Apollo配置中心讲解,至于nacos的动态刷新配置是如何实现的就留给大家当家庭作业了。
Apollo的项目架构
如果我们要讲Apollo配置中心动态刷新配置的原理,就不得不给大家介绍下Apollo的整个项目架构,如下图所示:

我们一边看着上面的架构图,一边回忆下我们平时是怎么使用apollo配置中心的。大致过程如下:
- 首先,我们进入到
portal service的前端页面,也就是apollo的管理页面,然后去添加或者修改或者删除某个配置。当我们做这个操作时,浏览器实际上会给portal service服务发送请求,然后portal service会去调用admin service服务的接口去操作config db数据库中的配置。 - 然后我们应用中的
apollo client会通过长轮训(以及定时任务作为fallback)调用config service服务的接口获取到最新的配置。 apollo client拿到最新的配置后,会去比对哪些配置发生了变化,将那些发生了变化的配置找出来。对于这些已经发生了变化的配置,apollo client会去找出所有使用了该配置的bean和字段,然后通过反射的方式来刷新字段的值。 (其实到这里,我已经将apollo配置中心的动态刷新原理给讲出来了)- 最后,apollo client会更新本地缓存,apollo通过本地文件的方式来缓存最新的配置数据。这个缓存设计主要还是为了防止apollo配置中心服务端“失联”导致整个服务不可用 。当apollo服务端宕机时,apollo client可以通过读取本地文件中缓存的配置来保证服务的高可用。
另外图中还有个meta service服务,这个服务其实就是对注册中心做了一层封装,它将注册中心的服务发现的能力变成了HTTP接口服务,而这种设计的目的则是为了兼容不同语言开发的apollo client ,否则的话,apollo配置中心就只有基于java语言开发的apollo client。所以,这里我们可以将meta service服务看做是整个项目的通讯录 ,portal service利用这个“通讯录”去查找admin service服务的地址,apollo client利用这个“通讯录”查找config servcie的地址。
如果将架构中的所有设计都讲一遍,这篇文章肯定是放不下的,而且也没必要这样做,因为apollo的GitHub wiki中对apollo的架构设计细节讲的已经非常清楚了,大家可自行了解。这里我主要讲讲apollo client是如何实现刷新配置而不需要重启服务器的,下面我将通过源码阅读来帮助大家理解其中的原理。
Apollo动态刷新配置的源码分析
首先,我们去apollo配置中心更新配置,如下图所示:

注意:在更新配置后,一定要点击发布,配置才会生效。当我们点击发布后,客户端就会去刷新应用的配置了,刷新完成后,apollo会在日志中打印这样一条日志,如下图所示:

然后我们看下打印这条日志的代码,并在对应地方打上断点

再次去配置中心页面修改apollo的配置并且发布配置,这样就会触发断点,方便我们了解apollo client刷新配置的流程,如下图所示:

接下来我们重点看堆栈信息中的onChange方法:
@Override
public void onChange(ConfigChangeEvent changeEvent) {
// 找到所有值发生了变化的配置
Set<String> keys = changeEvent.changedKeys();
if (CollectionUtils.isEmpty(keys)) {
return;
}
// 接下来就是通过for循环批量处理发生了变化的配置
for (String key : keys) {
// 下面的代码是为了找出在哪些bean的哪些字段中使用了该配置
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
if (targetValues == null || targetValues.isEmpty()) {
continue;
}
// 下面就是通过反射更新每一个使用了该配置的字段的值
for (SpringValue val : targetValues) {
// 更新字段的值
updateSpringValue(val);
}
}
}
大家需要重点关注的我在上面代码中添加的注释,接下来,我们重点看下updateSpringValue(val)这行代码的处理逻辑。
private void updateSpringValue(SpringValue springValue) {
try {
// 这里是在拿到该配置最新的值
Object value = resolvePropertyValue(springValue);
// 接下来则是刷新相关配置的值,也是我们要关注的重点。
springValue.update(value);
// 更新成功打印日志
logger.info("Auto update apollo changed value successfully, new value: {}, {}", value,
springValue);
} catch (Throwable ex) {
logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex);
}
}
接下来,我们重点关注springValue.update(value);这行代码的逻辑,了解apollo是如何更新配置的,具体代码如下:
public void update(Object newVal) throws IllegalAccessException, InvocationTargetException {
if (isField()) {
// 这里是在修改字段的值
injectField(newVal);
} else {
// 这里是为了处理方法注入,因为@value注解是可以放在set方法上的
injectMethod(newVal);
}
}
// 下面这个方法就是在修改字段的值
private void injectField(Object newVal) throws IllegalAccessException {
Object bean = beanRef.get();
if (bean == null) {
return;
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
field.set(bean, newVal);
field.setAccessible(accessible);
}
我们认真看下injectField方法的逻辑,是不是有点眼熟呢?没错,正是我们在学习java基础时接触过的反射。这里我插一嘴,凡是搞框架开发的,反射和动态代理是java中绕不过的两座大山,希望各位小伙伴要认真掌握这两门技术,要不然你是看不懂框架源码的。
到现在,我们已经大致知道了apollo是如何刷新配置的,同时我也在注释中提到了apollo依赖一个映射表(类似于hashmap之类的东西)去找到所有使用了某个配置的bean和相关字段。
// 下面的代码是为了找出在哪些bean的哪些字段中使用了该配置
Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
需要注意的是:这个映射表肯定是必须的,如果没有这个映射表,那apollo client又怎么知道哪些地方使用了这个配置呢?这点大家可以好好想想。
接下来我想问下大家,关于配置和使用该配置的bean以及字段的映射关系表是如何建立的呢?
为了解决这个问题,我们就需要重点看下springValueRegistry这个对象是如何存放配置和配置使用点的映射关系了。关于这个对象的信息如下图所示:

实际上关于这个对象,apollo利用了Google开源guice框架,这个框架可以看做是轻量级的依赖注入框架,跟spring框架一样,它也可以看作是一个容器,而且使用它也不需要我们去new去对象,guice框架和spring框架一样通过反射的方式为我们创建对象,并且注入依赖。
接下来,我们重点看下SpringValueRegistry类的register方法,因为这个方法含有用来存放配置和使用点映射关系的代码逻辑。
public void register(BeanFactory beanFactory, String key, SpringValue springValue) {
if (!registry.containsKey(beanFactory)) {
synchronized (LOCK) {
if (!registry.containsKey(beanFactory)) {
registry.put(beanFactory, Multimaps.synchronizedListMultimap(LinkedListMultimap.create()));
}
}
}
registry.get(beanFactory).put(key, springValue);
// lazy initialize
if (initialized.compareAndSet(false, true)) {
initialize();
}
}
所以,接下来我们在这个方法中打一个断点,并且以调试模式重启应用,看看apollo是如何收集映射关系的。

同样的,我们可以通过分析堆栈信息最终定位到存放映射关系的代码入口,如下图所示:

也就是说入口是在com.ctrip.framework.apollo.spring.annotation.ApolloProcessor类的postProcessBeforeInitialization方法中,我们还可以看到这个ApolloProcessor类实现org.springframework.beans.factory.config.BeanPostProcessor接口,也就是说apollo的配置和使用点的映射关系是在bean的初始化前就已经建立了。实际上到这里,关于apollo如何建立配置和使用点的映射关系这块儿我也给大家讲的差不多了。
实际上,到这里关于apollo配置中心如何实现动态刷新配置的源码解读我们就讲完了。
模拟apollo实现动态刷新配置的效果
为了方便大家理解apollo动态刷新配置的整个流程,我下面开发了一个mini apollo项目来模拟apollo建立配置-使用点的映射关系以及动态刷新配置的过程。
建立配置和使用点的映射关系
同样的,我也写了一个类似的ApolloProcessor去建立配置和使用点的映射关系,代码如下:
package com.lizemin.processor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
/**
* @author lzm
* @date 2026/1/17 15:43
* @description
*/
@Component
public class ApolloProcessor implements BeanPostProcessor {
/**
* 建立配置和使用点的映射关系
*
* @param bean bean实例
* @param beanName bean名字
* @return bean
* @throws BeansException ex
*/
@SneakyThrows
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 收集所有添加了@value注解的字段
ArrayList<Field> fields = new ArrayList<>();
ReflectionUtils.doWithFields(bean.getClass(), fields::add, field -> field.isAnnotationPresent(Value.class));
for (Field field : fields) {
ConfigPosition position = new ConfigPosition();
position.setBean(bean);
position.setField(field);
String configName = field.getAnnotation(Value.class).value();
if (configName.startsWith("${")) {
configName = StringUtils.substringBetween(configName, "${", "}");
position.setConfigName(configName);
field.setAccessible(true);
Object value = field.get(bean);
position.setConfigValue(value);
// 添加配置和使用点的映射关系
ConfigUtil.addConfigPositionMapping(configName, position);
}
}
return bean;
}
}
现在有了配置和配置使用点的映射关系表后,接下来我们就可以根据这个表去动态刷新配置的值了。
使用反射刷新配置
下面我开发了两个接口,它们分别是:
- 用来获取项目当前的所有配置
- 用来刷新项目的配置,模拟apollo拿到最新的配置后去刷新项目中所有标记了
@Value注解的字段的值的过程,因此这个接口是关键
package com.lizemin.controller;
import com.google.common.collect.ListMultimap;
import com.lizemin.processor.ConfigPosition;
import com.lizemin.processor.ConfigUtil;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
/**
* @author lzm
* @date 2026/1/17 15:41
* @description
*/
@RequestMapping("/apollo")
@RestController
public class ApolloController {
/**
* 获取当前配置
*
* @return 当前配置
*/
@GetMapping("/getCurrentConfig")
public LinkedHashMap<String, Object> getCurrentConfig() {
LinkedHashMap<String, Object> configValueMap = new LinkedHashMap<>();
ListMultimap<String, ConfigPosition> configPositionMapping = ConfigUtil.getConfigPositionMapping();
configPositionMapping.forEach((configName, configPosition) -> configValueMap.put(configName, configPosition.getConfigValue()));
return configValueMap;
}
/**
* 刷新当前配置
*
* @param newConfigs 最新的配置,假设是从apollo config service获取到的
* @return 刷新结果
*/
@PostMapping("/refreshConfig")
public String refreshConfig(@RequestBody HashMap<String, Object> newConfigs) {
newConfigs.forEach((configName, newConfigValue) -> {
// 找到使用了该配置的使用点
List<ConfigPosition> configPositions = ConfigUtil.getConfigPositionMapping(configName);
// 使用反射刷新字段的值
for (ConfigPosition configPosition : configPositions) {
Object bean = configPosition.getBean();
Field field = configPosition.getField();
field.setAccessible(true);
ReflectionUtils.setField(field, bean, newConfigValue);
}
});
return "success";
}
}
另外,我还在本地写了一个测试接口,用来测试项目中的配置是否真的刷新成功了。代码如下:
package com.lizemin.controller;
import com.lizemin.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lzm
* @date 2026/1/17 15:41
* @description
*/
@RequestMapping("/user")
@RestController
public class UserController {
@Value("${csdn.name}")
private String username;
@Value("${csdn.age}")
private Integer age;
@GetMapping("/getUser")
public User getUser() {
return new User(username, age);
}
}
下面是项目中的配置文件application.yml
server:
port: 8402
csdn:
name: 冬天吃雪糕
age: 18
到此,咱们的准备工作就做完了,下面咱们开始测试,检验下咱们刚才开发的代码是否具有动态刷新配置的能力。
测试动态刷新配置的效果
首先,咱们请求/getUser接口,结果如下:

可以看到它目前用的是application.yml配置文件中的配置。
接下来,咱们请求/getCurrentConfig接口,结果如下:

可以看到,它确实有获取当前配置的能力。
接下来就是好戏登场了,咱们请求/refreshConfig接口来刷新配置,结果如下:

接下来,咱们再次请求/getUser接口,可以看到配置age字段的值已经从18变成了188,如下图所示:

这说明咱们的代码是有效的,已经成功实现了动态刷新配置的效果。怎么样?是不是很神奇呢?哈哈哈。
到这里,关于apollo客户端是如何动态刷新配置的咱们就讲完了。
本文完整示例代码的地址

最后的总结
今天这篇文章主要是给大家分享了Apollo配置中心是如何实现动态刷新配置而不需要重启服务器的。一开始咱们是通过调试和源码分析的方式来理解apollo client刷新配置的流程,然后咱们还开发了一个mini apollo项目去模拟apollo client更新项目中的配置。
这其中的关键在于两件事,一是要拿到配置和配置使用点的映射表,二是通过反射去刷新字段的值。相信看完这篇文章,大家会对Apollo的工作原理有更加深刻的认识。
创作不易,觉得有收获的小伙伴可以点个赞,您的鼓励就是我最大的动力!
更多推荐

所有评论(0)