概述
Dubbo需要进行远程调用,远程通信需要建立服务端和客户端,那么客户端建立连接的时候必须知道服务端的信息。注册中心的好处是让服务端和客户端进行解耦,客户端不需要直接配置服务端的信息,而是从注册中心去获取。服务端启动的时候,将自身注册到注册中心。由注册中心统一去管理所有服务端的信息,这样服务端也可以随意变更,从而也不会影响客户端的使用。
Registry和RegistryFactory
Registry不是Dubbo SPI的扩展接口,RegistryFactory是Dubbo SPI的扩展接口,默认的扩展名是dubbo,代表DubboRegistryFactory。
RegistryFactory是Registry的工厂,用于创建Registry。
RegistryFactory
@SPI("dubbo")
public interface RegistryFactory {
/**
* 连接注册中心.
*
* 连接注册中心需处理契约:<br>
* 1. 当设置check=false时表示不检查连接,否则在连接不上时抛出异常。<br>
* 2. 支持URL上的username:password权限认证。<br>
* 3. 支持backup=10.20.153.10备选注册中心集群地址。<br>
* 4. 支持file=registry.cache本地磁盘文件缓存。<br>
* 5. 支持timeout=1000请求超时设置。<br>
* 6. 支持session=60000会话超时或过期设置。<br>
*
* @param url 注册中心地址,不允许为空
* @return 注册中心引用,总不返回空
*/
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
根据Dubbo SPI 的自适配机制,根据URL中的protocol参数获取具体扩展实现。
public class RegistryFactory$Adpative implements com.alibaba.dubbo.registry.RegistryFactory {
public com.alibaba.dubbo.registry.Registry getRegistry(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null)
throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null)
throw new IllegalStateException(
"Fail to get extension(com.alibaba.dubbo.registry.RegistryFactory) name from url(" + url
.toString() + ") use keys([protocol])");
com.alibaba.dubbo.registry.RegistryFactory extension = (com.alibaba.dubbo.registry.RegistryFactory) ExtensionLoader
.getExtensionLoader(com.alibaba.dubbo.registry.RegistryFactory.class).getExtension(extName);
return extension.getRegistry(arg0);
}
}
AbstractRegistryFactory
实现了getRegistry(),增加了通用逻辑,新增模板方法createRegistry() , 由子类实现。一个key对应一个Registry对象。
/**
* 首先从缓存中获取,没有就创建,一个注册中心url对应一个注册中心
*
* @param url 注册中心地址,不允许为空
* @return
*/
public Registry getRegistry(URL url) {
url = url.setPath(RegistryService.class.getName())
.addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
// 格式:protocol://username:password@ip:port/group/service:version
String key = url.toServiceString();
// Lock the registry access process to ensure a single instance of the registry
LOCK.lock();
try {
Registry registry = REGISTRIES.get(key);
if (registry != null) {
return registry;
}
registry = createRegistry(url);
if (registry == null) {
throw new IllegalStateException("Can not create registry " + url);
}
REGISTRIES.put(key, registry);
return registry;
} finally {
// Release the lock
LOCK.unlock();
}
}
ZookeeperRegistryFactory
创建ZookeeperRegistry对象
public class ZookeeperRegistryFactory extends AbstractRegistryFactory {
/**
* Dubbo SPI注入的,是自适应对象
*/
private ZookeeperTransporter zookeeperTransporter;
public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
this.zookeeperTransporter = zookeeperTransporter;
}
public Registry createRegistry(URL url) {
return new ZookeeperRegistry(url, zookeeperTransporter);
}
}
Registry
继承RegistryService,定义了注册(取消注册),订阅(取消订阅),查询。
public interface RegistryService {
/**
* 注册数据,比如:提供者地址,消费者地址,路由规则,覆盖规则,等数据。
* 注册需处理契约:<br>
* 1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。<br>
* 2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。<br>
* 3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。<br>
* 4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。<br>
* 5. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
*
* @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
*/
void register(URL url);
/**
* 取消注册.
* 取消注册需处理契约:<br>
* 1. 如果是dynamic=false的持久存储数据,找不到注册数据,则抛IllegalStateException,否则忽略。<br>
* 2. 按全URL匹配取消注册。<br>
*
* @param url 注册信息,不允许为空,如:dubbo://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
*/
void unregister(URL url);
/**
* 订阅符合条件的已注册数据,当有注册数据变更时自动推送.
* 订阅需处理契约:<br>
* 1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。<br>
* 2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。<br>
* 3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0<br>
* 4. 并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*<br>
* 5. 当注册中心重启,网络抖动,需自动恢复订阅请求。<br>
* 6. 允许URI相同但参数不同的URL并存,不能覆盖。<br>
* 7. 必须阻塞订阅过程,等第一次通知完后再返回。<br>
*
* @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
* @param listener 变更事件监听器,不允许为空
*/
void subscribe(URL url, NotifyListener listener);
/**
* 取消订阅.
* 取消订阅需处理契约:<br>
* 1. 如果没有订阅,直接忽略。<br>
* 2. 按全URL匹配取消订阅。<br>
*
* @param url 订阅条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
* @param listener 变更事件监听器,不允许为空
*/
void unsubscribe(URL url, NotifyListener listener);
/**
* 查询符合条件的已注册数据,与订阅的推模式相对应,这里为拉模式,只返回一次结果。
*
* @param url 查询条件,不允许为空,如:consumer://10.20.153.10/com.alibaba.foo.BarService?version=1.0.0&application=kylin
* @return 已注册信息列表,可能为空,含义同{@link com.alibaba.dubbo.registry.NotifyListener#notify(List<URL>)}的参数。
* @see com.alibaba.dubbo.registry.NotifyListener#notify(List)
*/
List<URL> lookup(URL url);
}
Dubbo框架为了做了注释,对每个方法的注释做下解释。
register(URL url)
参数url,表示需要注册到注册中心的url信息
- 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。FailbackRegistry实现了该功能,由定时器进行重试
- 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。以ZookeeperRegistry为例,dynamic=false,就会创建一个临时的节点
- 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。以ZookeeperRegistry为例,每个Service节点下面分为四个类型的节点,分别存放url信息
- 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。以ZookeeperRegistry为例,新增监听器,Zookeeper重连时,调用recover()自动恢复
subscribe(URL url, NotifyListener listener)
参数url,表示订阅条件
- 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。FailbackRegistry实现了该功能,由定时器进行重试
- 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。向指定的category类型发起订阅
- 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0,并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*。根据条件进行过滤,符合条件的才会进行下发
- 当注册中心重启,网络抖动,需自动恢复订阅请求。以ZookeeperRegistry为例,新增监听器,Zookeeper重连时,调用recover()自动恢复
AbstractRegistry
抽象了公用逻辑
FailbackRegistry
继承AbstractRegistry,对注册(取消注册)和订阅(取消订阅)方法都增加了重试机制。
public void register(URL url) {
super.register(url);
failedRegistered.remove(url);
failedUnregistered.remove(url);
try {
// 向服务器端发送注册请求
// Sending a registration request to the server side
doRegister(url);
} catch (Exception e) {
Throwable t = e;
// 如果开启了启动时检测,则直接抛出异常
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true)
&& !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
// 将失败的注册请求记录到失败列表,定时重试
// Record a failed registration request to a failed list, retry regularly
failedRegistered.add(url);
}
}
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener);
try {
// 向服务器端发送订阅请求
// Sending a subscription request to the server side
doSubscribe(url, listener);
} catch (Exception e) {
Throwable t = e;
// 从缓存中获取订阅url
List<URL> urls = getCacheUrls(url);
if (urls != null && urls.size() > 0) {
notify(url, listener, urls);
logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(Constants.FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t);
} else {
// 如果开启了启动时检测,则直接抛出异常
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true);
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
}
// 将失败的订阅请求记录到失败列表,定时重试
// Record a failed registration request to a failed list, retry regularly
addFailedSubscribed(url, listener);
}
}
ZookeeperRegistry
zookeeper是树形的目录结构,Dubbo 中的zk注册中心的目录结构为:
流程说明:
- 服务提供者启动时: 向
/dubbo/com.foo.BarService/providers
目录下写入自己的 URL 地址 - 服务消费者启动时: 订阅
/dubbo/com.foo.BarService/providers
目录下的提供者 URL 地址。并向/dubbo/com.foo.BarService/consumers
目录下写入自己的 URL 地址 - 监控中心启动时: 订阅
/dubbo/com.foo.BarService
目录下的所有提供者和消费者 URL 地址。
支持以下功能:
-
当提供者出现断电等异常停机时,注册中心能自动删除提供者信息
-
当注册中心重启时,能自动恢复注册数据,以及订阅请求
- 当会话过期时,能自动恢复注册数据,以及订阅请求
- 当设置
<dubbo:registry check="false" />
时,记录失败注册和订阅请求,后台定时重试 - 可通过
<dubbo:registry username="admin" password="1234" />
设置 zookeeper 登录信息 - 可通过
<dubbo:registry group="dubbo" />
设置 zookeeper 的根节点,不设置将使用无根树 - 支持
*
号通配符<dubbo:reference group="*" version="*" />
,可订阅服务的所有分组和所有版本的提供者
构造方法
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
super(url);
if (url.isAnyHost()) {
throw new IllegalStateException("registry address == null");
}
// group作为zookeeper的root节点名称,默认是dubbo
String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
if (!group.startsWith(Constants.PATH_SEPARATOR)) {
group = Constants.PATH_SEPARATOR + group;
}
this.root = group;
zkClient = zookeeperTransporter.connect(url);
// 增加监听器,使用匿名类
zkClient.addStateListener(new StateListener() {
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
// 状态为重连接,进行恢复逻辑,内存中保存注册的信息,和订阅信息
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
protected void doRegister(URL url) {
try {
// dynamic=false 表示持久化存储
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));
} catch (Throwable e) {
throw new RpcException(
"Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(),
e);
}
}
protected void doUnregister(URL url) {
try {
zkClient.delete(toUrlPath(url));
} catch (Throwable e) {
throw new RpcException(
"Failed to unregister " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(),
e);
}
}
创建对象的同时,会创建zkclient对象(zkclient对象也是由动态适配的,参照DuDubbo之Zookeeper)很多方法交由它进行处理,比如注册和取消注册就是创建和删除节点。Zookeeper注册中心就是一个Zookeeper的树,里面放置了提供者和订阅者的信息,这种信息都是以URL形式存储的。利用Zookeeper的watch机制,做到对变更的推送。
节点路径由URL进行转换,将URL转换成path,然后通过zookeeper客户端,维护目录。
/**
* 构建url路径:Root / Service / Category / Url
*
* @param url
* @return
*/
private String toUrlPath(URL url) {
return toCategoryPath(url) + Constants.PATH_SEPARATOR + URL.encode(url.toFullString());
}
目录路径格式为:root/service/category/url,比如:/dubbo/com.foo.BarService/providers/url信息,
- root可以通过 <dubbo:registry group/> 设置,默认为dubbo
- category总共有四种类型:providers、consumers、routers、configurators
- 最尾段是具体的URL信息,URL.encode(url.toFullString()),表示一个具体的地址
- root、service、category这三个目录都是持久节点,URL类型节点则根据`dynamic`参数,`dynamic=true`表示临时节点
订阅信息
订阅者订阅指定的节点,在节点上创建监听器,当提供者发生变更,新增或者关闭的时候,节点信息会变更。相应的监听器就会触发,将变更之后的子节点的信息推送给订阅者。
注册中心怎么知道订阅的是什么信息呢?是根据订阅条件,这个订阅条件是URL表示的。
protected void doSubscribe(final URL url, final NotifyListener listener) {
try {
if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
// 如果是*,订阅所有Service节点,比如监控中心的订阅
} else {
// 订阅指定Service节点
}
} catch (Throwable e) {
throw new RpcException(
"Failed to subscribe " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(),
e);
}
}
监听器ChildListener
Dubbo中定义了一个ChildListener接口,作用是在Zookeeper某个节点的字节点发生变更的时候触发。Dubbo中没有直接的实现,都是在ZookeeperRegistry中匿名实现。
public interface ChildListener {
/**
* path子节点发生变化的监听
*
* @param path
* @param children
*/
void childChanged(String path, List<String> children);
}
监听器StateListener
Dubbo中定义了一个StateListener接口,这个监听器定义了一种状态分别是:disconnected、connected、reconnetd,对应ZkClient的状态机。Dubbo中没有直接的实现,在ZookeeperRegistry构造函数,创建了StateListener的匿名类。用于在ZkCLient重连Zookeeper时候,做到自动恢复。
public interface StateListener {
int DISCONNECTED = 0;
int CONNECTED = 1;
int RECONNECTED = 2;
/**
* 状态发生变化监听
*
* @param connected
*/
void stateChanged(int connected);
}
// ZookeeperRegistry构造函数代码片段
// 增加监听器,使用匿名类,用于自动恢复
zkClient.addStateListener(new StateListener() {
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
// 状态为重连接,进行恢复逻辑,内存中保存注册的信息,和订阅信息
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
指定Service
多数都是只对某个Service发起订阅,比如消费者在启动的时候,需要引用某个服务,就会订阅某个服务,以便后续动态的获取服务提供者的信息,方便后续容错和负载均衡。具体的做法实在指定的Service节点上,创建并且添加监听器,这个监听器会监听这个节点的变更。Dubbo会将根据订阅条件(URL)从子节点中过滤出符合条件的才会下发给订阅者。
// 订阅指定Service节点
// 从url中判断需要订阅的分组信息,获取每个分组下面符合条件的url数组
List<URL> urls = new ArrayList<URL>();
// toCategoriesPath(url) 从参数URL中提取需要订阅的category路径
for (String path : toCategoriesPath(url)) {
// 获取zkListener
// 以防万一先创建节点,不存在才会创建,false表示持久化,这个path是Category节点
zkClient.create(path, false);
// 每次重新绑定监听器,获取子节点信息
List<String> children = zkClient.addChildListener(path, zkListener);
if (children != null) {
urls.addAll(toUrlsWithEmpty(url, path, children));
}
}
// 首次订阅时手动触发,获得全量urls数据,触发回调NotifyListener#notify()方法
notify(url, listener, urls);
获取zkListener代码片段
逻辑很简单,先从缓存获取,不命中才创建新的。监听器触发会调用notify()方法。
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
listeners = zkListeners.get(url);
}
// 创建zookeeper监听器
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
// 某个分组path下面的字节点发生变更,就会触发这个方法,
// 从而回调触发NotifyListener#notify()
// 什么时候数据会变更呢,比如新增一个服务提供者,或者某个服务提供者断掉,Providers
// 节点下面的信息发生了变更,会将变更之后的节点信息触发回调
public void childChanged(String parentPath, List<String> currentChilds) {
ZookeeperRegistry.this
.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
}
});
zkListener = listeners.get(listener);
}
所有Service
对所有的Service都订阅,比如监控中心。会在root节点上新增监听器,这样就能获取root下的所有service类型的节点,也就能做到订阅每个service了。
// 如果是*,订阅所有Service节点,比如监控中心的订阅
String root = toRootPath();
// 获取zkListener
// 以防万一先创建节点,不存在才会创建,false表示持久化,这个path是Category节点
zkClient.create(root, false);
// 每次重新绑定监听器,获取子节点信息
// 是在root节点上新增监听器,所以返回的是所有Service类型的节点信息
List<String> services = zkClient.addChildListener(root, zkListener);
// 循环对每个Service节点发起订阅
if (services != null && services.size() > 0) {
for (String service : services) {
service = URL.decode(service);
anyServices.add(service);
subscribe(url.setPath(service)
.addParameters(Constants.INTERFACE_KEY, service, Constants.CHECK_KEY,
String.valueOf(false)), listener);
}
}
获取zkListener代码片段
逻辑很简单,先从缓存获取,不命中才创建新的。监听器触发会调用subscribe()方法。
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null) {
zkListeners.putIfAbsent(url, new ConcurrentHashMap<NotifyListener, ChildListener>());
listeners = zkListeners.get(url);
}
// 创建zookeeper监听器
ChildListener zkListener = listeners.get(listener);
if (zkListener == null) {
listeners.putIfAbsent(listener, new ChildListener() {
public void childChanged(String parentPath, List<String> currentChilds) {
for (String child : currentChilds) {
child = URL.decode(child);
// child是Service类型节点
// service没有订阅过才订阅,不重复订阅
if (!anyServices.contains(child)) {
anyServices.add(child);
// 有Service变更时,对指定Service节点发起订阅
subscribe(url.setPath(child)
.addParameters(Constants.INTERFACE_KEY, child, Constants.CHECK_KEY,
String.valueOf(false)), listener);
}
}
}
});
zkListener = listeners.get(listener);
}
两者的区别
指定Service和所有Service的逻辑大同小异,主要的区别:
- 监听器的位置不一样,前者是某个Service节点,后者是Root节点
- 监听器的逻辑不一样,前者是调用notify(),后者是调用subscribe(),其实调用subscribe(),还是走到了指定Service的逻辑
匹配条件
任何一个订阅者在订阅某个Service的指定Category的时候,都会获得这个Category下的所有URL信息,然后会根据订阅条件从中进行匹配过滤,只返回自己感兴趣的URL。如果一个都没有,就返回一个默认的URL,(empty://)
/**
* 获得 providers 中和 consumer 匹配的url数组,如果为空,新增一个empty://的url返回
*
* @param consumer
* @param path
* @param providers
* @return
*/
private List<URL> toUrlsWithEmpty(URL consumer, String path, List<String> providers) {
List<URL> urls = toUrlsWithoutEmpty(consumer, providers);
if (urls == null || urls.isEmpty()) {
// empty://
int i = path.lastIndexOf('/');
String category = i < 0 ? path : path.substring(i + 1);
URL empty = consumer.setProtocol(Constants.EMPTY_PROTOCOL).addParameter(Constants.CATEGORY_KEY, category);
urls.add(empty);
}
return urls;
}
private List<URL> toUrlsWithoutEmpty(URL consumer, List<String> providers) {
List<URL> urls = new ArrayList<URL>();
if (providers != null && providers.size() > 0) {
for (String provider : providers) {
provider = URL.decode(provider);
if (provider.contains("://")) {
URL url = URL.valueOf(provider);
if (UrlUtils.isMatch(consumer, url)) {
urls.add(url);
}
}
}
}
return urls;
}
public static boolean isMatch(URL consumerUrl, URL providerUrl) {
String consumerInterface = consumerUrl.getServiceInterface();
String providerInterface = providerUrl.getServiceInterface();
// 先比较interface
// 如果consumerInterface!=*,并且consumerInterface和providerInterface不相等,则不匹配
if (!(Constants.ANY_VALUE.equals(consumerInterface) || StringUtils
.isEquals(consumerInterface, providerInterface)))
return false;
// 分组是否匹配
if (!isMatchCategory(providerUrl.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY),
consumerUrl.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY))) {
return false;
}
// provider的enabled=false,并且consumer的enabled不等于*,则不匹配
if (!providerUrl.getParameter(Constants.ENABLED_KEY, true) && !Constants.ANY_VALUE
.equals(consumerUrl.getParameter(Constants.ENABLED_KEY))) {
return false;
}
// 从group、version、classifier三个维度去比较
String consumerGroup = consumerUrl.getParameter(Constants.GROUP_KEY);
String consumerVersion = consumerUrl.getParameter(Constants.VERSION_KEY);
String consumerClassifier = consumerUrl.getParameter(Constants.CLASSIFIER_KEY, Constants.ANY_VALUE);
String providerGroup = providerUrl.getParameter(Constants.GROUP_KEY);
String providerVersion = providerUrl.getParameter(Constants.VERSION_KEY);
String providerClassifier = providerUrl.getParameter(Constants.CLASSIFIER_KEY, Constants.ANY_VALUE);
return (Constants.ANY_VALUE.equals(consumerGroup)
|| StringUtils.isEquals(consumerGroup, providerGroup)
|| StringUtils.isContains(consumerGroup, providerGroup)) && (Constants.ANY_VALUE
.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion)) && (
consumerClassifier == null
|| Constants.ANY_VALUE.equals(consumerClassifier)
|| StringUtils.isEquals(consumerClassifier, providerClassifier));
}
匹配顺序:
- 比较interface
- 比较category
- 比较是否启用
- 比较group、version、classifier,支持*通配符
notify()
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
// null 检查
if (url == null) {
throw new IllegalArgumentException("notify url == null");
}
if (listener == null) {
throw new IllegalArgumentException("notify listener == null");
}
if ((urls == null || urls.size() == 0)
&& !Constants.ANY_VALUE.equals(url.getServiceInterface())) {
logger.warn("Ignore empty notify urls for subscribe url " + url);
return;
}
if (logger.isInfoEnabled()) {
logger.info("Notify urls for subscribe url " + url + ", urls: " + urls);
}
// 将urls,根据category分组
// category -> urls
Map<String, List<URL>> result = new HashMap<String, List<URL>>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) {
// 默认分组是providers
String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
List<URL> categoryList = result.get(category);
if (categoryList == null) {
categoryList = new ArrayList<URL>();
result.put(category, categoryList);
}
categoryList.add(u);
}
}
if (result.size() == 0) {
return;
}
// 获取categoryNotified,记录下url
Map<String, List<URL>> categoryNotified = notified.get(url);
if (categoryNotified == null) {
notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
categoryNotified = notified.get(url);
}
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
categoryNotified.put(category, categoryList);
saveProperties(url);
// 触发监听器的回调方法
listener.notify(categoryList);
}
}
逻辑简述:
- null判断
- 将urls结合,根据category分组,分为category -> urls
- 依次根据category,回调触发器推送urls信息
- 将每次推送记录下来