dubbo之配置(Configuration)

前言

dubbo的Configuration负责内部所有配置信息的汇总、管理,考虑到各种可能场景,dubbo提供基于Configuration接口的静态配置和动态配置功能。通常情况下,静态配置用于服务、服务提供方、服务消费方等在dubbo服务暴露、引用阶段的初始化工作,初始化完成之后执行服务的暴露、注册、引用;动态配置则主要用于动态更新dubbo对外暴露的可覆盖配置以及用户自定义配置,无需重启服务,通常借助三方配置工具或配置中间件实现(比如Apollo、Zookeeper等,2.7版本暂不支持Nacos)。本文会分别从静态配置、动态配置两种场景进行分析;其中静态配置加载的触发阶段(包括与Spring生态的对接、服务暴露、服务引用)会在对应章节进行分析,本文仅解析静态配置的设计与使用;动态配置的处理相对独立,方便进行完整解析。

一、Configuration

顶级接口Configuration,定义dubbo中配置的基础功能,可以分为两类,基础功能和扩展接口,其中扩展接口主要用于子类个性化实现。基础功能以getString()方法为例,内部借助convert()实现

default String getString(String key, String defaultValue) {
    return convert(String.class, key, defaultValue);
}

default  T convert(Class cls, String key, T defaultValue) {
        // 这里仅处理字符串类型属性
        String value = (String) getProperty(key);

        if (value == null) {
            return defaultValue;
        }
                
            // 可以看出,dubbo仅支持几种类型配置:字符串、数字、枚举.
        Object obj = value;
        if (cls.isInstance(value)) {
            return cls.cast(value);
        }

        if (String.class.equals(cls)) {
            return cls.cast(value);
        }

        if (Boolean.class.equals(cls) || Boolean.TYPE.equals(cls)) {
            obj = Boolean.valueOf(value);
        } else if (Number.class.isAssignableFrom(cls) || cls.isPrimitive()) {
            if (Integer.class.equals(cls) || Integer.TYPE.equals(cls)) {
                obj = Integer.valueOf(value);
            } else if (Long.class.equals(cls) || Long.TYPE.equals(cls)) {
                obj = Long.valueOf(value);
            } else if (Byte.class.equals(cls) || Byte.TYPE.equals(cls)) {
                obj = Byte.valueOf(value);
            } else if (Short.class.equals(cls) || Short.TYPE.equals(cls)) {
                obj = Short.valueOf(value);
            } else if (Float.class.equals(cls) || Float.TYPE.equals(cls)) {
                obj = Float.valueOf(value);
            } else if (Double.class.equals(cls) || Double.TYPE.equals(cls)) {
                obj = Double.valueOf(value);
            }
        } else if (cls.isEnum()) {
            obj = Enum.valueOf(cls.asSubclass(Enum.class), value);
        }
        return cls.cast(obj);
  }

default Object getProperty(String key) {
        return getProperty(key, null);
}

然后是扩展接口,以getProperty方法为例,最终借助扩展方法getInternalProperty实现

default Object getProperty(String key, Object defaultValue) {
            //这里调用扩展接口
        Object value = getInternalProperty(key);
        return value != null ? value : defaultValue;
}

//模板方法,由子类实现
Object getInternalProperty(String key);

二、静态配置(AbstractPrefixConfiguration)

解析静态配置之前,先来了解一下java中的系统环境变量和系统属性。系统环境变量我们比较熟悉,入职第一天必做的事情就是配置各种环境变量;系统属性其实我们也经常用,比如最常用的System.getProperty("xxx"),或者配置jvm参数"-DXX:NewRatio=4",最终都会被解析至系统属性,另外,你会发现,在你没有手动配置系统环境变量与系统属性之前,就可以直接使用了(jvm内置的初始化属性,可以直接在jdk注释查看)。jdk对于enviroment的解释:

# 环境变量
The environment is a system-dependent mapping from names to values which is passed from parent to child processes,If the system does not support environment variables, an empty map is returned。
环境变量,依赖系统的name-value键值对,可以由父进程传递给子进程;若系统不支持环境变量,那么会返回一个空的map。

下面的demo,可以帮你区分系统环境变量与系统属内容的不同

// 系统环境变量
Map envProperties = System.getenv();
System.out.println("env.size = " + envProperties.size());
envProperties.forEach((k,v) ->{
    System.out.println(k + " : " + v);
});

// 系统属性 -D参数或者setProperties
Properties sysProperties = System.getProperties();
sysProperties.setProperty("哈哈哈","hahaha");
System.out.println("sys.size = " + sysProperties.entrySet().size());
sysProperties.forEach((k,v) ->{
    System.out.println(k + " : " + v);
});

了解完系统环境变量与系统属性,下面来看dubbo中静态配置的设计。静态环境配置并不是通过直接实现Configuration接口实现,先由基类AbstractPrefixConfiguration实现Configuration接口,然后静态配置全部继承AbstractPrefixConfiguration。我们先来看AbstractPrefixConfiguration,比较容易理解,直译就是抽象前缀配置,核心参数只有前缀prefix和id,直接来看代码:

public AbstractPrefixConfiguration(String prefix, String id) {
    super();
    //前缀要么为空,要么必须以"."结尾
    if (StringUtils.isNotEmpty(prefix) && !prefix.endsWith(".")) {
        this.prefix = prefix + ".";
    } else {
        this.prefix = prefix;
    }
    this.id = id;
}

// 核心方法,内部调用具体子类的getInternalProperty方法
@Override
public Object getProperty(String key, Object defaultValue) {
    Object value = null;
    if (StringUtils.isNotEmpty(prefix) && StringUtils.isNotEmpty(id)) {
        value = getInternalProperty(prefix + id + "." + key);
    }
    if (value == null && StringUtils.isNotEmpty(prefix)) {
        value = getInternalProperty(prefix + key);
    }

    if (value == null) {
        value = getInternalProperty(key);
    }
    return value != null ? value : defaultValue;
}

基于AbstractPrefixConfiguration静态配置基类,dubbo扩展出EnvironmentConfiguration(系统环境变量配置)、InmemoryConfiguration(内存配置)、PropertiesConfiguration(系统属性配置)、SystemConfiguration(系统配置),后两个配置其实有重叠也有区别,具体到代码分析阶段会详细说明。下面按照顺序,或者说叫优先级来依次解析

先来看EnvironmentConfiguration ,非常简单,仅借助System.getenv()方法实现了getInternalProperty。

public class EnvironmentConfiguration extends AbstractPrefixConfiguration {
    public EnvironmentConfiguration(String prefix, String id) {
        super(prefix, id);
    }

    public EnvironmentConfiguration() {
        this(null, null);
    }

    @Override
    public Object getInternalProperty(String key) {
        // 借助系统环境变量实现
        return System.getenv(key);
    }

}

InmemoryConfiguration,内存缓存配置,内部多了一个map,用于缓存相关配置,同时对外暴露一个addProperties、setProperties方法,核心方法getInternalProperty则是直接借助map缓存实现,比较简单

// 省略add、set方法
public class InmemoryConfiguration extends AbstractPrefixConfiguration {
    private Map store = new LinkedHashMap<>();

    public InmemoryConfiguration(String prefix, String id) {
        super(prefix, id);
    }

    public InmemoryConfiguration() {
        this(null, null);
    }

    @Override
    public Object getInternalProperty(String key) {
        return store.get(key);
    }
}

重点来看后两种配置,先来看SystemConfiguration,比较简单,内部主要借助System.getProperty实现。

// SystemConfiguration直接借助System.getProperty方法实现
public class SystemConfiguration extends AbstractPrefixConfiguration {
        public SystemConfiguration(String prefix, String id) {
        super(prefix, id);
    }

    public SystemConfiguration() {
        this(null, null);
    }

    @Override
    public Object getInternalProperty(String key) {
        return System.getProperty(key);
    }
}

最后来看PropertiesConfiguration,逻辑上与SystemConfiguration有一些重复,getInternalProperty方法借助ConfigUtils.getProperty实现。大致可以分为三步:

  1. 先从系统属性获取配置,配置值非空则直接返回,否则执行2;
  2. 加载制定配置文件,加载的优先级顺序: 系统属性中配置的"dubbo.properties.file"文件 > 系统环境变量中配置的"dubbo.properties.file" > classPath下的dubbo.properties文件。
  3. 配置值中有占位符,则使用2中的properties进行替换,并返回。
//重点关注getInternalProperty
public class PropertiesConfiguration extends AbstractPrefixConfiguration {
    private static final Logger logger = LoggerFactory.getLogger(PropertiesConfiguration.class);
    public PropertiesConfiguration(String prefix, String id) {
        super(prefix, id);
    }

    public PropertiesConfiguration() {
        this(null, null);
    }

    @Override
    public Object getInternalProperty(String key) {
        //借助ConfigUtils实现
        return ConfigUtils.getProperty(key);
    }
}

//再来看ConfigUtils.getProperty方法
public static String getProperty(String key, String defaultValue) {
        //优先从系统属性取,这里与SystemConfiguration功能重叠
        String value = System.getProperty(key);
        if (value != null && value.length() > 0) {
            return value;
        }
        // 否则加载指定配置文件,这里需要注意配置优先级
        Properties properties = getProperties();
        // 支持嵌套替换,举个例子:key = "abc",properties中有["abc","1${a.b.c}2${a.b.c}3"],["a.b.c","ABC"],最终                 // 结果是 "1ABC2ABC3"
        return replaceProperty(properties.getProperty(key, defaultValue), (Map) properties);
}

// 属性文件加载顺序,系统配置(-D参数,-Ddubbo.properties.file) -> 系统环境变量 -> dubbo.properties保底
 public static Properties getProperties() {
    //DCL保证属性文件一致性。
    if (PROPERTIES == null) {
        synchronized (ConfigUtils.class) {
            if (PROPERTIES == null) {
                // 优先读取系统属性 "dubbo.properties.file" 指定的文件,
                // 如: -Ddubbo.properties.file=dubbo.properties
                 String path = System.getProperty(Constants.DUBBO_PROPERTIES_KEY);
                 if (path == null || path.length() == 0) {
                     // 系统属性未配置,则从环境变量中读取属性 "dubbo.properties.file"
                     path = System.getenv(Constants.DUBBO_PROPERTIES_KEY);
                     if (path == null || path.length() == 0) {
                        // 系统环境变量未配置,则默认读取 dubbo.properties配置文件
                         path = Constants.DEFAULT_DUBBO_PROPERTIES;
                     }
                 }
                PROPERTIES = ConfigUtils.loadProperties(path, false, true);
            }
        }
   }
   return PROPERTIES;
}

// 属性替换例如:expression= "1${a.b.c}2${a.b.c}3" ,params = ["a.b.c","ABC"]
// 返回,替换后的字符串:"1ABC2ABC3"
public static String replaceProperty(String expression, Map params) {
  // 仅处理带占位符的属性"${xxx}"  
  if (expression == null || expression.length() == 0 || expression.indexOf('$') < 0) {
            return expression;
    }
  // 根据正则进行匹配
  Matcher matcher = VARIABLE_PATTERN.matcher(expression);
  StringBuffer sb = new StringBuffer();
  while (matcher.find()) {
      String key = matcher.group(1);
      String value = System.getProperty(key);
      if (value == null && params != null) {
            value = params.get(key);
      }
      if (value == null) {
           value = "";
      }
        // 字符串拼接
      matcher.appendReplacement(sb, Matcher.quoteReplacement(value));
  }
  matcher.appendTail(sb);
  // 最终结果
  return sb.toString();
}

以上就是静态配置的全部内容,相对于动态配置来说还是比较简单的。除了静态配置、动态配置之外,dubbo还提供了动态配置、静态配置的复合配置CompositeConfiguration,在复合配置中内置Configuration列表,没有静态、动态配置之分。核心方法getInternalProperty,逻辑也比较简单,只要匹配Configuration列表中第一个包含查找属性的配置即可,下面来看代码

// 复合配置,不区分动态、静态配置
public class CompositeConfiguration implements Configuration {
    private Logger logger = LoggerFactory.getLogger(CompositeConfiguration.class);
    private List configList = new LinkedList();

    public CompositeConfiguration() {

    }
        // 带参数的构造方法
    public CompositeConfiguration(Configuration... configurations) {
        if (configurations != null && configurations.length > 0) {
            Arrays.stream(configurations).filter(config -> !configList.contains(config)).forEach(configList::add);
        }
    }
        
    // 添加配置
    public void addConfiguration(Configuration configuration) {
        if (configList.contains(configuration)) {
            return;
        }
        this.configList.add(configuration);
    }
        
    public void addConfigurationFirst(Configuration configuration) {
        this.addConfiguration(0, configuration);
    }
        // 指定位置插入配置
    public void addConfiguration(int pos, Configuration configuration) {
        this.configList.add(pos, configuration);
    }

    @Override
    public Object getInternalProperty(String key) {
        Configuration firstMatchingConfiguration = null;
        for (Configuration config : configList) {
            try {
                //若有配置包含当前要查找的key信息,直接返回
                if (config.containsKey(key)) {
                    firstMatchingConfiguration = config;
                    break;
                }
            } catch (Exception e) {
                logger.error("Error when trying to get value for key " + key + " from " + config + ", will continue to try the next one.");
            }
        }
        if (firstMatchingConfiguration != null) {
            // 第一个匹配的配置,读取配置值
            return firstMatchingConfiguration.getProperty(key);
        } else {
            return null;
        }
    }

    @Override
    public boolean containsKey(String key) {
        return configList.stream().anyMatch(c -> c.containsKey(key));
    }
}

好了,以上就是所有静态配置实现。下面我们接着来看动态配置。

三、动态配置(DynamicConfiguration)

DynamicConfiguration接口继承顶级接口Configuration,用于动态同步三方配置。提到动态更新,就不得不提对应的Listener,即ConfigurationLisenter。很容易想到,动态配置借助ConfigurationListener监听配置变更,然后同步更新本地配置。在介绍Directory一文中,我们提到过ConfigurationListener,本文我们会对ConfigurationListener重点做介绍。

1、配置变更监听器(ConfigurationListener)

先来看ConfigurationListener接口设计,同样是顶级接口,内部只有一个process方法,用于处理配置变更事件。基于ConfigurationListener有两个主要扩展,分别是AbstractConfiguratorListener和ListenableRouter,其中ListenableRouter在Router一文已经做了介绍,本文不再冗述,仅关注AbstractConfiguratorListener。在AbstractConfigratorListener中,内置Configurator配置器列表,重点关注构造方法以及核心process()的逻辑,同时还额外定义了notifyOverrides()供子类实现(子类均在RegistryDirectory、RegistryProtocol)。

protected final void initWith(String key) {
    //动态配置
    DynamicConfiguration dynamicConfiguration = DynamicConfiguration.getDynamicConfiguration();
    // 如果是nopDynamicConfiguration,那么这里什么都不会做
    dynamicConfiguration.addListener(key, this);
    String rawConfig = dynamicConfiguration.getConfig(key);
    if (!StringUtils.isEmpty(rawConfig)) {
        //直接根据字符串配置,赋值configurators.
        process(new ConfigChangeEvent(key, rawConfig));
    }
}

// 顺便来看下DynamicConfiguration.getDynamicConfiguration方法的逻辑
static DynamicConfiguration getDynamicConfiguration() {
            // 先从环境变量中取动态配置
        Optional optional = Environment.getInstance().getDynamicConfiguration();
            // 动态配置为空,则通过DynamicConfigurationFactory的SPI,获取默认扩展实现。默认是NopDynamicConfiguration,即什              // 么都不做。
        return (DynamicConfiguration) optional.orElseGet(() -> getExtensionLoader(DynamicConfigurationFactory.class)
                .getDefaultExtension()
                .getDynamicConfiguration(null));
}

//process逻辑比较核心
@Override
    public void process(ConfigChangeEvent event) {
        if (logger.isInfoEnabled()) {
            logger.info("Notification of overriding rule, change type is: " + event.getChangeType() +
                    ", raw config content is:\n " + event.getValue());
        }
        //配置删除事件,则直接清空configurators
        if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
            configurators.clear();
        } else {
            try {
                // parseConfigurators will recognize app/service config automatically.
                // 借助配置解析器,解析配置内容至URL,最终再将URL转换为Configurator,并存放于Configurator列表
                configurators=Configurator.toConfigurators(ConfigParser.parseConfigurators(event.getValue()))
                        .orElse(configurators);
            } catch (Exception e) {
                logger.error("Failed to parse raw dynamic config and it will not take effect, the raw config                                    is: " +event.getValue(), e);
                return;
            }
        }
        // 通知子类,同步变更配置,其实这里是直接刷新RegistryDirectory内的Invoker。
        notifyOverrides();
    }

上面的代码可以看到,配置的解析分为配置解析和配置缓存两步。配置解析主要是将配置变更解析为URL列表,配置缓存则是把URL转为Congurator配置列表缓存。先来看配置解析

// 解析入口
public static List parseConfigurators(String rawConfig) {
    List urls = new ArrayList<>();
    // 借助Yaml工具,将配置信息解析至ConfiguratorConfig,ConfiguratorConfig的数据结构这里就不再列出来了。
    ConfiguratorConfig configuratorConfig = parseObject(rawConfig);
    String scope = configuratorConfig.getScope();
    // 具体配置项解析为URL,氛围应用级配置和服务级配置,默认为服务级配置。
    List items = configuratorConfig.getConfigs();
    if (ConfiguratorConfig.SCOPE_APPLICATION.equals(scope)) {
        //应用级配置
        items.forEach(item -> urls.addAll(appItemToUrls(item, configuratorConfig)));
    } else {
        // service scope by default.
        // 服务级配置
        items.forEach(item -> urls.addAll(serviceItemToUrls(item, configuratorConfig)));
    }
    return urls;
}

// 应用级配置与服务级配置大同小异,直接来看服务级配置
private static List serviceItemToUrls(ConfigItem item, ConfiguratorConfig config) {
        List urls = new ArrayList<>();
            // 获取配置地址,并根据配置地址解析为URL,这里有个细节,如果地址列表size=0时会默认返回一个anyhost。
        List addresses = parseAddresses(item);
        addresses.forEach(addr -> {
            StringBuilder urlBuilder = new StringBuilder();
            // URL中协议只能是override
            urlBuilder.append("override://").append(addr).append("/");
                        // serviceKey转URL信息,举个例子 serviceKey = test/com.edu.nbu.scm.KeyService:1.0,
            // appendService结果:com.edu.nbu.scm.KeyService?group=test&version=1.0
            urlBuilder.append(appendService(config.getKey()));
            // 参数拼接,比较简单,这里会拼接category等参数信息
            urlBuilder.append(toParameterString(item));
            // 拼接enabled
            parseEnabled(item, config, urlBuilder);

            urlBuilder.append("&category=").append(Constants.DYNAMIC_CONFIGURATORS_CATEGORY);
            urlBuilder.append("&configVersion=").append(config.getConfigVersion());
            // 这里是与appItemToUrls的不同点,这里会根据applications值来决定是否需要拼接application
            // 最终借助URL.valueOf方法,生成URL并放入列表。
            List apps = item.getApplications();
            if (apps != null && apps.size() > 0) {
                apps.forEach(app -> {
                    urls.add(URL.valueOf(urlBuilder.append("&application=").append(app).toString()));
                });
            } else {
                urls.add(URL.valueOf(urlBuilder.toString()));
            }
        });
        return urls;
    }

配置的解析结果是URL列表,完成解析后,需要将URL转为Configurator。思考为什么不是配置直接解析为Configurator,个人理解,dubbo中URL作为数据总线,整个RPC过程都会与URL进行交互,配置解析为URL更能保证数据的实时性,Congurator则不然,作用域仅限于配置变更阶段,无法保证全局实时。下面来看URL->Configurator的逻辑

static Optional> toConfigurators(List urls) {
    if (CollectionUtils.isEmpty(urls)) {
        return Optional.empty();
    }

    ConfiguratorFactory configuratorFactory = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
            .getAdaptiveExtension();
    List configurators = new ArrayList<>(urls.size());
    //empty协议,则直接清空configurator列表
    for (URL url : urls) {
        if (Constants.EMPTY_PROTOCOL.equals(url.getProtocol())) {
            configurators.clear();
            break;
        }
        Map override = new HashMap<>(url.getParameters());
        //The anyhost parameter of override may be added automatically, it can't change the judgement of changing url
        override.remove(Constants.ANYHOST_KEY);
        if (override.size() == 0) {
            configurators.clear();
            continue;
        }
        // 借助configuratorFactory,将URL转为Configurator
        configurators.add(configuratorFactory.getConfigurator(url));
    }
    // 排序规则需要注意,排序规则为: 地址ip -> 优先级值
    Collections.sort(configurators);
    return Optional.of(configurators);
}

配置解析最终结果是,所有变更信息全部缓存于configruator列表,供子类使用。配置变更监听器的基类实现AbstractConfiguratorListener已经全部介绍完毕了,下面来看他的子类实现,上面也提到过,子类实现分别在RegidtryDirectory和RegistryProcotol中,主要包括:ConsumerConfigurationListener(消费者配置变更监听器)、ReferenceConfigurationListener(引用配置变更监听器)、ProviderConfigurationListener(服务提供者配置变更监听器)、ServiceConfigurationListener(服务配置变更监听器),下面依次进行解析

// 消费者配置变更监听器的逻辑比较简单,直接刷新RegistryDirectory内的Invoker即可,刷新逻辑参考Directory一文
private static class ConsumerConfigurationListener extends AbstractConfiguratorListener {
    // 内置RegistryDirectory列表。
    List listeners = new ArrayList<>();
    ConsumerConfigurationListener() {
        // 仅根据application名进行初始化
        this.initWith(ApplicationModel.getApplication() + Constants.CONFIGURATORS_SUFFIX);
    }
    void addNotifyListener(RegistryDirectory listener) {
        this.listeners.add(listener);
    }
    @Override
    protected void notifyOverrides() {
        // 最终调用 RegistryDirectory的refreshInvoer,配置变更会同步刷新directory中的invoker
        listeners.forEach(listener -> listener.refreshInvoker(Collections.emptyList()));
    }
}
//引用配置变更监听器
private static class ReferenceConfigurationListener extends AbstractConfiguratorListener {
    private RegistryDirectory directory;
    private URL url;

    ReferenceConfigurationListener(RegistryDirectory directory, URL url) {
        this.directory = directory;
        this.url = url;
        // 需要借助url信息初始化
        this.initWith(url.getEncodedServiceKey() + Constants.CONFIGURATORS_SUFFIX);
    }

    @Override
    protected void notifyOverrides() {
        // 最终调用 RegistryDirectory的refreshInvoer,配置变更会同步刷新directory中的invoker
        directory.refreshInvoker(Collections.emptyList());
    }
}
// 服务提供者配置变更监听器
private class ProviderConfigurationListener extends AbstractConfiguratorListener {
        public ProviderConfigurationListener() {
        this.initWith(ApplicationModel.getApplication() + CONFIGURATORS_SUFFIX);
    }
    private  URL overrideUrl(URL providerUrl) {
        return RegistryProtocol.getConfigedInvokerUrl(configurators, providerUrl);
    }

    @Override
    protected void notifyOverrides() {
        // 最终借助NotifyListener实现
        overrideListeners.values().forEach(listener -> ((OverrideListener) listener).doOverrideIfNecessary());
    }
}
// 服务配置变更监听器
private class ServiceConfigurationListener extends AbstractConfiguratorListener {
    private URL providerUrl;
    private OverrideListener notifyListener;

    public ServiceConfigurationListener(URL providerUrl, OverrideListener notifyListener) {
        this.providerUrl = providerUrl;
        this.notifyListener = notifyListener;
        this.initWith(providerUrl.getEncodedServiceKey() + CONFIGURATORS_SUFFIX);
    }

    private  URL overrideUrl(URL providerUrl) {
        return RegistryProtocol.getConfigedInvokerUrl(configurators, providerUrl);
    }

    @Override
    protected void notifyOverrides() {
        // 同样,最终借助notifyListener实现
        notifyListener.doOverrideIfNecessary();
    }
}

四个子类的实现也比较简单,前两个位于RegistroyDirectory,核心逻辑都是借助RegistroyDirectory的refreshInvoker方法法实现;后两个则位于RegistryProtocol,核心逻辑都是借助OverrideListener(NotifyListener的子类)的doOverrideIfNecessary方法实现。

2、动态配置(DynamicConfiguration)

了解完配置变更监听器的逻辑,再来看动态配置,就比较容易了。DynamicConfiguration接口扩展了Configuration,新增配置变更监听器管理以及配置获取功能。dubbo内部对DynamicConfiguration的实现主要有ApolloDynamicConfiguration(Apollo作为三方配置中心)、MockDynamicConfiguration(降级配置)、NopDynamicConfiguration(终极降级配置)、ZookeeperDynamicConfiguration(Zookeeper作为三方配置中心)。初次之外,DynamicConfiguration的所有实现均由DynamicConfigurationFactory创建,DynamicConfigurationFactory接口支持SPI扩展,默认实现是NopDynamicConfigurationFactory,也就是说默认的DynamicConfiguration扩展是NopDynamicConfiguration。DynamicConfigurationFactory比较简单,就不做过多介绍了,下面直接来看DynamicConfiguration的四种实现。

ApolloDynamicConfiguration中,需要关注构造方法和配置变更监听器的管理以及Configuration接口方法的实现。构造方法中,初始化了大量与Apollo相关的附加属性,比如env、apollo.meta、apollo.cluster等;配置变更监听器的管理和Configuration接口方法则是借助Apollo的Config以及ConfigChangeListener实现。下面直接来看相关代码

// 构造方法,初始化附加属性以及Apollo的config
ApolloDynamicConfiguration(URL url) {
    this.url = url;
    // Instead of using Dubbo's configuration, I would suggest use the original configuration method Apollo provides.
    String configEnv = url.getParameter(APOLLO_ENV_KEY);
    String configAddr = getAddressWithProtocolPrefix(url);
    String configCluster = url.getParameter(Constants.CONFIG_CLUSTER_KEY);
    if (configEnv != null) {
        System.setProperty(APOLLO_ENV_KEY, configEnv);
    }
    if (StringUtils.isEmpty(System.getProperty(APOLLO_ENV_KEY)) && !Constants.ANYHOST_VALUE.equals(configAddr)) {
        System.setProperty(APOLLO_ADDR_KEY, configAddr);
    }
    if (configCluster != null) {
        System.setProperty(APOLLO_CLUSTER_KEY, configCluster);
    }
        // 初始化Apollo的Config
    dubboConfig = ConfigService.getConfig(url.getParameter(Constants.CONFIG_NAMESPACE_KEY, DEFAULT_GROUP));
    // Decide to fail or to continue when failed to connect to remote server.
    boolean check = url.getParameter(Constants.CONFIG_CHECK_KEY, true);
    if (dubboConfig.getSourceType() != ConfigSourceType.REMOTE) {
        //如果需要检查,则直接抛异常
        if (check) {
            throw new IllegalStateException("Failed to connect to config center, the config center is Apollo,    " +"the address is: " + (StringUtils.isNotEmpty(configAddr) ? configAddr : configEnv));
        } else {
            logger.warn("Failed to connect to config center, the config center is Apollo, " +
                    "the address is: " + (StringUtils.isNotEmpty(configAddr) ? configAddr : configEnv) +
                    ", will use the local cache value instead before eventually the connection is established.");
        }
    }
}

对于配置变更监听器的管理,这里以添加监听器为例来说明,其他操作大同小异

@Override
public void addListener(String key, String group, ConfigurationListener listener) {
    // listeners->ApolloListener的缓存,
    ApolloListener apolloListener = listeners.computeIfAbsent(group + key, k -> createTargetListener(key, group));
    // 借助apolloListener添加dubbo的ConfigurationListener
    apolloListener.addListener(listener);
    // apolloListener添加至Apollo的config
    dubboConfig.addChangeListener(apolloListener, Collections.singleton(key));
}

// 关注一下ApolloListener的实现
public class ApolloListener implements ConfigChangeListener {
   // listeners,dubbo的ConfigurationListener列表,管理dubbo的ConfigurationListener
     private Set listeners = new CopyOnWriteArraySet<>();
     ApolloListener() {}

   @Override
   // 配置变更事件处理
   public void onChange(com.ctrip.framework.apollo.model.ConfigChangeEvent changeEvent) {
       for (String key : changeEvent.changedKeys()) {
            ConfigChange change = changeEvent.getChange(key);
            if ("".equals(change.getNewValue())) {
                logger.warn("an empty rule is received for " + key + ", the current working rule is " +
                      change.getOldValue() + ", the empty rule will not take effect.");
                return;
            }
                        // apllo配置变更事件的处理
            ConfigChangeEvent event = new ConfigChangeEvent(key, change.getNewValue(),getChangeType(change));
            listeners.forEach(listener -> listener.process(event));
       }
  }
    // 辅助方法,配置变更事件转换
  private ConfigChangeType getChangeType(ConfigChange change) {
       if (change.getChangeType() == PropertyChangeType.DELETED) {
            return ConfigChangeType.DELETED;
       }
       return ConfigChangeType.MODIFIED;
  }
    // 添加配置变更监听器
  void addListener(ConfigurationListener configurationListener) {
        this.listeners.add(configurationListener);
  }
    // 删除配置变更监听器
  void removeListener(ConfigurationListener configurationListener) {
       this.listeners.remove(configurationListener);
  }
    
  boolean hasInternalListener() {
       return listeners != null && listeners.size() > 0;
  }
}

最后是Configuration接口方法的实现,比较简单

@Override
public String getInternalProperty(String key) {
    return dubboConfig.getProperty(key, null);
}

ZookeeperDynamicConfiguration则是利用Zookeeper实现配置动态变更。dubbo内部对Zookeeper的使用,全部借助Curator框架(有兴趣的同学可以研究一下,这里不做展开)。同样的,需要重点关注构造方法、配置变更监听器的管理以及Configuration接口方法,这里主要借助Curator的TreeCache(用作配置数据的缓存以及配置变更事件的响应)和TreeCacheListener实现。下面直接来看代码

ZookeeperDynamicConfiguration(URL url) {
    // 基础参数初始化
    this.url = url;
    rootPath = "/" + url.getParameter(CONFIG_NAMESPACE_KEY, DEFAULT_GROUP) + "/config";
        // 线程池参数,用于newFixedThreadPool
    RetryPolicy policy = new ExponentialBackoffRetry(1000, 3);
    int sessionTimeout = url.getParameter("config.session.timeout", 60 * 1000);
    int connectTimeout = url.getParameter("config.connect.timeout", 10 * 1000);
    String connectString = url.getBackupAddress();
    client = newClient(connectString, sessionTimeout, connectTimeout, policy);
    client.start();
    try {
        // 连接检测,默认等待时间30s
        boolean connected = client.blockUntilConnected(3 * connectTimeout, TimeUnit.MILLISECONDS);
        if (!connected) {
            if (url.getParameter(Constants.CONFIG_CHECK_KEY, true)) {
                throw new IllegalStateException("Failed to connect to config center (zookeeper): "
                        + connectString + " in " + 3 * connectTimeout + "ms.");
            } else {
                logger.warn("The config center (zookeeper) is not fully initialized in " + 3 * connectTimeout + "ms, address is: " + connectString);
            }
        }
    } catch (InterruptedException e) {
        throw new IllegalStateException("The thread was interrupted unexpectedly when trying connecting to zookeeper "+ connectString + " config center, ", e);
    }
        // 闭锁,防止数据不一致
    initializedLatch = new CountDownLatch(1);
    // 缓存变更监听器
    this.cacheListener = new CacheListener(rootPath, initializedLatch);
    this.executor = Executors.newFixedThreadPool(1, new NamedThreadFactory(this.getClass().getSimpleName(), true));
    // build local cache,构建本地缓存
    try {
        this.buildCache();
    } catch (Exception e) {
        logger.warn("Failed to build local cache for config center (zookeeper), address is ." + connectString);
    }
}

private void buildCache() throws Exception {
            // 初始化treeCode
        this.treeCache = new TreeCache(client, rootPath);
        // create the watcher for future configuration updates
        treeCache.getListenable().addListener(cacheListener, executor);
                // it's not blocking, so we use an extra latch 'initializedLatch' to make sure cache fully initialized before use.
        treeCache.start();
            //开启闭锁
        initializedLatch.await();
}

构造方法中使用了CacheListener,我们直接来看CacheListener,而且CacheListener也是实现对配置变更监听器管理的核心类。CacheListener基于Curator的TreeCacheListener扩展,对dubbo的ConfigurationListener进行管理(添加、删除)。CacheListenter的其他逻辑不用关注,直接来看对配置变更事件的处理

@Override
public void childEvent(CuratorFramework aClient, TreeCacheEvent event) throws Exception {
        TreeCacheEvent.Type type = event.getType();
    ChildData data = event.getData();
    //初始化事件,则关闭比闭锁
    if (type == TreeCacheEvent.Type.INITIALIZED) {
        initializedLatch.countDown();
        return;
    }

    // TODO, ignore other event types,代码省略

    // TODO We limit the notification of config changes to a specific path level, for example
    //  /dubbo/config/service/configurators, other config changes not in this level will not get notified,
    //  say /dubbo/config/dubbo.properties
    if (data.getPath().split("/").length >= 5) {
        byte[] value = data.getData();
        String key = pathToKey(data.getPath());
        ConfigChangeType changeType;
        switch (type) {
            case NODE_ADDED:
                changeType = ConfigChangeType.ADDED;
                break;
            case NODE_REMOVED:
                changeType = ConfigChangeType.DELETED;
                break;
            case NODE_UPDATED:
                changeType = ConfigChangeType.MODIFIED;
                break;
            default:
                return;
        }

        ConfigChangeEvent configChangeEvent = new ConfigChangeEvent(key, new String(value, StandardCharsets.UTF_8), changeType);
        Set listeners = keyListeners.get(key);
        // 真实处理逻辑,调用dubbo的ConfigurationListener处理逻辑
        if (CollectionUtils.isNotEmpty(listeners)) {
            listeners.forEach(listener -> listener.process(configChangeEvent));
        }
    }
}
@Override
// dubbo的Configuration接口方法实现
public Object getInternalProperty(String key) {
    //直接借助treeCache实现。
    ChildData childData = treeCache.getCurrentData(key);
    if (childData != null) {
        return new String(childData.getData(), StandardCharsets.UTF_8);
    }
    return null;
}

MockDynamicConfiguration和NopDynamicConfiguration主要用于动态配置的降级和保底,内部没有实际逻辑,不再做解析。

关于dubbo的Configuration就介绍到这里了,做个小结。dubbo内部通过静态配置与动态配置相结合,对外统一表现为CompositeConfiguration,在dubbo服务暴露、引用过程中提供属性初始化;然后,服务运行、消费期间,监听配置变更,并同步至URL、Invoker。

注:dubbo服务版本2.7.1,欢迎指正。

你可能感兴趣的:(dubbo之配置(Configuration))