引言

前面几篇文章分析了Dubbo的核心工作原理,本篇将对之前涉及到但却未细讲的服务目录进行深入分析,在开始之前先结合前面的文章思考下什么是服务目录?它的作用是什么?

正文

概念及作用

清楚Dubbo的调用过程就知道Dubbo在客户端和服务端都会为服务生成一个Invoker执行体,这个Invoker包含了所有的配置信息,也相当于是一个代理对象,所以这也就引发出几个问题:

  • 怎么管理Invoker?不可能让用户自己去管理,也不可能客户端每次调用服务时都新创建Invoker。
  • 当服务存在集群时,选择使用哪一个Invoker?怎么选择?
  • 服务列表变化时,Invoker列表怎么更新?

针对以上问题,Dubbo引入了服务目录的概念,简单的说就是Invoker的集合,由框架自身统一管理Invoker列表,并且提供订阅服务功能,使得服务变更时,会自动更新Invoker列表;同时当存在集群时,可以使得外部以统一的方法使用Invoker,即用户不用关心怎么选择使用哪一个Invoker(在之前的源码分析中我们看到过cluster.join就是实现该功能的API,将多个Invoker合成一个Invoker)。

继承结构

了解了基本概念后,我们来看看服务目录的继承体系:



Directory继承Node接口,该接口是Dubbo中节点的高度抽象,它提供了获取url配置、判断节点是否可用以及销毁节点的接口,由各个子类实现,只要是和服务节点相关的实现都可以实现该接口。比如Invoker、Directory、Registry等。目录的内置实现有StaticDirectory和RegistryDirectory两个,第一个是静态目录服务,其中的Inovker列表是不会改变的;而RegistryDirectory实现了NotifyListener接口,表示会监听注册中心节点的变化,当节点信息改变时,RegistryDirectory中的Inovker列表会自动更新。

源码分析

AbstractDirectory

从上面的继承图我们可以看到StaticDirectory和RegistryDirectory都继承了AbstractDirectory,可以猜到多半又是模板方法模式的实现,确实也是这样,AbstractDirectory提供了获取Invoker的接口,而具体的实现则是子类自行实现的,我们来看看其源码:

public abstract class AbstractDirectory<T> implements Directory<T> {

    public List<Invoker<T>> list(Invocation invocation) throws RpcException {
if (destroyed){
throw new RpcException("Directory already destroyed .url: "+ getUrl());
}
// 获取invoker列表,由子类实现
List<Invoker<T>> invokers = doList(invocation);
// 本地路由列表
List<Router> localRouters = this.routers;
if (localRouters != null && localRouters.size() > 0) {
for (Router router: localRouters){
try {
// 是否需要进行路由
if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, true)) {
invokers = router.route(invokers, getConsumerUrl(), invocation);
}
} catch (Throwable t) {
logger.error("Failed to execute router: " + getUrl() + ", cause: " + t.getMessage(), t);
}
}
}
return invokers;
} protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException ; // 省略其它方法 }

这个方法逻辑很简单,没什么好说的,其中路由配置可自行了解,本文不打算展开,下面主要看看子类是如何实现doList方法以及如何管理Inovker的。

RegistryDirectory

刚说了RegistryDirectory是动态的目录服务,会监听注册中心的节点变化,并自动刷新Invoker列表,用户可以通过它拿到实时的Invoker列表,下面就主要分析这三部分是如何实现的。

注册监听及自动更新Invoker列表

public void subscribe(URL url) {
setConsumerUrl(url);
registry.subscribe(url, this);
}

注册监听很简单,客户端创建Zookeeper连接时,会添加监听器监听节点变化,该监听器最终会调用到RegistryDirectory的subscribe方法,使得目录也可以监听节点变化,当节点发生变化时,又会触发NotifyListener的notify方法,RegistryDirectory就实现了该方法:

public synchronized void notify(List<URL> urls) {
// 存放provider节点下的url
List<URL> invokerUrls = new ArrayList<URL>();
// 存放router节点下的url
List<URL> routerUrls = new ArrayList<URL>();
// 存放configurator节点下的url
List<URL> configuratorUrls = new ArrayList<URL>();
// 每次传入的url都是节点下所有的url,并非增量
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
// 根据url的协议和分类存放到对应的List中
if (Constants.ROUTERS_CATEGORY.equals(category)
|| Constants.ROUTE_PROTOCOL.equals(protocol)) {
routerUrls.add(url);
} else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
|| Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
configuratorUrls.add(url);
} else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
} else {
// 忽略不支持的url
logger.warn("Unsupported category " + category + " in notified url: " + url + " from registry " + getUrl().getAddress() + " to consumer " + NetUtils.getLocalHost());
}
}
// 缓存configurator url
if (configuratorUrls != null && configuratorUrls.size() >0 ){
this.configurators = toConfigurators(configuratorUrls);
}
// 设置路由
if (routerUrls != null && routerUrls.size() >0 ){
List<Router> routers = toRouters(routerUrls);
if(routers != null){ // null - do nothing
setRouters(routers);
}
}
List<Configurator> localConfigurators = this.configurators; // local reference
// 合并override参数
this.overrideDirectoryUrl = directoryUrl;
if (localConfigurators != null && localConfigurators.size() > 0) {
for (Configurator configurator : localConfigurators) {
this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
}
}
// 根据provider节点下的url刷新invoker
refreshInvoker(invokerUrls);
}

这个方法主要缓存各种类型的url以及配置路由,Invoker的刷新主要是在refreshInvoker方法中:

private void refreshInvoker(List<URL> invokerUrls){
if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
// invokerUrls中只有一个url且协议为empty,则禁用所有服务
this.forbidden = true; // 禁止访问
this.methodInvokerMap = null; // 置空列表
destroyAllInvokers(); // 关闭所有Invoker
} else {
this.forbidden = false; // 允许访问
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference
if (invokerUrls.size() == 0 && this.cachedInvokerUrls != null){
// invokerUrls为空但缓存中有url,则将缓存中的url添加到invokerUrls
invokerUrls.addAll(this.cachedInvokerUrls);
} else {
// 将invokerUrls中的所有url缓存起来,便于交叉对比
this.cachedInvokerUrls = new HashSet<URL>();
this.cachedInvokerUrls.addAll(invokerUrls);
}
if (invokerUrls.size() ==0 ){
return;
}
// 将URL列表转成Invoker列表,key是url
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
// 换方法名映射Invoker列表
Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap);
// 转换出错,抛出异常
if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0 ){
logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :"+invokerUrls.size() + ", invoker.size :0. urls :"+invokerUrls.toString()));
return ;
}
// 合并多个invoker
this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
this.urlInvokerMap = newUrlInvokerMap;
try{
// 销毁无用的invoker
destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap);
}catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}

该方法首先会判断是否需要禁用服务,若不需要,则将url转化为Invoker,并将方法名映射到对应的Invoker,紧接着将多组Invoker合并后赋值给this.methodInvokerMap变量(该变量会在doList遍历Invoker时用到),最后会销毁掉缓存中已经无用的Invoker,避免调用到已怠机的服务。以上就是Invoker自动刷新的流程,其中各个依赖方法的细节感兴趣的可自行分析,下面就一起来看看如何获取Invoker列表。

获取Invoker列表

public List<Invoker<T>> doList(Invocation invocation) {
// 服务提供者关闭或禁止了服务抛出异常
if (forbidden) {
throw new RpcException(RpcException.FORBIDDEN_EXCEPTION, "Forbid consumer " + NetUtils.getLocalHost() + " access service " + getInterface().getName() + " from registry " + getUrl().getAddress() + " use dubbo version " + Version.getVersion() + ", Please check registry access list (whitelist/blacklist).");
}
List<Invoker<T>> invokers = null;
// 本地Invoker列表,在refreshInvoker中合并赋值
Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap;
if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
// 获取方法名和参数列表
String methodName = RpcUtils.getMethodName(invocation);
Object[] args = RpcUtils.getArguments(invocation);
// 第一个参数为String或者Enum,不太清楚有什么意义
if(args != null && args.length > 0 && args[0] != null
&& (args[0] instanceof String || args[0].getClass().isEnum())) {
invokers = localMethodInvokerMap.get(methodName + "." + args[0]); // 可根据第一个参数枚举路由
}
// 根据方法名获取Inovker列表
if(invokers == null) {
invokers = localMethodInvokerMap.get(methodName);
}
// 根据“*”获取Invoker列表
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 ? new ArrayList<Invoker<T>>(0) : invokers;
}

这个逻辑也非常清晰,就是根据方法名或“*”拿到对应的Invoker列表。以上就是动态服务目录的实现原理,下面再来看看静态服务目录。

StaticDirectory

public class StaticDirectory<T> extends AbstractDirectory<T> {

    private final List<Invoker<T>> invokers;

    public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
super(url == null && invokers != null && invokers.size() > 0 ? invokers.get(0).getUrl() : url, routers);
if (invokers == null || invokers.size() == 0)
throw new IllegalArgumentException("invokers == null");
this.invokers = invokers;
} public void destroy() {
if(isDestroyed()) {
return;
}
super.destroy();
for (Invoker<T> invoker : invokers) {
invoker.destroy();
}
invokers.clear();
} @Override
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException { return invokers;
} }

相比较而言,静态服务目录就简单多了,通过构造器创建,由于没有提供更新的方法,所以一旦创建就不会改变,而读取Inovker列表只需要将自身变量返回即可。

总结

服务目录的原理我们搞清楚了,再结合前面几篇文章,我们能够掌握Dubbo的核心实现原理。现在我们再来看看Dubbo的架构图:



抛开种种繁杂的功能,你会发现这个架构和RMI以及我们之前手写实现的RPC架构没太大区别,所以,大道至简,掌握基础才能更加快速地理解更复杂的框架应用。

至此,Dubbo系列暂时就写到这了,但其本身做为一个优秀的开源框架,发展这么多年,不可能这几篇文章就涵盖完全了,其它的诸如序列化、路由、Monitor以及文中未做详细分析的部分,读者们可自行阅读源码分析。

Dubbo——服务目录的更多相关文章

  1. K8S(09)交付实战-通过流水线构建dubbo服务

    k8s交付实战-流水线构建dubbo服务 目录 k8s交付实战-流水线构建dubbo服务 1 jenkins流水线准备工作 1.1 参数构建要点 1.2 创建流水线 1.2.1 创建流水线 1.2.2 ...

  2. dubbo源码阅读之服务目录

    服务目录 服务目录对应的接口是Directory,这个接口里主要的方法是 List<Invoker<T>> list(Invocation invocation) throws ...

  3. Dubbo源码(五) - 服务目录

    前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 今天,来聊聊Dubbo的服务目录(Directory).下面是官方文档对服务目录的定义: 服务目 ...

  4. dubbo服务自动化测试搭建

    java实现dubbo的消费者服务编写:ruby实现消费者服务的接口测试:通过消费者间接测试dubbo服务接口的逻辑 内容包括:dubbo服务本地调用环境搭建,dubbo服务启动,消费者部署,脚本编写 ...

  5. Dubbo_创建Dubbo服务并在ZooKeeper注册,然后通过Jar包执行

    一.安装ZooKeeper(略) 二.创建Dubbo服务  1.DemoService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ...

  6. 跟我学习dubbo-使用Maven构建Dubbo服务的可执行jar包(4)

    Dubbo服务的运行方式: 1.使用Servlet容器运行(Tomcat.Jetty等)----不可取 缺点:增加复杂性(端口.管理) 浪费资源(内存) 官方:服务容器是一个standalone的启动 ...

  7. Tomcat中部署web应用 ---- Dubbo服务消费者Web应用war包的部署

    样例视频:http://www.roncoo.com/course/view/f614343765bc4aac8597c6d8b38f06fd IP: 192.168.2.61 部署容器:apache ...

  8. dubbo服务运行的三种方式

    dubbo服务运行,也就是让生产服务的进程一直启动.如果生产者进程挂掉,也就不存在生产者,消费者不能进行消费. Dubbo服务运行的三种方式如下:1.使用Servlet容器运行(Tomcat.Jett ...

  9. RPC -dubbo 服务导出实现

    在阅读此文章之前,我希望阅读者对Spring 扩展机制的有一定的了解,比如:自定义标签与Spring整合, InitializingBean 接口,ApplicationContextAware,Be ...

随机推荐

  1. python脚本实现接口自动化轻松搞定上千条接口用例

    接口自动化目前是测试圈主流的一个话题,我也在网上搜索了很多关于自动化的关键词,大多数博主分享的python做接口自动化都是以开源的框架,比如:pytest.unittest+ddt(数据驱动) 最常见 ...

  2. Java-main方法中调用非static方法

    java的calss中,在public static void main(String[] args) { }方法中调用非static的方法:在main方法中创建该calss的对象,用对象调用非sta ...

  3. C#中的any和all

    any是判断列表里面是否有哪怕一个: all是判断列表里面是否每一项都包含:

  4. linux 去除^M 换行符

    一般,在windows下写的shell脚本,都会去linux执行,都会有^M 符号,那么怎么去除呢? 第一种方法:cat -A filename 就可以看到windows下的断元字符 ^M要去除他,最 ...

  5. 0507 构造代码块和static案例,接口interface

    0507构造代码块和static案例,接口interface [重点] 1.局部变量,成员变量,静态变量的特点 2.接口 接口语法:interface A {} 接口内的成员变量[缺省属性]publi ...

  6. SSI PAYLOAD

    <pre><!--#exec cmd="ls" --></pre><pre><!--#echo var="DATE_ ...

  7. Java rmi漏洞利用及原理记录

    CVE-2011-3556 该模块利用了RMI的默认配置.注册表和RMI激活服务,允许加载类来自任何远程(HTTP)URL.当它在RMI中调用一个方法时分布式垃圾收集器,可通过每个RMI使用endpo ...

  8. Chisel3 - 接口方向(Direction)

    https://mp.weixin.qq.com/s/36jreQGpDLCCNfmUwI34lA   模块接口有三种方向:Input/Output/Inout.Chisel在声明模块接口的时候,也需 ...

  9. 【大厂面试02期】Redis过期key是怎么样清理的?

    PS:本文已收录到1.1K Star数开源学习指南--<大厂面试指北>,如果想要了解更多大厂面试相关的内容,了解更多可以看 http://notfound9.github.io/inter ...

  10. 【Flume】知识总结

    Flume是Cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集.聚合和传输的系统,Flume支持在日志系统中定制各类数据发送方,用于收集数据:同时,Flume提供对数据进行简单处理,并 ...