服务目录

服务目录对应的接口是Directory,这个接口里主要的方法是

List<Invoker<T>> list(Invocation invocation) throws RpcException;

列出所有的Invoker,对于服务消费端而言,一个Invoker对应一个可用的服务提供者,底层封装了一个tcp连接。当然Invoker也可以是嵌套的,一个Invoker内包含了多个实际的Invoker。通过Cluster对象将一个服务目录封装成一个Invoker,内部包含了故障转移,服务路由,负载均衡,等等相关的集群逻辑。

回到服务目录,主要包括两种服务目录,StaticDirectory,RegistryDirectory。

  • StaticDirectory。静态服务目录,顾名思义,这个目录在创建的时候就会通过构造方法传进一个Invoker列表,在之后过程中这个列表不再变化。
  • RegistryDirectory。通过监听注册中心的服务提供者信息动态更新Invoker列表的服务目录。

从上节服务引入,我们知道,不论是StaticDirectory还是RegistryDirectory,最终都会通过Cluster.join方法封装为一个Invoker。由于静态服务目录的逻辑很简单,这里不再赘述,本节我们主要分析一下注册中心的服务目录。

RegistryDirectory概述

这个类除了继承了AbstractDirectory,还实现了NotifyListener接口。NotifyListener接口是一个监听类,用于监听注册中心配置信息的变更事件。我们首先简单看一下RegistryDirectory中实现Directory接口的部分代码。

AbstractDirectory.list

list方法的实现放在抽象类AbstractDirectory中,

public List<Invoker<T>> list(Invocation invocation) throws RpcException {
if (destroyed) {
throw new RpcException("Directory already destroyed .url: " + getUrl());
} return doList(invocation);
}

wishing就是一个状态的判断。doList是一个模板方法,由子类实现。

RegistryDirectory.doList

@Override
public List<Invoker<T>> doList(Invocation invocation) {
// 当状态量forbidden为true时,服务调用被禁止
// 什么时候forbidden为true呢??当url只有一个,且协议名称为empty时,就以为这没有服务提供者可用。
if (forbidden) {
// 1. No service provider 2. Service providers are disabled
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "No provider available from registry " +
getUrl().getAddress() + " for service " + getConsumerUrl().getServiceKey() + " on consumer " +
NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() +
", please check status of providers(disabled, not registered or in blacklist).");
} // 服务分组
if (multiGroup) {
return this.invokers == null ? Collections.emptyList() : this.invokers;
} List<Invoker<T>> invokers = null;
try {
// Get invokers from cache, only runtime routers will be executed.
// 从缓存中取出Invoker列表,并经由服务路由获取相应的Invoker
invokers = routerChain.route(getConsumerUrl(), invocation);
} catch (Throwable t) {
logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
} // FIXME Is there any need of failing back to Constants.ANY_VALUE or the first available method invokers when invokers is null?
/*Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap; // local reference
if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
String methodName = RpcUtils.getMethodName(invocation);
invokers = localMethodInvokerMap.get(methodName);
if (invokers == null) {
invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
}
if (invokers == null) {
Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
if (iterator.hasNext()) {
invokers = iterator.next();
}
}
}*/
return invokers == null ? Collections.emptyList() : invokers;
}

这个方法的主要逻辑是,首先判断服务是否可用(根据forbidden状态变量)。然后从路由链中取出Invoker列表。由于服务路由并不是本节的重点,所以我们只是简单第看一下RouterChain.route方法

RouterChain.route

public List<Invoker<T>> route(URL url, Invocation invocation) {
List<Invoker<T>> finalInvokers = invokers;
for (Router router : routers) {
finalInvokers = router.route(finalInvokers, url, invocation);
}
return finalInvokers;
}

一次调用路由列表中的路由规则,最终返回经过多个路由规则路由过的Invoker列表。类似于责任链模式,有点像web容器的过滤器,或者是spring-mvc中的拦截器,都是一个链式的调用。

实际上我们平时一般较少使用到路由功能,所以这里routers列表实际上是空的,这种情况下不用经过任何路由,直接原样返回Invokers列表。而至于RouterChain内部的invokers成员是哪来的,RegistryDirectory监听注册中心发生变更后刷新本地缓存中的Invokers列表,并将其注入到RouterChain对象中,我们后面会讲到。

RegistryDirectory.notify

接下来我们分析RegistryDirectory中最重要的方法,也就是监听方法,用于监听注册中心的变更事件。

public synchronized void notify(List<URL> urls) {
// 将监听到的url分类,
// 按照协议名称或者category参数分为configurators,routers,providers三类
Map<String, List<URL>> categoryUrls = urls.stream()
.filter(Objects::nonNull)
.filter(this::isValidCategory)
.filter(this::isNotCompatibleFor26x)
.collect(Collectors.groupingBy(url -> {
if (UrlUtils.isConfigurator(url)) {
return CONFIGURATORS_CATEGORY;
} else if (UrlUtils.isRoute(url)) {
return ROUTERS_CATEGORY;
} else if (UrlUtils.isProvider(url)) {
return PROVIDERS_CATEGORY;
}
return "";
})); // 如果有变化的configurators类别的url,那么将其转化为参数并设到成员变量configurators
List<URL> configuratorURLs = categoryUrls.getOrDefault(CONFIGURATORS_CATEGORY, Collections.emptyList());
this.configurators = Configurator.toConfigurators(configuratorURLs).orElse(this.configurators); // 如果有变更的路由信息url,那么将其转化为Router对象并覆盖原先的路由信息
List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
toRouters(routerURLs).ifPresent(this::addRouters); // providers
// 最后处理最重要的服务提供者变更信息,并用这些url刷新当前缓存的Invoker
List<URL> providerURLs = categoryUrls.getOrDefault(PROVIDERS_CATEGORY, Collections.emptyList());
refreshOverrideAndInvoker(providerURLs);
}

首先将从注册中心获取到的最新的url进行分类,根据协议名称或者category参数将url分为三类:configurators, routers, providers,

  • configurators类型的url被转换为Configurator列表,覆盖本地缓存
  • routers类型的url被转换为Router列表,并被设置到routerChain对象中
  • providers类型的url则被用于接下来的创建Invoker

RegistryDirectory.refreshOverrideAndInvoker

private void refreshOverrideAndInvoker(List<URL> urls) {
// mock zookeeper://xxx?mock=return null
// 用变更的配置信息覆盖overrideDirectoryUrl成员变量
overrideDirectoryUrl();
// 刷新缓存中的Invokers
refreshInvoker(urls);
}

overrideDirectoryUrl方法的作用主要是用从注册中心以及配置中心监听到的变更的配置覆盖本地的overrideDirectoryUrl成员变量中的配置。我们接着往下走。

RegistryDirectory.refreshInvoker

// 入参invokerUrls是从注册中心拉取的服务提供者url
private void refreshInvoker(List<URL> invokerUrls) {
Assert.notNull(invokerUrls, "invokerUrls should not be null"); // 如果只有一个服务提供者,并且协议名称是empty,说明无提供者可用
// 将状态forbidden设为true, invokers设为空列表
if (invokerUrls.size() == 1
&& invokerUrls.get(0) != null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden = true; // Forbid to access
this.invokers = Collections.emptyList();
routerChain.setInvokers(this.invokers);
destroyAllInvokers(); // Close all invokers
} else {
this.forbidden = false; // Allow to access
// 记下旧的Invoker列表
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
if (invokerUrls == Collections.<URL>emptyList()) {
invokerUrls = new ArrayList<>();
}
// 如果从注册中心没有拉取到服务提供者信息,那么使用之前缓存的服务提供者信息
// 这就是为什么dubbo在注册中心挂了之后消费者仍然能够调用提供者,因为消费者在本地进行了缓存
if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
invokerUrls.addAll(this.cachedInvokerUrls);
} else {
this.cachedInvokerUrls = new HashSet<>();
this.cachedInvokerUrls.addAll(invokerUrls);//Cached invoker urls, convenient for comparison
}
// 如果注册中心没有提供者信息,并且本地也没有缓存,那么就没法进行服务调用了
if (invokerUrls.isEmpty()) {
return;
}
// 将服务提供者url转化为Invoker对象存放到map中
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map /**
* If the calculation is wrong, it is not processed.
*
* 1. The protocol configured by the client is inconsistent with the protocol of the server.
* eg: consumer protocol = dubbo, provider only has other protocol services(rest).
* 2. The registration center is not robust and pushes illegal specification data.
*
*/
if (CollectionUtils.isEmptyMap(newUrlInvokerMap)) {
logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" + invokerUrls.size() + ", invoker.size :0. urls :" + invokerUrls
.toString()));
return;
} List<Invoker<T>> newInvokers = Collections.unmodifiableList(new ArrayList<>(newUrlInvokerMap.values()));
// pre-route and build cache, notice that route cache should build on original Invoker list.
// toMergeMethodInvokerMap() will wrap some invokers having different groups, those wrapped invokers not should be routed.
// 将生成的Invoker列表设置到routerChain的缓存中,
// routerChain将对这些Invoker进行路由
routerChain.setInvokers(newInvokers);
// 处理服务分组的情况
this.invokers = multiGroup ? toMergeInvokerList(newInvokers) : newInvokers;
// 将缓存的Invoker设置为新生成的
this.urlInvokerMap = newUrlInvokerMap; try {
// 这里实际上求新的Invoker列表和旧的差集,将不再使用的旧的Invoker销毁
destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
} catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}
  • 这个方法首先根据监听到的提供者url列表判断是否处于服务禁用状态,判断依据是:如果只有一个url,并且该url协议名称是empty,说明无提供者可用,将forbidden变量设为true,即禁止服务调用,

    并做一下其他的相关设置以及销毁缓存中的Invoker。

  • 如果不是禁止状态,继续往下走。如果从注册中心获取到的url列表为空,那么检查本地缓存的url列表是否为空,如果缓存不为空就用缓存的列表。如果本地缓存也为空,说明无服务可用,直接返回。

  • 如果如果从注册中心获取到的url列表不为空,说明有服务可用,这时就不会再去尝试本地缓存了(因为缓存已经过期了),并且将本地缓存更新为新获取的url列表。

  • 将可用的提供者url列表转化为Invoker列表。

  • 将新创建的Invoker列表设置到routerChain中,这里呼应了前文提到的在doList方法中,从routerChain对象中取出缓存的Invoker列表。

  • 将本地缓存的url->Invoker map更新为新创建的。

  • 最后销毁缓存中不再使用的Invoker

RegistryDirectory.toInvokers

/**
* Turn urls into invokers, and if url has been refer, will not re-reference.
*
* @param urls 从注册中心拉取的服务提供者信息
* @return invokers
*/
private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<>();
if (urls == null || urls.isEmpty()) {
return newUrlInvokerMap;
}
// 用于防止对相同的url重复创建Invoker
Set<String> keys = new HashSet<>();
String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
for (URL providerUrl : urls) {
// If protocol is configured at the reference side, only the matching protocol is selected
// 如果消费端配置了协议名称,那么只有符合条件的提供者url才会被使用
// 这段代码有待商榷 ,应该先把queryProtocols处理好,避免重复做同样的工作
if (queryProtocols != null && queryProtocols.length() > 0) {
boolean accept = false;
String[] acceptProtocols = queryProtocols.split(",");
for (String acceptProtocol : acceptProtocols) {
if (providerUrl.getProtocol().equals(acceptProtocol)) {
accept = true;
break;
}
}
if (!accept) {
continue;
}
}
// 如果协议名称是empty,那么忽略该条url
if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
continue;
}
// 如果当前classpath下找不到与提供者url中协议名称相对应的Protocol类,那么打印错误日志同时忽略该条url
if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
logger.error(new IllegalStateException("Unsupported protocol " + providerUrl.getProtocol() +
" in notified url: " + providerUrl + " from registry " + getUrl().getAddress() +
" to consumer " + NetUtils.getLocalHost() + ", supported protocol: " +
ExtensionLoader.getExtensionLoader(Protocol.class).getSupportedExtensions()));
continue;
}
// 合并消费端设置的参数以及从注册中心,配置中心监听到的配置变更
URL url = mergeUrl(providerUrl); // 以全路径作为该url的唯一标识
String key = url.toFullString(); // The parameter urls are sorted
if (keys.contains(key)) { // Repeated url
continue;
}
keys.add(key);
// Cache key is url that does not merge with consumer side parameters, regardless of how the consumer combines parameters, if the server url changes, then refer again
Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap; // local reference
// 如果之前已经创建过该url的Invoker对象,那么就不用再重复创建
Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
if (invoker == null) { // Not in the cache, refer again
try {
boolean enabled = true;
// 检查disabled和enabled参数的值
if (url.hasParameter(Constants.DISABLED_KEY)) {
enabled = !url.getParameter(Constants.DISABLED_KEY, false);
} else {
enabled = url.getParameter(Constants.ENABLED_KEY, true);
}
if (enabled) {
// 真正创建Invoker的地方,
// InvokerDelegate只是个简单的包装类,不需要多说
invoker = new InvokerDelegate<>(protocol.refer(serviceType, url), url, providerUrl);
}
} catch (Throwable t) {
logger.error("Failed to refer invoker for interface:" + serviceType + ",url:(" + url + ")" + t.getMessage(), t);
}
if (invoker != null) { // Put new invoker in cache
newUrlInvokerMap.put(key, invoker);
}
} else {
newUrlInvokerMap.put(key, invoker);
}
}
keys.clear();
return newUrlInvokerMap;
}
  • 首先根据协议名称检查url是否可用。url的协议必须在本地配置的协议列表中(如果没有配置就不需要做此检查);如果协议名称是empty则忽略这个url;如果当前classpath下找不到与提供者url中协议名称相对应的Protocol类,那么打印错误日志同时忽略该条url
  • 合并消费端设置的参数以及从注册中心,配置中心监听到的配置变更
  • 检查disabled,enabled参数的值,判断该url是否启用,如果disabled为true则跳过该url;如果没有disabled参数,检查enabled参数,如果enabled为false则跳过该url,enabled默认是true。
  • 调用Protocol.refer方法创建Invoker对象。

这里需要说明一下,由于Directory不是通过SPI机制加载的,所以RegistryDirectory也不是通过ExtensionLoader加载的,所以也就不会受到ExtensionLoader的IOC影响。RegistryDirectory内部的protocol成员是在RegistryDirectory初始化之后通过调用setter方法设置进去的,是在RegistryProtocol.doRefer方法中完成的。而RegistryProtocol是通过ExtensionLoader机制加载的,会受到IOC影响,所以RegistryProtocol实例内部的protocol成员是通过ExtensionLoader的IOC机制自动注入的,是一个自适应的扩展类。

另外,InvokerDelegate只是个简单的包装类,不需要多说。

Invoker的创建最终还是通过protocol.refer方法,我们以最常用的dubbo协议为例进行分析。

DubboProtocol.refer

@Override
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
optimizeSerialization(url); // create rpc invoker.
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker); return invoker;
}

这个方法很简单,直接new了一个DubboInvoker。

DubboInvoker

看一下doInvoke方法,这个方法主要是处理了同步,异步,超时,单向调用等参数,并且对调用结果封装了异步调用,同步调用的逻辑。

真正执行远程调用的部分是靠ExchangeClient实现的,再往下就是调用参数的序列化,tcp连接创建,发送报文,获取响应报文,反序列化结果等的逻辑了,本文不再深入下去。

dubbo源码阅读之服务目录的更多相关文章

  1. dubbo源码阅读之服务引入

    服务引入 服务引入使用reference标签来对要引入的服务进行配置,包括服务的接口 ,名称,init,check等等配置属性. 在DubboNamespaceHandler中,我们可以看到refer ...

  2. dubbo源码阅读之服务导出

    dubbo服务导出 常见的使用dubbo的方式就是通过spring配置文件进行配置.例如下面这样 <?xml version="1.0" encoding="UTF ...

  3. 【Dubbo源码阅读系列】服务暴露之远程暴露

    引言 什么叫 远程暴露 ?试着想象着这么一种场景:假设我们新增了一台服务器 A,专门用于发送短信提示给指定用户.那么问题来了,我们的 Message 服务上线之后,应该如何告知调用方服务器,服务器 A ...

  4. 【Dubbo源码阅读系列】服务暴露之本地暴露

    在上一篇文章中我们介绍 Dubbo 自定义标签解析相关内容,其中我们自定义的 XML 标签 <dubbo:service /> 会被解析为 ServiceBean 对象(传送门:Dubbo ...

  5. 【Dubbo源码阅读系列】之远程服务调用(上)

    今天打算来讲一讲 Dubbo 服务远程调用.笔者在开始看 Dubbo 远程服务相关源码的时候,看的有点迷糊.后来慢慢明白 Dubbo 远程服务的调用的本质就是动态代理模式的一种实现.本地消费者无须知道 ...

  6. 【Dubbo源码阅读系列】之 Dubbo SPI 机制

    最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解.如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出. Dubbo SPI 介绍 Java ...

  7. Dubbo源码学习之-服务导出

    前言 忙的时候,会埋怨学习的时间太少,缺少个人的空间,于是会争分夺秒的工作.学习.而一旦繁忙的时候过去,有时间了之后,整个人又会不自觉的陷入一种懒散的状态中,时间也显得不那么重要了,随便就可以浪费掉几 ...

  8. Dubbo源码阅读顺序

    转载: https://blog.csdn.net/heroqiang/article/details/85340958 Dubbo源码解析之配置解析篇,主要内容是<dubbo:service/ ...

  9. Dubbo源码阅读-服务导出

    Dubbo服务导出过程始于Spring容器发布刷新事件,Dubbo在接收到事件后,会立即执行服务导出逻辑.整个逻辑大致可分为三个部分,第一部分是前置工作,主要用于检查参数,组装URL.第二部分是导出服 ...

随机推荐

  1. ICEM——对msh文件或者cas文件重新划分边界

    原视频下载地址:https://pan.baidu.com/s/1jIoKSuy 密码: m3uv

  2. EasyExcel写入百万级数据到多sheet---非注解方式

    EasyExcel是什么? 快速.简单避免OOM的java处理Excel工具 一.项目需求 从mongo库中查询数据,导出到excel文件中.但是动态导出的excel有多少列.列名是什么.有多少she ...

  3. Systemback制作大于4G的Ubuntu系统镜像

    1 安装Systemback 依此执行如下命令. sudo apt-get update sudo add-apt-repository ppa:nemh/systemback sudo apt-ge ...

  4. php怎么用正则取出网址中某个参数?

    $str = <<<TEXT 如下类似网址: https://v.qq.com/iframe/player.html?vid=j00169ib5er&tiny=0&a ...

  5. FFMPEG Tips 如何提取码流的基本信息

    原文连接: https://zhuanlan.zhihu.com/p/23448271 1. 码流中的哪些信息值得关注 ? [ ] 是否包含:音频.视频 [ ] 码流的封装格式 [ ] 视频的编码格式 ...

  6. golang 基于channel封装资源池(可用于封装redis、mq连接池)

    package pool import ( "errors" "io" "sync" "time" ) var ( Er ...

  7. 【VS开发】【C/C++开发】memcpy和memmove的区别

    memcpy和memmove()都是C语言中的库函数,在头文件string.h中,作用是拷贝一定长度的内存的内容,原型分别如下: void *memcpy(void *dst, const void ...

  8. java开发异常Exception集锦

    背景:整理开发过程中的异常问题 java.lang.Exception: No tests found matching 一般出现在新导入的工程中.在sts中通过open project的方式导入工程 ...

  9. spring security进阶2 添加账户并对账户密码进行加密

    目录 spring security 添加账户并对账户密码进行加密 一.原理分析 1.1加密原理 1.2加密后的登录过程 二.代码实现 2.1添加用户的页面如下, register.html 2.2c ...

  10. 一、Spring之组件注册-@Configuration&@Bean给容器中注册组件

    xml配置方式 首先我们创建一个实体类Person public class Person { private String name; private Integer age; private St ...