携程Apollo配置中心介绍以及实现细节中我们介绍过配置中心的实现细节,但是有一块内容没有介绍,配置中心客户端。作为一个业务系统,假设我是一个java或者C++、python应用,我如何从配置中心拉取配置数据,并且到配置发生改变时,业务系统要能立即感知到配置发生了变更,实现配置信息的热加载。

前段时间因为公司需要,然后开发了一个配置中心组件,主要包含:

配置中心服务端:开发语言是go,采用了sqlite数据库,提供了restful接口以及cli接口;

配置中心前端:基于react + antd + umi + json schema;

由于是公司内部代码,无法开源,所以这里也不进行详细介绍。公司没有开发配置中心客户端的计划,但是个人认为配置中心客户端应该是配置中心各个模块中较为重要的一环,并且开发难度也是最高的。

这里我们将参考Apollo客户端的实现,介绍配置中心java客户端是如何实现。

一 配置中心客户端

1.1 客户端设计

首先我们回顾一下Apollo配置中心客户端的设计:

配置中心java客户端实现_java

  • 客户端和服务端保持了一个http长轮询,从而能第一时间获得配置更新的推送;(通过Http Long Polling实现)
  • 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置;
  • 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中;
  • 客户端会把从服务端获取到的配置在本地文件系统缓存一份在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置;
  • 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知;

我们自己实现的配置中心目前没有提长轮询的接口(通过spring提供的DeferredResult实现长轮询服务端推送消息),因此只能采用定时轮询从配置中心拉取最新配置。

同时我们配置中心在实现的时候,并没有采用命名空间,组的设计思路,因此获取配置的接口只需要配置实例id一个参数。

这里介绍一下我们获取配置的接口:

http://127.0.0.1:8009/api-cmc/instance/json-schema/items?instanceId=1713136831132860416

instance为配置实例id,接口返回数据格式:

{
    "code": "00000",
    "data": {
        "instanceId": "1713136831132860416",
        "templateId": "1713136481888894976",
        "jsonSchema": "{\"type\":\"object\",\"displayType\":\"row\",\"properties\":{\"test.boolean\":{\"title\":\"test.boolean\",\"type\":\"boolean\",\"widget\":\"checkbox\",\"default\":true},\"test.string\":{\"title\":\"test.string\",\"type\":\"string\",\"props\":{},\"default\":\"字符串\"},\"test.integer\":{\"title\":\"test.integer\",\"type\":\"number\",\"default\":10},\"test.long\":{\"title\":\"test.long\",\"type\":\"number\",\"default\":100},\"test.double\":{\"title\":\"test.double\",\"type\":\"number\",\"description\":\"\",\"default\":10.2},\"test.array\":{\"title\":\"test.array\",\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"title\":\"name\",\"type\":\"string\",\"props\":{}}}},\"props\":{}},\"test.map\":{\"title\":\"test.map\",\"type\":\"object\",\"properties\":{\"name\":{\"title\":\"输入框\",\"type\":\"string\",\"default\":\"zhengy\",\"props\":{}}}}},\"labelWidth\":120}",
        "configuration": "{\"test.boolean\":true,\"test.string\":\"sasddadssddsds\",\"test.integer\":10,\"test.long\":100,\"test.double\":10.2,\"test.array\":[{\"name\":\"1115eee\"},{\"name\":\"111\"}],\"test.map\":{\"name\":\"dsdsdssdss\"}}"
    },
    "err": "",
    "msg": "操作成功"
}

其中configuration是我们想要的配置信息,这里的数据是json序列化后的,我们反序列化处理:

{
    "test.boolean": true,
    "test.string": "sasddadssddsds",
    "test.integer": 10,
    "test.long": 100,
    "test.double": 10.2,
    "test.array": [{
        "name": "1115eee"
    }, {
        "name": "111"
    }],
    "test.map": {
        "name": "dsdsdssdss"
    }
}

二 、客户端软件架构设计

我们的客户端设计依然参考了Apollo客户端,相当于是Apollo删减版,并加入了一些优化,整体程序架构基本相同。

2.1 Config接口

既然是配置中心,那么我们首先想到的就是抽象出配置接口Config,并且提供以下功能:

  • 根据配置key获取配置值;
  • 根据配置key获取配置值,返回字符串类型;
  • 根据配置key获取配置值,返回各种数字类型;
  • 根据配置key获取配置值,返回字符串数组类型;
  • 根据配置key获取配置值,返回日期类型;
  • 获取所有的配置key;

此外,如果我们配置发生变更了,是否需要通知业务系统,因此还需要提供配置订阅功能:

  • 新增配置变更监听器;
  • 移除配置变更监听器;

最终实现了Conifg及其子类:

配置中心java客户端实现_客户端_02

 抽象类AbstractConfig ,实现 Config 接口,功能如下:

  • 实现了所有的获得属性值的方法,并利用google Cache缓存属性key->value;
  • 配置发生改变时,异步通知监听器;
  • 计算属性变化等等特性;

DefaultConfig ,实现 RepositoryChangeListener 接口,继承 AbstractConfig 抽象类,默认 Config 实现类,功能如下:

  • 构造时,指定配置仓库,并且订阅了配置仓库发生改变事件;
  • 配置仓库发生改变时,计算发生变化的属性集合,异步通知监听器;

2.2 ConfigRepository

配置仓库接口主要提供了以下功能:

  • Properties getConfig();

此外,如果我们配置仓库发生变更了,是否需要通知业务系统,因此还需要提供配置仓库订阅功能:

  • 新增配置仓库变更监听器;
  • 移除配置仓库变更监听器;

最终实现了ConfigRepository及其子类:

配置中心java客户端实现_初始化_03

 抽象类AbstractConfigRepository ,实现 ConfigRepository接口,功能如下:

  • 定义了sync抽象方法,用于将配置同步到缓存;

RemoteConfigRepository ,实现 AbstractConfigRepository 抽象类,远程配置 Repository ,功能如下:

  • 实现了从配置中心服务端拉取配置,并缓存在内存中。(开启定时任务 + 实时同步配置到缓存)
  • 配置发生变更时,发布配置仓库改变事件,触发配置仓库变更监听器;

LocalFileConfigRepository ,实现 RepositoryChangeListener 接口,继承 AbstractConfigRepository 抽象类,本地文件配置 Repository 实现类,功能如下:

  • 构造时,指定上游配置仓库,并且订阅了上游配置仓库发生改变事件;
  • 上游配置仓库发生变更时,上游配置仓库发布配置仓库改变事件,触发配置仓库变更监听器;

2.3 FuhsiConfigService

介绍完了Config以及ConfigRepository,我们再来介绍以下FuhsiConfigService,FuhsiConfigService是配置中心客户端的门户,提供了获取Config对象的单例方法。

三、客户端集成Spring

前面介绍的内容还是比较简单的,集成Spring这一块相对来说还是比较复杂的,这里我们只介绍Spring基于java bean的集成方式。

我们回顾一下,Spring中读取配置文件的注解一般使用的有以下两个:

因此我们需要重写这两个注解的处理器,保存使用这些注解的bean的相关信息,使用配置中心的配置去初始化这些的Bean。

Spring在启动的时候会加载外部化配置的资源文件,因此我们需要实现我们自己的配置源PropertySource ,配置源的配置数据来自我们之前介绍的的Config对象;

Spring抽象了一个Environment来表示Spring应用程序环境配置,它整合了各种各样的外部化配置的资源类型,并且提供统一访问的方法。因此我们需要将我们的配置添加到Environment中,同时需要将我们的配置源设置为最高,这样才能首先拿到我们配置源中的配置项。

当配置发生改变时,我们需要订阅配置发生变化,实现bean的热加载。

3.1 FuhsiAbstractProcessor

抽象处理器,实现了BeanPostProcessor:

package com.chaos.framework.fuhsi.client.spring.processor;

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * {@link FuhsiAbstractProcessor}后缀处理器,实现了{@link BeanPostProcessor}
 * 抽象一些公共方法
 *
 * @author zy
 * @since 2021/10/9
 */
public abstract class FuhsiAbstractProcessor implements BeanPostProcessor, PriorityOrdered {

    /**
     * Bean初始化后调用
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName  当前正在初始化的Bean名称
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }

    /**
     * 在Spring容器Bean初始化前调用,注意这里的初始化是指对象创建之后,属性赋值之前(依赖注入)
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName  当前正在初始化的Bean名称
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        Class clazz = bean.getClass();

        // 遍历所有字段
        for (Field field : getFields(clazz)) {
            processBeanField(bean, beanName, field);
        }

        // 遍历所有方法
        for (Method method : getMethods(clazz)) {
            processBeanMethod(bean, beanName, method);
        }

        return bean;
    }

    /**
     * 处理Bean中指定字段
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     * @param field 当前bean的字段
     */
    protected abstract void processBeanField(Object bean, String beanName, Field field);

    /**
     * 处理Bean中指定方法
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     * @param method 当前bean的方法
     */
    protected abstract void processBeanMethod(Object bean, String beanName, Method method);

    /**
     * 获取所有的字段、包含父类以及私有字段
     *
     * @param clazz 类型
     * @return 字段列表
     */
    List<Field> getFields(Class clazz) {
        List<Field> res = new ArrayList<>();
        ReflectionUtils.doWithFields(clazz, res::add);
        return res;
    }

    /**
     * 获取所有的方法,包含父类以及私有方法
     *
     * @param clazz 类型
     * @return 方法列表
     */
    List<Method> getMethods(Class clazz) {
        List<Method> res = new ArrayList<>();
        ReflectionUtils.doWithMethods(clazz, res::add);
        return res;
    }

    /**
     * 最低优先级
     */
    @Override
    public int getOrder() {
        //make it as late as possible
        return Ordered.LOWEST_PRECEDENCE;
    }
}

3.2 SpringValueProcessor

处理Spring中的Value注解,將属性或者方法中包含Value注解的Bean信息添加到属性注册表。

package com.chaos.framework.fuhsi.client.spring.processor;

import com.chaos.framework.fuhsi.core.spi.InjectorUtil;
import com.chaos.framework.fuhsi.client.spring.property.SpringValue;
import com.chaos.framework.fuhsi.client.spring.property.SpringValueRegistry;
import com.chaos.framework.fuhsi.core.util.spring.PlaceholderResolverUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Set;

/**
 * {@link SpringValueProcessor}: Spring {@link @Value}注解处理器
 *
 * <p>
 * 处理Spring中的Value注解,將属性或者方法中包含Value注解的Bean信息添加到属性注册表。
 * <a href="javascript:void(0)" target="_blank">java的Spring中@Value注解的使用</a>
 *
 * @author zy
 * @since 2021/8/11
 */
@Slf4j
public class SpringValueProcessor extends FuhsiAbstractProcessor {
    /**
     * 属性注册表
     */
    private SpringValueRegistry springValueRegistry;

    /**
     * 构造函数
     */
    public SpringValueProcessor() {
        springValueRegistry = InjectorUtil.getInstance(SpringValueRegistry.class);
    }

    /**
     *  在Spring容器Bean初始化前调用,注意这里的初始化是指对象创建之后,属性赋值之前(依赖注入)
     *  这里主要做了一个操作就是,遍历当前bean的所有属性和方法,查看是否使用了{@link @Value}注解
     *  如果使用了{@link @Value}注解,向springValueRegistry添加属性key => SpringValue(对应的bean信息)
     *  然后从配置中心获取配置信息,更新bean中属性
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName  当前正在初始化的Bean名称
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        super.postProcessBeforeInitialization(bean, beanName);
        return bean;
    }


    /**
     * 处理Bean中指定字段
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     * @param field 当前bean的字段
     */
    @Override
    protected void processBeanField(Object bean, String beanName, Field field) {
        // 获取字段注解@Value
        Value value = field.getAnnotation(Value.class);
        if (value == null) {
            return;
        }

        // 解析占位符 获取所有属性名
        Set<String> keys = PlaceholderResolverUtil.extractPlaceholderKeys(value.value());
        if (keys.isEmpty()) {
            return;
        }

        // 遍历占位符中属性名
        for (String key : keys) {
            SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field);
            springValueRegistry.register(key, springValue);
            log.debug("monitoring {}", springValue);
        }
    }

    /**
     * 处理Bean中指定方法
     *
     * @param bean 当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     * @param method 当前bean的方法
     */
    @Override
    protected void processBeanMethod(Object bean, String beanName, Method method) {
        // 获取方法注解@Value
        Value value = method.getAnnotation(Value.class);
        if (value == null) {
            return;
        }

        //skip Configuration bean methods
        if (method.getAnnotation(Bean.class) != null) {
            return;
        }

        // 解析占位符 获取所有属性名
        Set<String> keys = PlaceholderResolverUtil.extractPlaceholderKeys(value.value());
        if (keys.isEmpty()) {
            return;
        }

        // 参数不是1个
        if (method.getParameterTypes().length != 1) {
            log.error("ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters",
                    bean.getClass().getName(), method.getName(), method.getParameterTypes().length);
            return;
        }

        // 遍历占位符中属性名
        for (String key : keys) {
            SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method);
            springValueRegistry.register(key, springValue);
            log.debug("monitoring {}", springValue);
        }
    }
}

3.3 SpringConfigurationPropertiesProcessor

处理Spring中的ConfigurationProperties注解,將类或者方法中包含Value注解的ConfigurationProperties信息添加到属性注册表。

 

package com.chaos.framework.fuhsi.client.spring.processor;


import com.chaos.framework.fuhsi.core.spi.InjectorUtil;
import com.chaos.framework.fuhsi.client.spring.property.SpringValue;
import com.chaos.framework.fuhsi.client.spring.property.SpringValueRegistry;
import com.chaos.framework.fuhsi.core.util.spring.PlaceholderResolverUtil;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;

/**
 * {@link SpringConfigurationPropertiesProcessor}: {@link @ConfigurationProperties}注解处理器
 *
 * <p>
 *  处理Spring中的ConfigurationProperties注解,將类或者方法中包含Value注解的ConfigurationProperties信息添加到属性注册表。
 * <a href="javascript:void(0)" target="_blank">注解@ConfigurationProperties使用方法</a>
 *
 * @author zy
 * @since 2021/10/9
 */
@Slf4j
public class SpringConfigurationPropertiesProcessor extends FuhsiAbstractProcessor implements BeanFactoryPostProcessor {
    /**
     * 属性注册表
     */
    private SpringValueRegistry springValueRegistry;

    /**
     * 保存使用@ConfigurationProperties修饰的方法或者类的信息
     * key为方法返回的bean名称、value为@ConfigurationProperties prefix值
     */
    private HashMap<String, String> configurationPropertiesMap = Maps.newHashMap();

    /**
     * 构造函数
     */
    public SpringConfigurationPropertiesProcessor() {
        springValueRegistry = InjectorUtil.getInstance(SpringValueRegistry.class);
    }

    /**
     * 在Spring容器Bean初始化前调用,注意这里的初始化是指对象创建之后,属性赋值之前(依赖注入)
     * 这里主要做了一个操作就是,查看当前bean,查看是否使用了{@link @ConfigurationProperties}注解
     * 如果使用了{@link @ConfigurationProperties}注解,遍历所有属性,
     * 向springValueRegistry添加属性key => SpringValue(对应的bean信息)
     * 然后从配置中心获取配置信息,更新bean中属性
     *
     * @param bean     当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        super.postProcessBeforeInitialization(bean, beanName);
        return bean;
    }

    /**
     * 处理类或者方法上的ConfigurationProperties注解
     *
     * @param bean     当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     * @param field    当前bean的字段
     */
    @Override
    protected void processBeanField(Object bean, String beanName, Field field) {
        // 未使用
        if(!configurationPropertiesMap.containsKey(beanName)){
            return;
        }

        // 获取prefix值
        String prefix = configurationPropertiesMap.get(beanName);
        String key = prefix + "." + field.getName();
        String placeholder = PlaceholderResolverUtil.PLACEHOLDER_PREFIX + key + PlaceholderResolverUtil.PLACEHOLDER_SUFFIX;

        SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field);
        springValueRegistry.register(key, springValue);
        log.debug("monitoring {}", springValue);
    }

    /**
     * 无
     *
     * @param bean     当前正在初始化的Bean对象
     * @param beanName 当前正在初始化的Bean名称
     * @param method   当前bean的方法
     */
    @Override
    protected void processBeanMethod(Object bean, String beanName, Method method) { }

    /**
     * BeanFactory后置处理器
     *
     * 获取所有使用@ConfigurationProperties注解的方法或者类
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 获取所有使用@ConfigurationProperties注解bean名称  包含类或者方法上使用@ConfigurationProperties注解的bean
        String[] beanNames = beanFactory.getBeanNamesForAnnotation(ConfigurationProperties.class);

        // 遍历bean
        for (String beanName : beanNames) {
            ConfigurationProperties configurationProperties = beanFactory.findAnnotationOnBean(beanName, ConfigurationProperties.class);
            String prefix = configurationProperties.prefix();
            configurationPropertiesMap.put(beanName, prefix);
        }
    }
}

3.4 FuhsiPropertySourceProcessor

注册配置源、从配置源获取配置信息,具体功能:

  •  创建Config对象,用于从配置中心/本地缓存获取配置信息;
  • 添加监听器,监听Config对象配置发生变更事件,实现Spring配置的自动更新;
  • 将配置信息转为PropertySource对象,加载到Spring的Environment对象;
package com.chaos.framework.fuhsi.client.spring.processor;

import com.chaos.framework.fuhsi.client.config.Config;
import com.chaos.framework.fuhsi.client.consts.Constants;
import com.chaos.framework.fuhsi.client.FuhsiConfigService;
import com.chaos.framework.fuhsi.client.spring.property.FuhsiPropertySource;
import com.chaos.framework.fuhsi.client.spring.listener.AutoUpdateConfigChangeListener;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;


/**
 * 注册配置源、从配置源获取配置信息
 *
 * <p>
 * 创建Config对象,用于从配置中心/本地缓存获取配置信息
 * 添加监听器,监听Config对象配置发生变更事件,实现Spring配置的自动更新
 * 将配置信息转为PropertySource对象,加载到Spring的Environment对象
 *
 * @author zy
 * @since 2021/8/11
 */
public class FuhsiPropertySourceProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered {
    /**
     * 保存Spring环境配置信息
     *
     * <p>
     * Spring抽象了一个Environment来表示Spring应用程序环境配置,它整合了各种各样的外部化配置的资源类型,并且提供统一访问的方法。
     */
    private ConfigurableEnvironment environment;

    /**
     * 在 spring容器加载了BeanDefinition之后,bean实例化之前执行的。
     *
     * @param beanFactory Spring容器
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 始化配置源,并添加到environment
        initializePropertySource(beanFactory);
    }

    /**
     * 初始化配置源,并添加到environment
     *
     * @param beanFactory Spring容器
     */
    private void initializePropertySource(ConfigurableListableBeanFactory beanFactory) {
        // 判断是否已经包含了该配置源
        if (environment.getPropertySources().contains(Constants.PROPERTY_SOURCE_NAME)) {
            return;
        }

        // 获取Config对象
        Config config = FuhsiConfigService.getConfig();

        // 订阅配置发生改变事件  从而实现bean的热加载   由于第一次从配置中心获取配置时,还为初始化属性注册表,所以即使触发了配置改变事件,其实无意义
        config.addChangeListener(new AutoUpdateConfigChangeListener(
                environment, beanFactory));

        // 创建配置源,并添加到environment
        PropertySource<Config> propertySource = new FuhsiPropertySource(Constants.PROPERTY_SOURCE_NAME, config);

        // 设置propertySource最高优先级  也就是加载key信息时,首先从propertySource获取,获取不到才会从其他配置源获取
        environment.getPropertySources().addFirst(propertySource);
    }

    /**
     * 设置环境
     *
     * @param environment 环境信息
     */
    @Override
    public void setEnvironment(Environment environment) {
        this.environment = (ConfigurableEnvironment) environment;
    }

    /**
     * 设置执行优先级最高
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

 

3.5 AutoUpdateConfigChangeListener

配置变更监听器,实现配置信息的实时更新。

实现原理也很简单,假设spring.port属性发生变更了,则从属性注册表中查找哪个bean引用了这个属性,然后更新bean的属性即可

 

package com.chaos.framework.fuhsi.client.spring.listener;

import com.chaos.framework.fuhsi.client.event.ConfigChangeEvent;
import com.chaos.framework.fuhsi.client.listener.ConfigChangeListener;
import com.chaos.framework.fuhsi.client.spring.property.SpringValueRegistry;
import com.chaos.framework.fuhsi.core.spi.InjectorUtil;
import com.chaos.framework.fuhsi.client.spring.property.SpringValue;
import com.chaos.framework.fuhsi.core.util.spring.PlaceholderResolverUtil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.Map;
import java.util.Set;

/**
 * 配置变更监听器,实现Spring配置信息的实时更新
 * 实现原理也很简单,假设spring.port属性发生变更了,则从属性注册表中查找哪个bean引用了这个属性,然后更新bean的属性即可
 *
 * @author zy
 * @since 2021/8/11
 */
@Slf4j
public class AutoUpdateConfigChangeListener implements ConfigChangeListener {
    /**
     * Spring容器
     */
    private ConfigurableBeanFactory beanFactory;

    /**
     * 属性注册表
     */
    private SpringValueRegistry springValueRegistry;

    /**
     * 构造函数
     *
     * @param environment  Spring环境配置信息
     * @param beanFactory Spring容器
     */
    public AutoUpdateConfigChangeListener(Environment environment, ConfigurableListableBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        this.springValueRegistry = InjectorUtil.getInstance(SpringValueRegistry.class);
    }

    /**
     * 配置发生改变时执行
     */
    @Override
    public void onChange(ConfigChangeEvent event) {
        // 获取发生改变的key
        Set<String> keys = event.changedKeys();

        // 空校验
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }

        // 遍历key
        for (String key : keys) {
            // 获取使用该key的bean信息
            Collection<SpringValue> targetValues = springValueRegistry.get(key);
            if (CollectionUtils.isEmpty(targetValues)) {
                continue;
            }

            // 遍历 更新值
            for (SpringValue springValue : targetValues) {
                updateSpringValue(springValue);
            }
        }
    }

    /**
     * 更新bean的值
     *
     * @param springValue Spring @Value 注解 info
     */
    private void updateSpringValue(SpringValue springValue) {
        try {
            Object value = resolvePropertyValue(springValue);
            springValue.update(value);
            log.info("auto update changed value successfully, new value: {}, {}", value,
                    springValue);
        } catch (Exception ex) {
            log.error("auto update changed value failed, {}", springValue.toString(), ex);
        }
    }

    /**
     * 获取配置值,并转换成目标类型
     *
     * <a href='https://blog.csdn.net/liujianyangbj/article/details/111352703'>Spring的@Value可以注入复杂类型吗?今天教你通过@value注入自定义类型<a/>
     *
     *  @param springValue Spring @Value 注解 info
     */
    private Object resolvePropertyValue(SpringValue springValue) {
        // 获取配置值
        Object value = PlaceholderResolverUtil.resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder());

        // 获取spring类型转换器
        TypeConverter typeConverter = this.beanFactory.getTypeConverter();

        // string -> map类型特殊处理
        if(springValue.getTargetType().equals(Map.class)) {
            Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
            return gson.fromJson(value.toString(), Map.class);
        }

        // 如果是字段
        if (springValue.isField()) {
            value = typeConverter.convertIfNecessary(value, springValue.getTargetType(), springValue.getField());
        } else {
            value = typeConverter.convertIfNecessary(value, springValue.getTargetType(), springValue.getMethodParameter());
        }
        return value;
    }

}

除了上面介绍的这些,配置中心客户端还有一些其他功能相关的类,比如EnableFuhsiConfig、FuhsiConfigRegistrar、以及java spi加载机制等。这里也不做过多的介绍了。

四、源码

fuhsi-cmc-client