2023-08-16  阅读(6)
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/372

Dubbo从 2.7.0 版本开始正式支持配置中心,在服务自省架构中也依赖配置中心完成 Service ID 与 Service Name 的映射。配置中心在 Dubbo 中主要承担两个职责:

  • 外部化配置:目的之一是实现配置的集中式管理,目前已经有很多成熟的专业配置管理系统(例如,携程开源的 Apollo、阿里开源的 Nacos 等),Dubbo 配置中心的目的不是”重复造轮子”,而是保证 Dubbo 能与这些成熟的配置管理系统一起正常工作;
  • 服务治理:负责服务治理规则的存储与通知。

Dubbo 可以同时支持多种配置来源。在 Dubbo 初始化过程中,会从多个来源获取配置,并按照固定的优先级将这些配置整合起来,实现 高优先级的配置覆盖低优先级配置 的效果。这些配置的汇总结果将会参与形成 URL,以及后续的服务发布和服务引用。

Dubbo 目前支持下面四种配置来源,优先级由 1 到 4 逐级降低:

  1. System Properties,即-D 参数;
  2. 外部化配置,也就是本章要介绍的配置中心;
  3. API 接口、注解、XML 配置等编程方式收到的配置,最终得到 ServiceConfig、ReferenceConfig 等对象;
  4. 本地 dubbo.properties 配置文件。

一、核心接口

我们先来看于配置中心相关的一些核心接口。

1.1 Configuration

Configuration 接口是 Dubbo 中所有配置的基础接口,其中定义了根据指定 Key 获取对应配置值的相关方法,如下图所示:

202308162143595331.png

从上图中可以看到,Configuration 针对不同的 boolean、int、String 返回值都有对应的 get() 方法,同时还提供了带有默认值的 get() 方法。这些 get() 方法底层首先调用 getInternalProperty() 方法获取配置值,然后调用 convert() 方法将获取到的配置值转换成返回值的类型之后返回。

下图展示了 Dubbo 中提供的 Configuration 接口实现,包括:SystemConfiguration、EnvironmentConfiguration、InmemoryConfiguration、PropertiesConfiguration、CompositeConfiguration、ConfigConfigurationAdapter 和 DynamicConfiguration。下面我将结合具体代码逐个介绍其实现。

202308162144002832.png

SystemConfiguration & EnvironmentConfiguration

SystemConfiguration 是从 Java Properties 配置(也就是 -D 配置参数)中获取相应的配置项,EnvironmentConfiguration 是从使用环境变量中获取相应的配置。两者的 getInternalProperty() 方法实现如下:

    public class SystemConfiguration implements Configuration {
        public Object getInternalProperty(String key) {
            return System.getProperty(key); // 读取-D配置参数
        }
    }
    
    public class EnvironmentConfiguration implements Configuration {
        public Object getInternalProperty(String key) {
            String value = System.getenv(key);
            if (StringUtils.isEmpty(value)) {
                // 读取环境变量中获取相应的配置
                value = System.getenv(StringUtils.toOSStyleKey(key));
            }
            return value;
        }
    }

InmemoryConfiguration

InmemoryConfiguration 会在内存中维护一个 Map 集合(store 字段),其 getInternalProperty() 方法的实现就是从 store 集合中获取对应配置值:

    // InmemoryConfiguration.java
    
    public class InmemoryConfiguration implements Configuration {
        private Map<String, String> store = new LinkedHashMap<>();
        @Override
        public Object getInternalProperty(String key) {
            return store.get(key);
        }
        // 省略addProperty()等写入store集合的方法
    }

PropertiesConfiguration

PropertiesConfiguration 涉及 OrderedPropertiesProvider,其接口的定义如下:

    // OrderedPropertiesProvider.java
    
    @SPI
    public interface OrderedPropertiesProvider {
        // 用于排序
        int priority();
        // 获取Properties配置
        Properties initProperties();
    }

在 PropertiesConfiguration 的构造方法中,会加载 OrderedPropertiesProvider 接口的全部扩展实现,并按照 priority() 方法进行排序。然后,加载默认的 dubbo.properties.file 配置文件。最后,用 OrderedPropertiesProvider 中提供的配置覆盖 dubbo.properties.file 文件中的配置。

PropertiesConfiguration 的构造方法的具体实现如下:

    // PropertiesConfiguration.java
    
    public PropertiesConfiguration() {
        // 获取OrderedPropertiesProvider接口的全部扩展名称
        ExtensionLoader<OrderedPropertiesProvider> propertiesProviderExtensionLoader = ExtensionLoader.getExtensionLoader(OrderedPropertiesProvider.class);
        Set<String> propertiesProviderNames = propertiesProviderExtensionLoader.getSupportedExtensions();
        if (propertiesProviderNames == null || propertiesProviderNames.isEmpty()) {
            return;
        }
        // 加载OrderedPropertiesProvider接口的全部扩展实现
        List<OrderedPropertiesProvider> orderedPropertiesProviders = new ArrayList<>();
        for (String propertiesProviderName : propertiesProviderNames) {
            orderedPropertiesProviders.add(propertiesProviderExtensionLoader.getExtension(propertiesProviderName));
        }
        // 排序OrderedPropertiesProvider接口的扩展实现
        orderedPropertiesProviders.sort((OrderedPropertiesProvider a, OrderedPropertiesProvider b) -> {
            return b.priority() - a.priority();
        });
        // 加载默认的dubbo.properties.file配置文件,加载后的结果记录在ConfigUtils.PROPERTIES这个static字段中
        Properties properties = ConfigUtils.getProperties();
        // 使用OrderedPropertiesProvider扩展实现,按序覆盖dubbo.properties.file配置文件中的默认配置
        for (OrderedPropertiesProvider orderedPropertiesProvider :
                orderedPropertiesProviders) {
            properties.putAll(orderedPropertiesProvider.initProperties());
        }
        // 更新ConfigUtils.PROPERTIES字段
        ConfigUtils.setProperties(properties);
    }

在 PropertiesConfiguration.getInternalProperty() 方法中,直接从 ConfigUtils.PROPERTIES 这个 Properties 中获取覆盖后的配置信息:

    // PropertiesConfiguration.java
    
    public Object getInternalProperty(String key) {
        return ConfigUtils.getProperty(key);
    }

CompositeConfiguration

CompositeConfiguration 是一个复合的 Configuration 对象, 其核心就是将多个 Configuration 对象组合起来,对外表现为一个 Configuration 对象

CompositeConfiguration 组合的 Configuration 对象都保存在 configList 字段中(LinkedList<Configuration> 集合),CompositeConfiguration 提供了 addConfiguration() 方法用于向 configList 集合中添加 Configuration 对象,如下所示:

    // CompositeConfiguration.java
    
    public void addConfiguration(Configuration configuration) {
        if (configList.contains(configuration)) {
            return; // 不会重复添加同一个Configuration对象
        }
        this.configList.add(configuration);
    }

在 CompositeConfiguration 中维护了一个 prefix 字段和 id 字段,两者可以作为 Key 的前缀进行查询,在 getProperty() 方法中的相关代码如下:

    // CompositeConfiguration.java
    
    public Object getProperty(String key, Object defaultValue) {
        Object value = null;
        if (StringUtils.isNotEmpty(prefix)) { // 检查prefix
            if (StringUtils.isNotEmpty(id)) { // 检查id
                // prefix和id都作为前缀,然后拼接key进行查询
                value = getInternalProperty(prefix + id + "." + key);
            }
            if (value == null) {
                // 只把prefix作为前缀,拼接key进行查询
                value = getInternalProperty(prefix + key);
            }
        } else {
            // 若prefix为空,则直接用key进行查询
            value = getInternalProperty(key);
        }
        return value != null ? value : defaultValue;
    }

在 getInternalProperty() 方法中,会按序遍历 configList 集合中的全部 Configuration 查询对应的 Key,返回第一个成功查询到的 Value 值,如下示例代码:

    // CompositeConfiguration.java
    
    public Object getInternalProperty(String key) {
        Configuration firstMatchingConfiguration = null;
        for (Configuration config : configList) { // 遍历所有Configuration对象
            try {
                if (config.containsKey(key)) { // 得到第一个包含指定Key的Configuration对象
                    firstMatchingConfiguration = config; 
                    break;
                }
            } catch (Exception e) {
                logger.error("...");
            }
        }
        if (firstMatchingConfiguration != null) { // 通过该Configuration查询Key并返回配置值
            return firstMatchingConfiguration.getProperty(key);
        } else {
            return null;
        }
    }

ConfigConfigurationAdapter

Dubbo 通过 AbstractConfig 类来抽象实例对应的配置,如下图所示:

202308162144008643.png

这些 AbstractConfig 实现基本都对应一个固定的配置,也定义了配置对应的字段以及 getter/setter() 方法。例如,RegistryConfig 这个实现类就对应了注册中心的相关配置,其中包含了 address、protocol、port、timeout 等一系列与注册中心相关的字段以及对应的 getter/setter() 方法,来接收用户通过 XML、Annotation 或是 API 方式传入的注册中心配置。

ConfigConfigurationAdapter 是 AbstractConfig 与 Configuration 之间的适配器 ,它会将 AbstractConfig 对象转换成 Configuration 对象。在 ConfigConfigurationAdapter 的构造方法中会获取 AbstractConfig 对象的全部字段,并转换成一个 Map 集合返回,该 Map 集合将会被 ConfigConfigurationAdapter 的 metaData 字段引用。相关示例代码如下:

    // ConfigConfigurationAdapter.java
    
    public ConfigConfigurationAdapter(AbstractConfig config) {
        // 获取该AbstractConfig对象中的全部字段与字段值的映射
        Map<String, String> configMetadata = config.getMetaData();
        metaData = new HashMap<>(configMetadata.size());
        // 根据AbstractConfig配置的prefix和id,修改metaData集合中Key的名称
        for (Map.Entry<String, String> entry : configMetadata.entrySet()) {
            String prefix = config.getPrefix().endsWith(".") ? config.getPrefix() : config.getPrefix() + ".";
            String id = StringUtils.isEmpty(config.getId()) ? "" : config.getId() + ".";
            metaData.put(prefix + id + entry.getKey(), entry.getValue());
        }
    }

在 ConfigConfigurationAdapter 的 getInternalProperty() 方法实现中,直接从 metaData 集合中获取配置值即可,如下所示:

    // ConfigConfigurationAdapter.java
    
    public Object getInternalProperty(String key) {
        return metaData.get(key);
    }

1.2 DynamicConfiguration

DynamicConfiguration 是对 Dubbo 中动态配置的抽象,其核心方法有下面三类:

  • getProperties()/ getConfig() / getProperty() 方法:从配置中心获取指定的配置,在使用时,可以指定一个超时时间;
  • addListener()/ removeListener() 方法:添加或删除对指定配置的监听器;
  • publishConfig() 方法:发布一条配置信息。

在上述三类方法中,每个方法都用多个重载,其中,都会包含一个带有 group 参数的重载,也就是说 配置中心的配置可以按照 group 进行分组

与 Dubbo 中很多接口类似,DynamicConfiguration 接口本身不被 @SPI 注解修饰(即不是一个扩展接口), 而是在 DynamicConfigurationFactory 上添加了 @SPI 注解,使其成为一个扩展接口

在 DynamicConfiguration 中提供了 getDynamicConfiguration() 静态方法,该方法会从传入的配置中心 URL 参数中,解析出协议类型并获取对应的 DynamicConfigurationFactory 实现,如下所示:

    // DynamicConfiguration.java
    
    static DynamicConfiguration getDynamicConfiguration(URL connectionURL) {
        String protocol = connectionURL.getProtocol();
        DynamicConfigurationFactory factory = getDynamicConfigurationFactory(protocol);
        return factory.getDynamicConfiguration(connectionURL);
    }

DynamicConfigurationFactory 接口的定义如下:

    // DynamicConfigurationFactory.java
    
    @SPI("nop") 
    public interface DynamicConfigurationFactory {
        DynamicConfiguration getDynamicConfiguration(URL url);
        static DynamicConfigurationFactory getDynamicConfigurationFactory(String name) {
            // 根据扩展名称获取DynamicConfigurationFactory实现
            Class<DynamicConfigurationFactory> factoryClass = DynamicConfigurationFactory.class;
            ExtensionLoader<DynamicConfigurationFactory> loader = getExtensionLoader(factoryClass);
            return loader.getOrDefaultExtension(name);
        }
    }

DynamicConfigurationFactory 接口的继承关系以及 DynamicConfiguration 接口对应的继承关系如下:

202308162144021704.png

202308162144028145.png

我们先来看 AbstractDynamicConfigurationFactory 的实现,其中会维护一个 dynamicConfigurations 集合(Map<String, DynamicConfiguration> 类型),在 getDynamicConfiguration() 方法中会填充该集合,实现 缓存 DynamicConfiguration 对象的效果。同时,AbstractDynamicConfigurationFactory 提供了一个 createDynamicConfiguration() 方法给子类实现,来 创建 DynamicConfiguration 对象。

以 ZookeeperDynamicConfigurationFactory 实现为例,其 createDynamicConfiguration() 方法创建的就是 ZookeeperDynamicConfiguration 对象:

    // ZookeeperDynamicConfigurationFactory.java
    
    protected DynamicConfiguration createDynamicConfiguration(URL url) {
        // 这里创建ZookeeperDynamicConfiguration使用的ZookeeperTransporter就是前文在Transport层中针对Zookeeper的实现
        return new ZookeeperDynamicConfiguration(url, zookeeperTransporter);
    }

接下来我们再以 ZookeeperDynamicConfiguration 为例,分析 DynamicConfiguration 接口的具体实现。首先来看 ZookeeperDynamicConfiguration 的核心字段:

  • executor(Executor 类型):用于执行监听器的线程池;
  • rootPath(String 类型):以 Zookeeper 作为配置中心时,配置也是以 ZNode 形式存储的,rootPath 记录了所有配置节点的根路径;
  • zkClient(ZookeeperClient 类型):与 Zookeeper 集群交互的客户端;
  • initializedLatch(CountDownLatch 类型):阻塞等待 ZookeeperDynamicConfiguration 相关的监听器注册完成;
  • cacheListener(CacheListener 类型):用于监听配置变化的监听器;
  • url(URL 类型):配置中心对应的 URL 对象。

在 ZookeeperDynamicConfiguration 的构造函数中,会 初始化上述核心字段 ,具体实现如下:

    // ZookeeperDynamicConfiguration.java
    
    ZookeeperDynamicConfiguration(URL url, ZookeeperTransporter zookeeperTransporter) {
        this.url = url;
        // 根据URL中的config.namespace参数(默认值为dubbo),确定配置中心ZNode的根路径
        rootPath = PATH_SEPARATOR + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";
    
        // 初始化initializedLatch以及cacheListener,在cacheListener注册成功之后,会调用cacheListener.countDown()方法
        initializedLatch = new CountDownLatch(1);
        this.cacheListener = new CacheListener(rootPath, initializedLatch);
    
        // 初始化executor字段,用于执行监听器的逻辑
        this.executor = Executors.newFixedThreadPool(1, new NamedThreadFactory(this.getClass().getSimpleName(), true));
    
        // 初始化Zookeeper客户端
        zkClient = zookeeperTransporter.connect(url);
    
        // 在rootPath上添加cacheListener监听器
        zkClient.addDataListener(rootPath, cacheListener, executor);
        try {
            // 从URL中获取当前线程阻塞等待Zookeeper监听器注册成功的时长上限
            long timeout = url.getParameter("init.timeout", 5000);
            // 阻塞当前线程,等待监听器注册完成
            boolean isCountDown = this.initializedLatch.await(timeout, TimeUnit.MILLISECONDS);
            if (!isCountDown) {
                throw new IllegalStateException("...");
            }
        } catch (InterruptedException e) {
            logger.warn("...");
        }
    }

在上述初始化过程中,ZookeeperDynamicConfiguration 会创建 CacheListener 监听器。在前面章节中,我介绍了 dubbo-remoting-zookeeper 对外提供了 StateListener、DataListener 和 ChildListener 三种类型的监听器。 这里的 CacheListener 就是 DataListener 监听器的具体实现

在 CacheListener 中维护了一个 Map 集合(keyListeners 字段)用于记录所有添加的 ConfigurationListener 监听器,其中 Key 是配置信息在 Zookeeper 中存储的 path,Value 为该 path 上的监听器集合。当某个配置项发生变化的时候,CacheListener 会从 keyListeners 中获取该配置对应的 ConfigurationListener 监听器集合,并逐个进行通知。

该逻辑是在 CacheListener 的 dataChanged() 方法中实现的:

    // CacheListener.java
    
    public void dataChanged(String path, Object value, EventType eventType) {
        if (eventType == null) {
            return;
        }
        if (eventType == EventType.INITIALIZED) {
            // 在收到INITIALIZED事件的时候,表示CacheListener已经成功注册,会释放阻塞在initializedLatch上的主线程
            initializedLatch.countDown();
            return;
        }
        if (path == null || (value == null && eventType != EventType.NodeDeleted)) {
            return;
        }
    
        if (path.split("/").length >= MIN_PATH_DEPTH) { // 对path层数进行过滤
            String key = pathToKey(path); // 将path中的"/"替换成"."
            ConfigChangeType changeType;
            switch (eventType) { // 将Zookeeper中不同的事件转换成不同的ConfigChangedEvent事件
                case NodeCreated:
                    changeType = ConfigChangeType.ADDED;
                    break;
                case NodeDeleted:
                    changeType = ConfigChangeType.DELETED;
                    break;
                case NodeDataChanged:
                    changeType = ConfigChangeType.MODIFIED;
                    break;
                default:
                    return;
            }
            // 使用ConfigChangedEvent封装触发事件的Key、Value、配置group以及事件类型
            ConfigChangedEvent configChangeEvent = new ConfigChangedEvent(key, getGroup(path), (String) value, changeType);
            // 从keyListeners集合中获取对应的ConfigurationListener集合,然后逐一进行通知
            Set<ConfigurationListener> listeners = keyListeners.get(path);
            if (CollectionUtils.isNotEmpty(listeners)) {
                listeners.forEach(listener -> listener.process(configChangeEvent));
            }
        }
    }

CacheListener 中调用的监听器都是 ConfigurationListener 接口实现,如下图所示,这里前面章节介绍的 TagRouter、AppRouter 和 ServiceRouter,它们主要是监听路由配置的变化;还涉及 RegistryDirectory 和 RegistryProtocol 中的四个内部类(AbstractConfiguratorListener 的子类),它们主要监听 Provider 和 Consumer 的配置变化。

202308162144034656.png

这些 ConfigurationListener 实现在前面的课程中已经详细介绍过了,这里就不再重复。ZookeeperDynamicConfiguration 中还提供了 addListener()、removeListener() 两个方法用来增删 ConfigurationListener 监听器,具体实现比较简单,这里就不再赘述。

介绍完 ZookeeperDynamicConfiguration 的初始化过程之后,我们再来看 ZookeeperDynamicConfiguration 中 读取配置、写入配置 的相关操作。相关方法的实现如下:

    // ZookeeperDynamicConfiguration.java
    
    public Object getInternalProperty(String key) {
        // 直接从Zookeeper中读取对应的Key
        return zkClient.getContent(key);
    }
    public boolean publishConfig(String key, String group, String content) {
        // getPathKey()方法中会添加rootPath和group两部分信息到Key中
        String path = getPathKey(group, key);
        // 在Zookeeper中创建对应ZNode节点用来存储配置信息
        zkClient.create(path, content, false);
        return true;
    }

二、配置中心初始化

在上一节,我详细分析了 Configuration 接口以及 DynamicConfiguration 接口的实现,其中 DynamicConfiguration 接口实现是动态配置中心的基础。那 Dubbo 中的动态配置中心是如何启动的呢?我将在本节详细介绍。

2.1 基础配置类

在 DubboBootstrap 初始化的过程中,会调用 ApplicationModel.initFrameworkExts() 方法初始化所有 FrameworkExt 接口实现,继承关系如下图所示:

202308162144043737.png

相关代码片段如下:

    // FrameworkExt.java
    
    public static void initFrameworkExts() {
        Set<FrameworkExt> exts = ExtensionLoader.getExtensionLoader(FrameworkExt.class).getSupportedExtensionInstances();
        for (FrameworkExt ext : exts) {
            ext.initialize();
        }
    }

ConfigManager 用于管理当前 Dubbo 节点中全部 AbstractConfig 对象,其中就包括 ConfigCenterConfig 这个实现的对象,我们通过 XML、Annotation 或是 API 方式添加的配置中心的相关信息(例如,配置中心的地址、端口、协议等),会转换成 ConfigCenterConfig 对象。

Environment 中维护了多个 Configuration 对象 ,具体含义如下。

  • propertiesConfiguration(PropertiesConfiguration 类型):全部 OrderedPropertiesProvider 实现提供的配置以及环境变量或是 -D 参数中指定配置文件的相关配置信息;
  • systemConfiguration(SystemConfiguration 类型):-D 参数配置直接添加的配置信息;
  • environmentConfiguration(EnvironmentConfiguration 类型):环境变量中直接添加的配置信息;
  • externalConfiguration、appExternalConfiguration(InmemoryConfiguration 类型):使用 Spring 框架且将 include-spring-env 配置为 true 时,会自动从 Spring Environment 中读取配置。默认依次读取 key 为 dubbo.properties 和 application.dubbo.properties 到这里两个 InmemoryConfiguration 对象中;
  • globalConfiguration(CompositeConfiguration 类型):用于组合上述各个配置来源;
  • dynamicConfiguration(CompositeDynamicConfiguration 类型):用于组合当前全部的配置中心对应的 DynamicConfiguration。
  • configCenterFirst(boolean 类型):用于标识配置中心的配置是否为最高优先级。

在 Environment 的构造方法中会初始化上述 Configuration 对象,在 initialize() 方法中会将从 Spring Environment 中读取到的配置填充到 externalConfiguration 以及 appExternalConfiguration 中。相关的实现片段如下:

    // Environment.java
    
    public Environment() {
        // 创建上述Configuration对象
        this.propertiesConfiguration = new PropertiesConfiguration();
        this.systemConfiguration = new SystemConfiguration();
        this.environmentConfiguration = new EnvironmentConfiguration();
        this.externalConfiguration = new InmemoryConfiguration();
        this.appExternalConfiguration = new InmemoryConfiguration();
    }
    public void initialize() throws IllegalStateException {
        // 读取对应配置,填充上述Configuration对象
        ConfigManager configManager = ApplicationModel.getConfigManager();
        Optional<Collection<ConfigCenterConfig>> defaultConfigs = configManager.getDefaultConfigCenter();
        defaultConfigs.ifPresent(configs -> {
            for (ConfigCenterConfig config : configs) {
                this.setExternalConfigMap(config.getExternalConfiguration());
                this.setAppExternalConfigMap(config.getAppExternalConfiguration());
            }
        });
    this.externalConfiguration.setProperties(externalConfigurationMap);
        this.appExternalConfiguration.setProperties(appExternalConfigurationMap);
    }

2.2 启动配置中心

完成了 Environment 的初始化之后,DubboBootstrap 接下来会调用 startConfigCenter() 方法启动一个或多个配置中心客户端,核心操作有两个:

  1. 调用 ConfigCenterConfig.refresh() 方法刷新配置中心的相关配置;
  2. 通过 prepareEnvironment() 方法,根据 ConfigCenterConfig 中的配置创建 DynamicConfiguration 对象。
    // DubboBootstrap.java
    
    private void startConfigCenter() {
        Collection<ConfigCenterConfig> configCenters = configManager.getConfigCenters();
        if (CollectionUtils.isEmpty(configCenters)) { // 未指定配置中心
            ... ... // 省略该部分逻辑
        } else {
            for (ConfigCenterConfig configCenterConfig : configCenters) { // 可能配置了多个配置中心
                configCenterConfig.refresh(); // 刷新配置
                // 检查配置中心的配置是否合法           ConfigValidationUtils.validateConfigCenterConfig(configCenterConfig);
            }
        }
        if (CollectionUtils.isNotEmpty(configCenters)) {
            // 创建CompositeDynamicConfiguration对象,用于组装多个DynamicConfiguration对象
            CompositeDynamicConfiguration compositeDynamicConfiguration = new CompositeDynamicConfiguration();
            for (ConfigCenterConfig configCenter : configCenters) {
                // 根据ConfigCenterConfig创建相应的DynamicConfig对象,并添加到CompositeDynamicConfiguration中
    compositeDynamicConfiguration.addConfiguration(prepareEnvironment(configCenter));
            }
            // 将CompositeDynamicConfiguration记录到Environment中的dynamicConfiguration字段
            environment.setDynamicConfiguration(compositeDynamicConfiguration);
        }
        configManager.refreshAll(); // 刷新所有AbstractConfig配置
    }

1. 刷新配置中心的配置

首先来看 ConfigCenterConfig.refresh() 方法,该方法会组合 Environment 对象中全部已初始化的 Configuration,然后遍历 ConfigCenterConfig 中全部字段的 setter 方法,并从 Environment 中获取对应字段的最终值。具体实现如下:

    // ConfigCenterConfig.java
    
    public void refresh() {
        // 获取Environment对象
        Environment env = ApplicationModel.getEnvironment();
        // 将当前已初始化的所有Configuration合并返回
        CompositeConfiguration compositeConfiguration = env.getPrefixedConfiguration(this);
        Method[] methods = getClass().getMethods();
        for (Method method : methods) {
            if (MethodUtils.isSetter(method)) { // 获取ConfigCenterConfig中各个字段的setter方法
                // 根据配置中心的相关配置以及Environment中的各个Configuration,获取该字段的最终值
                String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
                // 调用setter方法更新ConfigCenterConfig的相应字段
                if (StringUtils.isNotEmpty(value) && ClassUtils.isTypeMatch(method.getParameterTypes()[0], value)) {
                    method.invoke(this, ClassUtils.convertPrimitive(method.getParameterTypes()[0], value));
                }
            } else if (isParametersSetter(method)) { // 设置parameters字段,与设置其他字段的逻辑基本类似,但是实现有所不同
                String value = StringUtils.trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
                if (StringUtils.isNotEmpty(value)) {
                    // 获取当前已有的parameters字段
                    Map<String, String> map = invokeGetParameters(getClass(), this);
                    map = map == null ? new HashMap<>() : map;
                    // 覆盖parameters集合 
                    map.putAll(convert(StringUtils.parseParameters(value), ""));
                    // 设置parameters字段
                    invokeSetParameters(getClass(), this, map);
                }
            }
        }
    }

这里我们关注一下 Environment.getPrefixedConfiguration() 方法,该方法会将 Environment 中已有的 Configuration 对象以及当前的 ConfigCenterConfig 按照顺序合并,得到一个 CompositeConfiguration 对象,用于确定配置中心的最终配置信息。具体实现如下:

    // Environment.java
    
    public synchronized CompositeConfiguration getPrefixedConfiguration(AbstractConfig config) {
        // 创建CompositeConfiguration对象,这里的prefix和id是根据ConfigCenterConfig确定的
        CompositeConfiguration prefixedConfiguration = new CompositeConfiguration(config.getPrefix(), config.getId());
        // 将ConfigCenterConfig封装成ConfigConfigurationAdapter
        Configuration configuration = new ConfigConfigurationAdapter(config);
        if (this.isConfigCenterFirst()) { // 根据配置确定ConfigCenterConfig配置的位置
            // The sequence would be: SystemConfiguration -> AppExternalConfiguration -> ExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
            // 按序组合已有Configuration对象以及ConfigCenterConfig
            prefixedConfiguration.addConfiguration(systemConfiguration);
            prefixedConfiguration.addConfiguration(environmentConfiguration);
            prefixedConfiguration.addConfiguration(appExternalConfiguration);
            prefixedConfiguration.addConfiguration(externalConfiguration);
            prefixedConfiguration.addConfiguration(configuration);
            prefixedConfiguration.addConfiguration(propertiesConfiguration);
        } else {
            // 配置优先级如下:SystemConfiguration -> AbstractConfig -> AppExternalConfiguration -> ExternalConfiguration -> PropertiesConfiguration
            prefixedConfiguration.addConfiguration(systemConfiguration);
            prefixedConfiguration.addConfiguration(environmentConfiguration);
            prefixedConfiguration.addConfiguration(configuration);
            prefixedConfiguration.addConfiguration(appExternalConfiguration);
            prefixedConfiguration.addConfiguration(externalConfiguration);
            prefixedConfiguration.addConfiguration(propertiesConfiguration);
        }
        return prefixedConfiguration;
    }

2. 创建 DynamicConfiguration 对象

通过 ConfigCenterConfig.refresh() 方法确定了所有配置中心的最终配置之后,接下来就会对每个配置中心执行 prepareEnvironment() 方法,得到对应的 DynamicConfiguration 对象。具体实现如下:

    // DynamicConfiguration.java
    
    private DynamicConfiguration prepareEnvironment(ConfigCenterConfig configCenter) {
        // 检查ConfigCenterConfig是否合法
        if (configCenter.isValid()) { 
            if (!configCenter.checkOrUpdateInited()) { 
                return null; // 检查ConfigCenterConfig是否已初始化,这里不能重复初始化
            }
            // 根据ConfigCenterConfig中的各个字段,拼接出配置中心的URL,创建对应的DynamicConfiguration对象
            DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
            // 从配置中心获取externalConfiguration和appExternalConfiguration,并进行覆盖
            String configContent = dynamicConfiguration.getProperties(configCenter.getConfigFile(), configCenter.getGroup());
    
            String appGroup = getApplication().getName();
            String appConfigContent = null;
            if (isNotEmpty(appGroup)) {
                appConfigContent = dynamicConfiguration.getProperties
                        (isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),
                                appGroup
                        );
            }
            try {
                // 更新Environment
                environment.setConfigCenterFirst(configCenter.isHighestPriority());
                environment.updateExternalConfigurationMap(parseProperties(configContent));
                environment.updateAppExternalConfigurationMap(parseProperties(appConfigContent));
            } catch (IOException e) {
                throw new IllegalStateException("Failed to parse configurations from Config Center.", e);
            }
            return dynamicConfiguration; // 返回通过该ConfigCenterConfig创建的DynamicConfiguration对象
        }
        return null;
    }

完成 DynamicConfiguration 的创建之后,DubboBootstrap 会将多个配置中心对应的 DynamicConfiguration 对象封装成一个 CompositeDynamicConfiguration 对象,并记录到 Environment.dynamicConfiguration 字段中,等待后续使用。另外,还会调用全部 AbstractConfig 的 refresh() 方法(即根据最新的配置更新各个 AbstractConfig 对象的字段)。这些逻辑都在 DubboBootstrap.startConfigCenter() 方法中,不再赘述。

2.3 初始化后续流程

完成明确指定的配置中心初始化之后,DubboBootstrap 接下来会执行 useRegistryAsConfigCenterIfNecessary() 方法,检测当前 Dubbo 是否要将注册中心也作为一个配置中心使用(常见的注册中心,都可以直接作为配置中心使用,这样可以降低运维成本)。

    // DubboBootstrap.java
    
    private void useRegistryAsConfigCenterIfNecessary() {
        if (environment.getDynamicConfiguration().isPresent()) {
            return; // 如果当前配置中心已经初始化完成,则不会将注册中心作为配置中心
        }
        if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {
            return; // 明确指定了配置中心的配置,哪怕配置中心初始化失败,也不会将注册中心作为配置中心
        }
        // 从ConfigManager中获取注册中心的配置(即RegistryConfig),并转换成配置中心的配置(即ConfigCenterConfig)
        configManager.getDefaultRegistries().stream()
                .filter(registryConfig -> registryConfig.getUseAsConfigCenter() == null || registryConfig.getUseAsConfigCenter())
                .forEach(registryConfig -> {
                    String protocol = registryConfig.getProtocol();
                    String id = "config-center-" + protocol + "-" + registryConfig.getPort();
                    ConfigCenterConfig cc = new ConfigCenterConfig();
                    cc.setId(id);
                    if (cc.getParameters() == null) {
                        cc.setParameters(new HashMap<>());
                    }
                    if (registryConfig.getParameters() != null) {
                        cc.getParameters().putAll(registryConfig.getParameters());
                    }
                    cc.getParameters().put(CLIENT_KEY, registryConfig.getClient());
                    cc.setProtocol(registryConfig.getProtocol());
                    cc.setPort(registryConfig.getPort());
                    cc.setAddress(registryConfig.getAddress());
                    cc.setNamespace(registryConfig.getGroup());
                    cc.setUsername(registryConfig.getUsername());
                    cc.setPassword(registryConfig.getPassword());
                    if (registryConfig.getTimeout() != null) {
                        cc.setTimeout(registryConfig.getTimeout().longValue());
                    }
                    cc.setHighestPriority(false); // 这里优先级较低
                    configManager.addConfigCenter(cc);
                });
        startConfigCenter(); // 重新调用startConfigCenter()方法,初始化配置中心
    }

完成配置中心的初始化之后,后续需要 DynamicConfiguration 的地方直接从 Environment 中获取即可,例如,DynamicConfigurationServiceNameMapping 就是依赖 DynamicConfiguration 实现 Service ID 与 Service Name 映射的管理。

接下来,DubboBootstrap 执行 loadRemoteConfigs() 方法,根据前文更新后的 externalConfigurationMap 和 appExternalConfigurationMap 配置信息,确定是否配置了额外的注册中心或 Protocol,如果有,则在此处转换成 RegistryConfig 和 ProtocolConfig,并记录到 ConfigManager 中,等待后续逻辑使用。

随后,DubboBootstrap 执行 checkGlobalConfigs() 方法完成 ProviderConfig、ConsumerConfig、MetadataReportConfig 等一系列 AbstractConfig 的检查和初始化,具体实现比较简单,这里不再赘述。

再紧接着,DubboBootstrap 会通过 initMetadataService() 方法初始化 MetadataReport、MetadataReportInstance 以及 MetadataService、MetadataServiceExporter。

在 DubboBootstrap 初始化的最后,会调用 initEventListener() 方法将 DubboBootstrap 作为 EventListener 监听器添加到 EventDispatcher 中。DubboBootstrap 继承了 GenericEventListener 抽象类,如下图所示:

202308162144050628.png

GenericEventListener 是一个泛型监听器,它可以让子类监听任意关心的 Event 事件,只需定义相关的 onEvent() 方法即可 。在 GenericEventListener 中维护了一个 handleEventMethods 集合,其中 Key 是 Event 的子类,即监听器关心的事件,Value 是处理该类型 Event 的相应 onEvent() 方法。

在 GenericEventListener 的构造方法中,通过反射将当前 GenericEventListener 实现的全部 onEvent() 方法都查找出来,并记录到 handleEventMethods 字段中。具体查找逻辑在 findHandleEventMethods() 方法中实现:

    // GenericEventListener.java
    
    private Map<Class<?>, Set<Method>> findHandleEventMethods() {
        Map<Class<?>, Set<Method>> eventMethods = new HashMap<>();
        of(getClass().getMethods()) // 遍历当前GenericEventListener子类的全部方法
                // 过滤得到onEvent()方法,具体过滤条件在isHandleEventMethod()方法之中:
                // 1.方法必须是public的
                // 2.方法参数列表只有一个参数,且该参数为Event子类
                // 3.方法返回值为void,且没有声明抛出异常
                .filter(this::isHandleEventMethod) 
                .forEach(method -> {
                    Class<?> paramType = method.getParameterTypes()[0];
                    Set<Method> methods = eventMethods.computeIfAbsent(paramType, key -> new LinkedHashSet<>());
                    methods.add(method);
                });
        return eventMethods;
    }

在 GenericEventListener 的 onEvent() 方法中,会根据收到的 Event 事件的具体类型,从 handleEventMethods 集合中找到相应的 onEvent() 方法进行调用,如下所示:

    // GenericEventListener.java
    
    public final void onEvent(Event event) {
        // 获取Event的实际类型
        Class<?> eventClass = event.getClass(); 
        // 根据Event的类型获取对应的onEvent()方法并调用
        handleEventMethods.getOrDefault(eventClass,  emptySet()).forEach(method -> {
            ThrowableConsumer.execute(method, m -> {
                m.invoke(this, event);
            });
        });
    }

我们可以查看 DubboBootstrap 的所有方法,目前并没有发现符合 isHandleEventMethod() 条件的方法。但在 GenericEventListener 的另一个实现—— LoggingEventListener 中,可以看到多个符合 isHandleEventMethod() 条件的方法(如下图所示),在这些 onEvent() 方法重载中会输出 INFO 日志。

202308162144055889.png

至此,DubboBootstrap 整个初始化过程,以及该过程中与配置中心相关的逻辑就介绍完了。

三、总结

本章我重点介绍了 Dubbo 配置中心中的多种配置接口:

  • 首先,我讲解了 Configuration 这个顶层接口的核心方法;
  • 接着,我介绍了 Configuration 接口的相关实现,这些实现可以从环境变量、-D 启动参数、Properties文件以及其他配置文件或注解处读取配置信息;
  • 然后,我还着重介绍了 DynamicConfiguration 这个动态配置接口的定义,并分析了以 Zookeeper 为动态配置中心的 ZookeeperDynamicConfiguration 实现;
  • 最后,我介绍了 Dubbo 动态配置中心启动的核心流程,以及该流程涉及的重要组件类。

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文