背景介绍

相信应该有不少小伙伴在工作中接触过Apollo配置中心,毕竟在国内用的最多的两款配置中心就是Apollo和nacos,记得我之前从事过的某家大公司就用到了它。我们都知道像Apollo和nacos这种现代化的配置中心是支持配置动态刷新的,整个过程中不需要重启服务器、接下来,我要问大家一个问题:这种动态刷新配置,而不需要重启服务器的能力是如何实现的?

接下来我将带大家解决这个问题,话不多话,咱们开始吧!需要注意的是:本篇文章基于Apollo配置中心讲解,至于nacos的动态刷新配置是如何实现的就留给大家当家庭作业了。

Apollo的项目架构

如果我们要讲Apollo配置中心动态刷新配置的原理,就不得不给大家介绍下Apollo的整个项目架构,如下图所示:
在这里插入图片描述
我们一边看着上面的架构图,一边回忆下我们平时是怎么使用apollo配置中心的。大致过程如下:

  1. 首先,我们进入到portal service的前端页面,也就是apollo的管理页面,然后去添加或者修改或者删除某个配置。当我们做这个操作时,浏览器实际上会给portal service服务发送请求,然后portal service会去调用admin service服务的接口去操作config db数据库中的配置。
  2. 然后我们应用中的apollo client会通过长轮训(以及定时任务作为fallback)调用config service服务的接口获取到最新的配置。
  3. apollo client拿到最新的配置后,会去比对哪些配置发生了变化,将那些发生了变化的配置找出来。对于这些已经发生了变化的配置,apollo client会去找出所有使用了该配置的bean和字段,然后通过反射的方式来刷新字段的值。 (其实到这里,我已经将apollo配置中心的动态刷新原理给讲出来了)
  4. 最后,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;
    }

}

现在有了配置和配置使用点的映射关系表后,接下来我们就可以根据这个表去动态刷新配置的值了。

使用反射刷新配置

下面我开发了两个接口,它们分别是:

  1. 用来获取项目当前的所有配置
  2. 用来刷新项目的配置,模拟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的工作原理有更加深刻的认识。

创作不易,觉得有收获的小伙伴可以点个赞,您的鼓励就是我最大的动力!

更多推荐