Dubbo源码---服务订阅

264 阅读11分钟

Dubbo服务订阅的接口定义在dubbo-registry包中,接口为RegistryService。先来看RegistryService的源码:

public interface RegistryService {

    /**
     * 1.当URL设置check=false参数时。当注册失败时,不会抛出异常并在后台重试。
     * 2. 当URL设置category=routers时,只通知指定的分类数据。多个分类用逗号分隔,允许用星号进行匹配,表示订阅了所有分类数据。
     * 3.允许接口、组、版本和分类器作为条件查询,例如:interface=org.apache.dubbo.foo.BarService&version=1.0.0 
     * 4。并且查询条件允许星号匹配,订阅所有接口的所有包的所有版本,例如:interface=*&group=*&version=*&classifier=* 
     * 5。当注册表重新启动和网络抖动时,需要自动恢复订阅请求。
     * 6. 允许具有相同URL但不同参数的URL共存,它们不能相互覆盖。
     * 7. 当第一个通知完成并返回时,必须阻止订阅过程。
     * @param url      订阅条件,不能为null, 例如: consumer://10.20.153.10/org.apache.dubbo.foo.BarService?version=1.0.0&application=kylin
     * @param listener 事件更改的监听器,不允许为空
     */
    void subscribe(URL url, NotifyListener listener);

    /**
     * 取消订阅
     * <p>
     * 为了支持契约,需要取消订阅
     * 1. 如果不订阅,直接忽略它
     * 2. 取消订阅完全URL匹配
     *
     * @param url      订阅条件,不能为null. 例如: consumer://10.20.153.10/org.apache.dubbo.foo.BarService?version=1.0.0&application=kylin
     * @param listener 更改事件监听器,不允许为null
     */
    void unsubscribe(URL url, NotifyListener listener);

}

订阅

subscribe的入口有两个,一个是在服务导出时,通知订阅的服务进行注册表的更新,一个是在服务引用时,订阅该服务的所有provider。在服务导出中,其入口时RegistryProtocol#export方法,在服务引用时,其入口在RegistryProtocol#doRefer方法中,通过注册目录(RegistryDirectory)的subscribe方法进行订阅。接下下我们先分析下这两个入口

服务导出入口

上面有提过,subscribe在服务导出的入口是RegistryProtocol#export方法,下面我们来分析下

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
    // 获取注册中心 URL,以 zookeeper 注册中心为例,得到的示例 URL 如下:
    // zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2
    // &export=dubbo%3A%2F%2F172.17.48.52%3A20880%2Fcom.alibaba.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider
    URL registryUrl = getRegistryUrl(originInvoker);

    // 获取provider的url
    // dubbo://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider
    // &bind.ip=192.168.1.101&bind.port=20880&default=true&deprecated=false&dubbo=2.0.2&dynamic=true
    // &generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync
    // &pid=15770&release=&side=provider&timestamp=1635326008251
    URL providerUrl = getProviderUrl(originInvoker);

    // 订阅URL,比如:
    // provider://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider
    // &bind.ip=192.168.1.101&bind.port=20880&category=configurators&check=false&default=true&deprecated=false
    // &dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote
    // &methods=sayHello,sayHelloAsync&pid=15770&release=&side=provider&timestamp=1635326008251
    final URL overrideSubscribeUrl = getSubscribedOverrideUrl(providerUrl);
    final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
    overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

    // TODO
    providerUrl = overrideUrlWithConfig(providerUrl, overrideSubscribeListener);

    // 导出Export
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // 根据 URL 加载 Registry 实现类,比如 ZookeeperRegistry
    final Registry registry = getRegistry(originInvoker);
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

    // 是否需要延迟加载
    boolean register = providerUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 向注册中心注册服务
        register(registryUrl, registeredProviderUrl);
    }

    // 在提供程序模型上注册声明的url
    registerStatedUrl(registryUrl, registeredProviderUrl, register);


    exporter.setRegisterUrl(registeredProviderUrl);
    exporter.setSubscribeUrl(overrideSubscribeUrl);

    // 弃用!订阅以覆盖2.6中的规则。x或之前。
    registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

    notifyExport(exporter);
    // 确保每次导出时都返回一个新的导出器实例
    return new DestroyableExporter<>(exporter);
}

由上面的方法可以看出,在服务导出的过程中,会将解析获取到的providerUrl转换成overrideSubscribeUrl,在这个过程中将providerUrl的协议如dubbo、redis、http、hassian等转换成provider,并添加key为category,value为configurators、key为check,value为false的参数;此外通过overrideSubscribeUrl和RegistryProtocol#export方法的参数Invoker构建OverrideListener,最后调用registry.subscribe(overrideSubscribeUrl,overrideSubscribeListener)方法进行订阅。这里需要注意overrideSubscribeUrl的几处改变:

  1. 将protocol设置为provider
  2. 添加category参数,值为configurators
  3. 添加check参数,值为false

转换之后的overrideSubscribeUrl的格式为:provider://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=dubbo-demo-api-provider&bind.ip=192.168.1.101&bind.port=20880&category=configurators&check=false&default=true&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&metadata-type=remote&methods=sayHello,sayHelloAsync&pid=15770&release=&side=provider&timestamp=1635326008251。在服务服务导出中,最终传递给RegistryService#subscribe方法的URL为上面的url,加粗部分为转换的协议和添加的参数。最后,来分析下RegistryProtocol#export方法中registry是如何获取到的:

private RegistryFactory registryFactory;
​
public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
  // 省略无关代码
  final Registry registry = getRegistry(originInvoker);
  // 省略无关代码
}
​
protected Registry getRegistry(final Invoker<?> originInvoker) {
    URL registryUrl = getRegistryUrl(originInvoker);
    return registryFactory.getRegistry(registryUrl);
}

这里的代码很简单,但是中间转换了好几次,但都是围绕Dubbo SPI来做的,下面简单介绍下执行流程,自己可根据需要自行调试:

  1. 在执行RegistryProtocol#export之前,通过Dubbo SPI获取到了RegistryProtocol,并在注入时将registryFacotry赋值为RegistryFacotry的自适应扩展类RegistryFactory$Adaptive
  2. 调用RegistryFactory$Adaptive#getRegistry方法,通过解析URL获取到Registry,如DubboRegistry、NacosRegistry,又因在Dubbo SPI配置中配置了ListenerRegistryWrapper包装类,所以,此时获取到的ListenerRegistryWrapper实例。

服务引用入口

前文有提过,subscribe在服务导出的入口是RegistryProtocol#doRefer方法:

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    // 设置Registry
    directory.setRegistry(registry);
    // 设置Protocol
    directory.setProtocol(protocol);
    // all attributes of REFER_KEY
    Map<String, String> parameters = new HashMap<String, String>(directory.getConsumerUrl().getParameters());
    // 构建消费者订阅的URL
    URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
    // 是否需要注册
    if (directory.isShouldRegister()) {
        // 设置消费者URL
        directory.setRegisteredConsumerUrl(subscribeUrl);
        // 注册
        registry.register(directory.getRegisteredConsumerUrl());
    }
    // 设置路由
    directory.buildRouterChain(subscribeUrl);
    // 订阅
    directory.subscribe(toSubscribeUrl(subscribeUrl));
    // 一个注册中心可能有多个服务提供者,因此这里需要将多个服务提供者合并为一个
    Invoker<T> invoker = cluster.join(directory);
    List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
    if (CollectionUtils.isEmpty(listeners)) {
        return invoker;
    }
​
    RegistryInvokerWrapper<T> registryInvokerWrapper = new RegistryInvokerWrapper<>(directory, cluster, invoker);
    for (RegistryProtocolListener listener : listeners) {
        // 监听
        listener.onRefer(this, registryInvokerWrapper);
    }
    return registryInvokerWrapper;
}

从上面的代码可看出subsribeUrl经过了两次转换

  1. 通过构造URL构造方法,subscribeUrl的格式为:consumer://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?XXX=xxx
  2. 通过toSubscribeUrl方法,subscribeUrl变为:consumer://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers,configurators,routers&XXX=xxx

接下里继续看RegistryDirectory#subscribe方法:

public void subscribe(URL url) {
    // 设置消费者URL
    setConsumerUrl(url);
    // 添加通知监听器
    CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);
    serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
    // 订阅
    registry.subscribe(url, this);
}

此方法非常简单,在这里发起了具体的Registry#subsribe调用,并在这里指定了服务更改的监听器--RegistryDirectory,这一点很重要,不管用的nacos,zk、eureka或其他注册中心,当服务发生变化时,都会通过此监听器通知消费者

到这里,我们分析出了在服务导出与服务引用时,传递给RegistryService#subscribe方法的URL格式:

  • 服务导出:provider://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=configurators&check=false
  • 服务引用:consumer://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers,configurators,routers&XXX=xxx

订阅

接下来分析具体订阅的源码,因Dubbo框架中的Dubbo SPI配置文件中已配置了Registry的扩展ListenerRegistryWrapper包装类,so,在不配置其他包装类的情况下,subscribe的入口为ListenerRegistryWrapper#subscribe,接下来我们对该方法进行具体的分析:

public void subscribe(URL url, NotifyListener listener) {
    try {
        // 订阅 NacosRegistry
        registry.subscribe(url, listener);
    } finally {
        if (CollectionUtils.isNotEmpty(listeners)) {
            RuntimeException exception = null;
            for (RegistryServiceListener registryListener : listeners) {
                if (registryListener != null) {
                    try {
                        // 监听订阅
                        registryListener.onSubscribe(url);
                    } catch (RuntimeException t) {
                        logger.error(t.getMessage(), t);
                        exception = t;
                    }
                }
            }
            if (exception != null) {
                throw exception;
            }
        }
    }
}

这里做了两件事,一是继续深入执行subcribe方法,二是执行subsribe方法的事件监听。我们分两块来讲解,另外,接下来的subscribe方法将以NacosRegistry#subscribe来分析,其他的如DubboRegistry、ZookeeperRegistry自行分析。由上面的代码可知,在ListenerRegistryWraper#subscribe方法中执行执行了NacosRegistry#subscribe方法,此方法在NacosRegistry的父类FailbackRegistry中:

public void subscribe(URL url, NotifyListener listener) {
    // 在缓存中添加监听器
    super.subscribe(url, listener);
    // 删除订阅失败的数据
    removeFailedSubscribed(url, listener);
    try {
        // 向服务器端发送订阅请求
        doSubscribe(url, listener);
    } catch (Exception e) {
        Throwable t = e;

        List<URL> urls = getCacheUrls(url);
        if (CollectionUtils.isNotEmpty(urls)) {
            notify(url, listener, urls);
            logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(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);
            }
        }

        // 将失败的注册请求记录到失败列表中,定期重试
        addFailedSubscribed(url, listener);
    }
}

在FailbackRegistry#subscribe方法中,做了四件事:

  1. 添加缓存,将providerUrl对应的监听器添加到缓存中
  2. 删除订阅失败的数据,如失败重试的任务、监听器
  3. 向服务端发送订阅请求
  4. 异常处理,将失败的订阅请求记录到失败列表中,定期重试

接下来,深入分析doSubscribe方法,需要下沉到具体的实现类中,继续看NacosRegistry#doSubscribe方法

public void doSubscribe(final URL url, final NotifyListener listener) {
    // 获取服务名
    Set<String> serviceNames = getServiceNames(url, listener);
​
    // 设置相应的servicename,方便以后查找
    if (isServiceNamesWithCompatibleMode(url)) {
        for (String serviceName : serviceNames) {
            NacosInstanceManageUtil.setCorrespondingServiceNames(serviceName, serviceNames);
        }
    }
​
    doSubscribe(url, listener, serviceNames);
}

此方法逻辑不多,首先是获取服务名,其次将获取到名称添加到NacosInstanceManageUtil#CORRESPONDING_SERVICE_NAMES_MAP缓存中,最后进一步执行doSubscribe方法

private void doSubscribe(final URL url, final NotifyListener listener, final Set<String> serviceNames) {
    execute(namingService -> {
      	// url的protocol非admin,且是否是具体的名称
        if (isServiceNamesWithCompatibleMode(url)) {
            List<Instance> allCorrespondingInstanceList = Lists.newArrayList();
            for (String serviceName : serviceNames) {
                // 从nacos注册中心获取所有实例
                List<Instance> instances = namingService.getAllInstances(serviceName,
                        getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP));
                // 添加到缓存
                NacosInstanceManageUtil.initOrRefreshServiceInstanceList(serviceName, instances);
                allCorrespondingInstanceList.addAll(instances);
            }
            // 通知
            notifySubscriber(url, listener, allCorrespondingInstanceList);
            for (String serviceName : serviceNames) {
                // 发布订阅事件
                subscribeEventListener(serviceName, url, listener);
            }
        } else {
            List<Instance> instances = new LinkedList<>();
            for (String serviceName : serviceNames) {
                instances.addAll(namingService.getAllInstances(serviceName
                        , getUrl().getParameter(GROUP_KEY, Constants.DEFAULT_GROUP)));
                notifySubscriber(url, listener, instances);
                subscribeEventListener(serviceName, url, listener);
            }
        }

    });
}

此方法做了以下几件事:

  1. 从注册中心获取所有实例
  2. 将实例添加到缓存
  3. 订阅通知
  4. 订阅事件监听

接着往下看订阅通知

private void notifySubscriber(URL url, NotifyListener listener, Collection<Instance> instances) {
    List<Instance> healthyInstances = new LinkedList<>(instances);
    if (healthyInstances.size() > 0) {
        // 过滤出健康的实例
        filterHealthyInstances(healthyInstances);
    }
    List<URL> urls = toUrlWithEmpty(url, healthyInstances);
    NacosRegistry.this.notify(url, listener, urls);
}

以上的代码很简单,需要注意的toUrlWithEmpth这个方法,在这个方法中可能会有protocol协议的转换,我们深入其逻辑:

private List<URL> toUrlWithEmpty(URL consumerURL, Collection<Instance> instances) {
    List<URL> urls = buildURLs(consumerURL, instances);
    // 构建新的URL,protocol设置为empty
    if (urls.size() == 0) {
        URL empty = URLBuilder.from(consumerURL)
                .setProtocol(EMPTY_PROTOCOL)
                .addParameter(CATEGORY_KEY, DEFAULT_CATEGORY)
                .build();
        urls.add(empty);
    }
    return urls;
}

private List<URL> buildURLs(URL consumerURL, Collection<Instance> instances) {
        List<URL> urls = new LinkedList<>();
        if (instances != null && !instances.isEmpty()) {
            for (Instance instance : instances) {
                URL url = buildURL(instance);
                if (UrlUtils.isMatch(consumerURL, url)) {
                    urls.add(url);
                }
            }
        }
        return urls;
    }

private URL buildURL(Instance instance) {
        Map<String, String> metadata = instance.getMetadata();
        String protocol = metadata.get(PROTOCOL_KEY);
        String path = metadata.get(PATH_KEY);
        return new URL(protocol,
                instance.getIp(),
                instance.getPort(),
                path,
                instance.getMetadata());
    }

根据上述三个方法可看出,遍历Instance列表,获取到providerUrl,其格式为:dubbo://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers,然后通过UrlUtils#isMatch方法确定是否添加到需要返回的URL的列表中,如果返回的URL列表的size为0,则构建一个URL并添加到list中返回,其格式为:empty://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers,下面我们深入去了解下UrlUtils#isMatch方法:>

public static boolean isMatch(URL consumerUrl, URL providerUrl) {
    // 获取consumerInterface
    String consumerInterface = consumerUrl.getServiceInterface();
    // 获取providerInterface
    String providerInterface = providerUrl.getServiceInterface();
    //FIXME accept providerUrl with '*' as interface name, after carefully thought about all possible scenarios I think it's ok to add this condition.
    // 当consumerInterface不为*、providerInterface不为*、consumerInterface和providerInterface不同(不相等的含义看StringUtils#isEquals方法)时,返回false
    if (!(ANY_VALUE.equals(consumerInterface)
            || ANY_VALUE.equals(providerInterface)
            || StringUtils.isEquals(consumerInterface, providerInterface))) {
        return false;
    }
    // 当providerUrl中的category参数的值在consumerUrl的category参数的值中不存在时,返回false
    if (!isMatchCategory(providerUrl.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY),
            consumerUrl.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY))) {
        return false;
    }
    // 当provider的enabled参数为false且consumerUrl的enabled参数不为*时,返回false
    if (!providerUrl.getParameter(ENABLED_KEY, true)
            && !ANY_VALUE.equals(consumerUrl.getParameter(ENABLED_KEY))) {
        return false;
    }

    String consumerGroup = consumerUrl.getParameter(GROUP_KEY);
    String consumerVersion = consumerUrl.getParameter(VERSION_KEY);
    String consumerClassifier = consumerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE);

    String providerGroup = providerUrl.getParameter(GROUP_KEY);
    String providerVersion = providerUrl.getParameter(VERSION_KEY);
    String providerClassifier = providerUrl.getParameter(CLASSIFIER_KEY, ANY_VALUE);
    // 满足以下三个条件时返回true,否则返回false
    // 1、consumerGroup为*,或consumerGroup等于providerGroup,或consumerGroup包含providerGroup
    // 2、consumerVersion为*,或consumerVersion和providerVersion相同
    // 3、consumer的classifier为null,或为*,或consumer和provider的classifier相同
    return (ANY_VALUE.equals(consumerGroup) || StringUtils.isEquals(consumerGroup, providerGroup) || StringUtils.isContains(consumerGroup, providerGroup))
            && (ANY_VALUE.equals(consumerVersion) || StringUtils.isEquals(consumerVersion, providerVersion))
            && (consumerClassifier == null || ANY_VALUE.equals(consumerClassifier) || StringUtils.isEquals(consumerClassifier, providerClassifier));
}

看上面的代码可知,无非就是比较consumerUrl和providerUrl的interface参数、category参数、enabled参数、group参数、version参数、classifier参数,该方法返回结果总结如下:

  1. 当consumerInterface不为 、providerInterface不为、consumerInterface和providerInterface不同(不相等的含义看StringUtils#isEquals方法)时,返回false

  2. 当providerUrl中的category参数的值在consumerUrl的category参数的值中不存在时,返回false

  3. 当provider的enabled参数为false且consumerUrl的enabled参数不为*时,返回false

  4. 满足以下三个条件时返回true,否则返回false

    1. consumerGroup为 ,或consumerGroup等于providerGroup,或consumerGroup包含providerGroup
    2. consumerVersion为*,或consumerVersion和providerVersion相同
    3. consumer的classifier为null,或为*,或consumer和provider的classifier相同

前面我们分析出了服务导出、服务引用时的consumerUrl以及获取到的proderUrl的格式,由此可知,当在服务导出时执行subscribe,通过NacosRegistry#toUrlWithEmpty方法获取到的URL格式为empty://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers,接下来回到这里继续分析,看NacosRegistry.this.notify(url, listener, urls)这行代码,将执行FailbackRegistry#notify(URL, NotifyListener, List)方法:

protected void notify(URL url, NotifyListener listener, List<URL> urls) {
    if (url == null) {
        throw new IllegalArgumentException("notify url == null");
    }
    if (listener == null) {
        throw new IllegalArgumentException("notify listener == null");
    }
    try {
        doNotify(url, listener, urls);
    } catch (Exception t) {
        // 将失败的订阅请求记录到失败列表中,定期重试
        addFailedNotified(url, listener, urls);
        logger.error("Failed to notify for subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
    }
}
​
protected void doNotify(URL url, NotifyListener listener, List<URL> urls) {
        super.notify(url, listener, urls);
    }
​

这里除了两个判空校验外,就是执行FailbackRegistry#doNotify方法后调用AbstractRegistry#notify方法,其次就是对FailbackRegistry#doNotify方法执行异常时的错误处理,就是将失败的订阅请求记录大失败列表中,定期重试,这里先不深入分析起重试机制,后面会在Dubbo源码---重试机制中进行详解。接下里继续深入AbstractRegistry#notify方法:

protected void notify(URL url, NotifyListener listener, List<URL> urls) {
    if (url == null) {
        throw new IllegalArgumentException("notify url == null");
    }
    if (listener == null) {
        throw new IllegalArgumentException("notify listener == null");
    }
    if ((CollectionUtils.isEmpty(urls))
            && !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);
    }
  	/*-------------------------便签1---------------------------------*/
    // 保留每个provider的category
    Map<String, List<URL>> result = new HashMap<>();
    for (URL u : urls) {
        if (UrlUtils.isMatch(url, u)) {
            String category = u.getParameter(CATEGORY_KEY, DEFAULT_CATEGORY);
            List<URL> categoryList = result.computeIfAbsent(category, k -> new ArrayList<>());
            categoryList.add(u);
        }
    }
    if (result.size() == 0) {
        return;
    }
  /*-------------------------便签2---------------------------------*/
    // 添加缓存
    Map<String, List<URL>> categoryNotified = notified.computeIfAbsent(url, u -> new ConcurrentHashMap<>());
    for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
        String category = entry.getKey();
        List<URL> categoryList = entry.getValue();
        categoryNotified.put(category, categoryList);
        listener.notify(categoryList);
        // 我们将在每次通知后更新缓存文件。
      	// 当我们的注册表由于网络抖动而订阅失败时,我们至少可以返回现有的缓存URL。
        saveProperties(url);
    }
}

我们结合前面分析的内容,先对AbstractRegistry#notify方法的三个参数做下简单的分析:

  1. URL(消费者URL):

    1. 服务导出:provider://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=configurators&check=false
    2. 服务引用:consumer://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers,configurators,routers&XXX=xxx
  2. NotifyListener(监听器):

    1. 服务导出:RegistryProtoco.OverrideListener
    2. 服务引用:RegistryDirectory
  3. List(提供者URL):

    1. 服务导出:empty://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers

    2. 服务引用:dubbo://192.168.1.101:20880/org.apache.dubbo.demo.DemoService?category=providers

也就是说,不管subscribe的入口是服务引用还是服务导出,对AbstractRegistry#notify方法的第三个参数影响不大,无非就是改了protocol,最主要的还是第一个参数的改变。当入口为服务导出时,AbstractRegistry#notify方法标签1到标签2之间的逻辑时,subsribe的订阅逻辑在这里就结束了,而后会直接执行ListenerRegistryWraper#subscribe方法的监听逻辑。接下来我们继续标签2下面的逻辑,只不过这之后的逻辑已经跟服务导出时的订阅没关系了,因为下面的逻辑只会发生在服务引用的过程中。 遍历便签1获取的providerUrl,然后发起通知及更新订阅信息到缓存,其中发起通知我们会在Dubbo源码---集群容错之服务目录进行详解。

发起通知

上文有提到过,AbstractRegsitry#notify方法的NotifyListener在服务引用时为RegistryDirectory,接下来看RegistryDirectory#notify方法:

public synchronized void notify(List<URL> urls) {
    // 过滤并排序
    Map<String, List<URL>> categoryUrls = urls.stream()
            .filter(Objects::nonNull)
            .filter(this::isValidCategory)
            .filter(this::isNotCompatibleFor26x)
            .collect(Collectors.groupingBy(this::judgeCategory));
​
    // 获取category为configurators的URL,并赋值到configuration
    List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
    this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators);
​
    // 获取category为routers的URL,并添加到routerChain
    List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
    // 过滤掉routerURLs中protocol为empty到URL,并做了以下四件事
    // 1、过滤掉routerURLs中protocol为empty到URL
    // 2、如果URL中包含router参数,且其值不为null或空字符串时,将该URL的protocol设置为该值
    // 3、通过URL构建Router
    // 4、若返回的Router列表不为空,则将其添加到routerChain中
    toRouters(routerURLs).ifPresent(this::addRouters);
​
    // 获取category为providers的URL,
    List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());
​
    // 3.x 版本添加的逻辑
    // 获取AddressListener
    ExtensionLoader<AddressListener> addressListenerExtensionLoader = ExtensionLoader.getExtensionLoader(AddressListener.class);
    List<AddressListener> supportedListeners = addressListenerExtensionLoader.getActivateExtension(getUrl(), (String[]) null);
    if (supportedListeners != null && !supportedListeners.isEmpty()) {
        for (AddressListener addressListener : supportedListeners) {
            // 处理
            providerURLs = addressListener.notify(providerURLs, getConsumerUrl(),this);
        }
    }
    // 刷新
    refreshOverrideAndInvoker(providerURLs);
}

此方法的逻辑很清晰,分为了五个步骤来做:

  1. 过滤掉不符合条件的providerUrl并排序

  2. 获取category为configurators的URL,并赋值到configuration

  3. 获取category为routers的URL,并并做了以下操作:

    1. 过滤掉routerURLs中protocol为empty到URL
    2. 如果URL中包含router参数,且其值不为null或空字符串时,将该URL的protocol设置为该值
    3. 通过URL构建Router
    4. 若返回的Router列表不为空,则将其添加到routerChain中
  4. 获取category为providers的URL,并做了以下操作

    1. 获取AddressListener扩展
    2. 遍历扩展执行AddressListener#notify方法
  5. 刷新override和Invoker(refreshOverrideAndInvoker)

下面继续深入RegistryDirectory#refreshOverrideAndInvoker方法:

更新缓存

事件监听

回到ListenerRegistryWraper#subscribe方法:

public class ListenerRegistryWrapper implements Registry {
  
  private final Registry registry;
  private final List<RegistryServiceListener> listeners;
​
  public ListenerRegistryWrapper(Registry registry, List<RegistryServiceListener> listeners) {
    this.registry = registry;
    this.listeners = listeners;
  }
  
  public void register(URL url) {
      try {
          // 注册
          registry.register(url);
      } finally {
          if (CollectionUtils.isNotEmpty(listeners)) {
              RuntimeException exception = null;
              for (RegistryServiceListener listener : listeners) {
                  if (listener != null) {
                      try {
                          // 注册监听
                          listener.onRegister(url);
                      } catch (RuntimeException t) {
                          logger.error(t.getMessage(), t);
                          exception = t;
                      }
                  }
              }
              if (exception != null) {
                  throw exception;
              }
          }
      }
  }
}

listeners是一个成员变量,是通过构造方法初始化的。而ListenerRegistryWrapper是通过RegistryFactoryWrapper#getRegistry方法创建的:

public Registry getRegistry(URL url) {
    return new ListenerRegistryWrapper(registryFactory.getRegistry(url),
            Collections.unmodifiableList(ExtensionLoader.getExtensionLoader(RegistryServiceListener.class)
                    .getActivateExtension(url, "registry.listeners")));
}

由上面的代码可看出,ListenerRegistryWrapper#listeners实际上通过Dubbo SPI实例化的,如果使用可参考Dubbo源码---SPI自适应扩展