前言
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实现。大致可以分为三步:
- 先从系统属性获取配置,配置值非空则直接返回,否则执行2;
- 加载制定配置文件,加载的优先级顺序: 系统属性中配置的"dubbo.properties.file"文件 > 系统环境变量中配置的"dubbo.properties.file" > classPath下的dubbo.properties文件。
- 配置值中有占位符,则使用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,欢迎指正。