Dubbo之注册中心

概述

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信息

  1. 当URL设置了check=false时,注册失败后不报错,在后台定时重试,否则抛出异常。FailbackRegistry实现了该功能,由定时器进行重试
  2. 当URL设置了dynamic=false参数,则需持久存储,否则,当注册者出现断电等情况异常退出时,需自动删除。以ZookeeperRegistry为例,dynamic=false,就会创建一个临时的节点
  3. 当URL设置了category=routers时,表示分类存储,缺省类别为providers,可按分类部分通知数据。以ZookeeperRegistry为例,每个Service节点下面分为四个类型的节点,分别存放url信息
  4. 当注册中心重启,网络抖动,不能丢失数据,包括断线自动删除数据。以ZookeeperRegistry为例,新增监听器,Zookeeper重连时,调用recover()自动恢复

subscribe(URL url, NotifyListener listener)

参数url,表示订阅条件

  1. 当URL设置了check=false时,订阅失败后不报错,在后台定时重试。FailbackRegistry实现了该功能,由定时器进行重试
  2. 当URL设置了category=routers,只通知指定分类的数据,多个分类用逗号分隔,并允许星号通配,表示订阅所有分类数据。向指定的category类型发起订阅
  3. 允许以interface,group,version,classifier作为条件查询,如:interface=com.alibaba.foo.BarService&version=1.0.0,并且查询条件允许星号通配,订阅所有接口的所有分组的所有版本,或:interface=*&group=*&version=*&classifier=*。根据条件进行过滤,符合条件的才会进行下发
  4. 当注册中心重启,网络抖动,需自动恢复订阅请求。以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));
}

匹配顺序:

  1. 比较interface
  2. 比较category
  3. 比较是否启用
  4. 比较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);
    }
}

逻辑简述:

  1. null判断
  2. 将urls结合,根据category分组,分为category -> urls
  3. 依次根据category,回调触发器推送urls信息
  4. 将每次推送记录下来

 

转载于:https://my.oschina.net/cregu/blog/2245408

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值