深入学习Motan系列(四)—— 客户端
困惑的袋鼠,对框架的把握有些茫然,但是仍然一步步向前,行动总比一直迷茫停止不前要好,您说呢,各位客官?
这篇开始客户端的分析。有些地方的代码,就不每段都标出了,中间有跳跃的地方,请自己对照代码来看。鄙人认为,光看武功秘籍没用,得动手debug,您说是不?
文章发了之前,斟酌了一会,因为自己认为写的不理想,有很多东西分析的不透彻,但是,决定还是发布出来,作为自己学习的历程,也希望大家多多给出建议。不断努力。
Demo
当然还是看源码中的tutorial demo了。
 public class MotanApiClientDemo {
     public static void main(String[] args) {
         RefererConfig<MotanDemoService> motanDemoServiceReferer = new RefererConfig<MotanDemoService>();
         // 设置接口及实现类
         motanDemoServiceReferer.setInterface(MotanDemoService.class);
         // 配置服务的group以及版本号
         motanDemoServiceReferer.setGroup("motan-demo-rpc");
         motanDemoServiceReferer.setVersion("1.0");
         motanDemoServiceReferer.setRequestTimeout(1000);
         // 配置注册中心直连调用
         RegistryConfig registry = new RegistryConfig();
         //use direct registry
         //registry.setRegProtocol("direct");
         //registry.setAddress("127.0.0.1:8002");
         // use ZooKeeper registry
         registry.setRegProtocol("zookeeper");
         registry.setAddress("127.0.0.1:2181");
         motanDemoServiceReferer.setRegistry(registry);
         // 配置RPC协议
         ProtocolConfig protocol = new ProtocolConfig();
         protocol.setId("motan");
         protocol.setName("motan");
         motanDemoServiceReferer.setProtocol(protocol);
         // motanDemoServiceReferer.setDirectUrl("localhost:8002");  // 注册中心直连调用需添加此配置
         // 使用服务
34         MotanDemoService service = motanDemoServiceReferer.getRef();
35         System.out.println(service.hello("motan"));
         System.exit(0);
     }
 }
前边的部分跟服务端的差不多,都是配置信息的设置,重点在34行和35行。注意,这边用的是RefererConfig这个类,它和ServiceConfig的关系如下:

(手残,打lol总输5555555555555,图形大家勉强看着,用StarUML画的)
motanDemoServiceReferer.getRef()这行代码中就是Referer的初始化方法
 public T getRef() {
         if (ref == null) {
             initRef();
         }
         return ref;
     }
     @SuppressWarnings({"unchecked", "rawtypes"})
     public synchronized void initRef() {
。。。省略。。。
         checkInterfaceAndMethods(interfaceClass, methods);
         clusterSupports = new ArrayList<>(protocols.size());
         List<Cluster<T>> clusters = new ArrayList<>(protocols.size());
         String proxy = null;
         ConfigHandler configHandler = ExtensionLoader.getExtensionLoader(ConfigHandler.class).getExtension(MotanConstants.DEFAULT_VALUE);
         List<URL> registryUrls = loadRegistryUrls();
         String localIp = getLocalHostAddress(registryUrls);
         for (ProtocolConfig protocol : protocols) {
             Map<String, String> params = new HashMap<>();
。。。省略。。。
             String path = StringUtils.isBlank(serviceInterface) ? interfaceClass.getName() : serviceInterface;
             URL refUrl = new URL(protocol.getName(), localIp, MotanConstants.DEFAULT_INT_VALUE, path, params);
             ClusterSupport<T> clusterSupport = createClusterSupport(refUrl, configHandler, registryUrls);
             clusterSupports.add(clusterSupport);
             clusters.add(clusterSupport.getCluster());
             if (proxy == null) {
                 String defaultValue = StringUtils.isBlank(serviceInterface) ? URLParamType.proxy.getValue() : MotanConstants.PROXY_COMMON;
                 proxy = refUrl.getParameter(URLParamType.proxy.getName(), defaultValue);
             }
         }
         ref = configHandler.refer(interfaceClass, clusters, proxy);
         initialized.set(true);
     }
上面的的代码重要的功能,我用图形表示一下:

上文的initRef()中最重要的当属Cluster的初始化。什么是Cluster?他其实代表provider服务集群,每一个具体的provider服务都被抽象成一个Referer接口,这个接口与客户端的Exporter相对应。因此,Cluster的本质是Referer的容器,并且提供了负载均衡和HA服务。每一个Cluster都被ClusterSupport封装起来,以提供对Cluster的刷新机制和生命周期管理。一个RefererConfig只会对应一个Cluster和一个ClusterSupport。(这段话,摘自http://kriszhang.com/motan-rpc-impl/)
createClusterSupport()
 private ClusterSupport<T> createClusterSupport(URL refUrl, ConfigHandler configHandler, List<URL> registryUrls) {
         List<URL> regUrls = new ArrayList<>();
         // 如果用户指定directUrls 或者 injvm协议访问,则使用local registry
         if (StringUtils.isNotBlank(directUrl) || MotanConstants.PROTOCOL_INJVM.equals(refUrl.getProtocol())) {
。。。省略。。。
         } else { // 通过注册中心配置拼装URL,注册中心可能在本地,也可能在远端
             if (registryUrls == null || registryUrls.isEmpty()) {
。。。
             }
             for (URL url : registryUrls) {
                 regUrls.add(url.createCopy());
             }
         }
         for (URL url : regUrls) {
             url.addParameter(URLParamType.embed.getName(), StringTools.urlEncode(refUrl.toFullStr()));
         }
           // 这里,委托给SimpleConfigHandler进行ClusterSupport的创建
         return configHandler.buildClusterSupport(interfaceClass, regUrls);
     }
class ClusterSupport<T> implements NotifyListener
buildClusterSupport方法中主要是new ClusterSupport,然后进行初始化,我们看看他初始化里的内容。
 public void init() {
         long start = System.currentTimeMillis();
       // 方法里,通过SPI机制,生成ClusterSpi、ConfigurableWeightLoadBalance、FailoverHaStrategy三个对象,然后设置到ClusterSpi的实例中。
         prepareCluster();
         URL subUrl = toSubscribeUrl(url);
         for (URL ru : registryUrls) {
             String directUrlStr = ru.getParameter(URLParamType.directUrl.getName());
             // 如果有directUrl,直接使用这些directUrls进行初始化,不用到注册中心discover
             if (StringUtils.isNotBlank(directUrlStr)) {
                 List<URL> directUrls = parseDirectUrls(directUrlStr);
                 if (!directUrls.isEmpty()) {
                     notify(ru, directUrls);
                     LoggerUtil.info("Use direct urls, refUrl={}, directUrls={}", url, directUrls);
                     continue;
                 }
             }
             // client 注册自己,同时订阅service列表         重点在这两行
               // getRegistry方法中首先是通过SPI机制生成ZookeeperRegistryFactory实例,利用这个工厂模式,创建ZookeeperRegistry
             Registry registry = getRegistry(ru);
               // 这里做的是服务的订阅。方法中最终调用的是CommandFailbackRegistry的doSubscribe方法,相关类图的关系图请往下看
21             registry.subscribe(subUrl, this);
         }
。。。 省略 。。。
     }
服务发现与刷新
RegistryFactory相关类图关系如下:

Registry相关类图关系如下:

CommandFailbackRegistry doSubscribe()的内容如下:
 protected void doSubscribe(URL url, final NotifyListener listener) {
         LoggerUtil.info("CommandFailbackRegistry subscribe. url: " + url.toSimpleString());
         URL urlCopy = url.createCopy();
         CommandServiceManager manager = getCommandServiceManager(urlCopy);
         manager.addNotifyListener(listener);
         subscribeService(urlCopy, manager);
         subscribeCommand(urlCopy, manager);
         List<URL> urls = doDiscover(urlCopy);
         if (urls != null && urls.size() > 0) {
             this.notify(urlCopy, listener, urls);
         }
     }
AbstractRegistry
 protected void notify(URL refUrl, NotifyListener listener, List<URL> urls) {
         if (listener == null || urls == null) {
             return;
         }
 。。。 省略。。。
         for (List<URL> us : nodeTypeUrlsInRs.values()) {
               // 重点在这里,调用了listener的notify方法。那么这个listerner是谁,就是ClusterSupport,它是在上面方法中的这两行代码里注册的
               // Registry registry = getRegistry(ru);
               // registry.subscribe(subUrl, this); this 指定的是ClusterSupport
         // 说了一大堆就是,概括说明就是,通过retistry进行回调notify方法  ※
             listener.notify(getUrl(), us);
         }
     }
我们继续探索ClusterSupport中的notfiy方法:
下面的代码,它主要就是根据新的URL(从zk端获得)创建Referer对象,并且刷新整个集群。刷新操作主要将新的Referer加入集群,并将旧的Referer对象释放掉。需要注意,这里并没有直接释放Referer资源,而是采用了延迟机制,主要考虑到Referer可能正在执行中,马上销毁会影响正常请求。我们会一层层进入代码内部去分析。
/**
* <pre>
* 1 notify的执行需要串行
* 2 notify通知都是全量通知,在设入新的referer后,cluster需要把不再使用的referer进行回收,避免资源泄漏;
* 3 如果该registry对应的referer数量为0,而没有其他可用的referers,那就忽略该次通知;
* 4 此处对protoco进行decorator处理,当前为增加filters
* </pre>
*/
@Override
public synchronized void notify(URL registryUrl, List<URL> urls) {
。。。 省略。。。
// 通知都是全量通知,在设入新的referer后,cluster内部需要把不再使用的referer进行回收,避免资源泄漏
// //////////////////////////////////////////////////////////////////////////////// // 判断urls中是否包含权重信息,并通知loadbalance。
processWeights(urls); List<Referer<T>> newReferers = new ArrayList<Referer<T>>();
for (URL u : urls) {
if (!u.canServe(url)) {
continue;
}
Referer<T> referer = getExistingReferer(u, registryReferers.get(registryUrl));
if (referer == null) {
// careful u: serverURL, refererURL的配置会被serverURL的配置覆盖
URL refererURL = u.createCopy();
mergeClientConfigs(refererURL);
// protocol 的类 :ProtocolFilterDecorator
// debug进入代码内部可以发现,里面是referer的创建和初始化操作。referer可以看成是RPC客户端的操作的类
// 里面,创建了NettyClient,在初始化操作中,跟server的类似,委托NettyClient进行transport层的操作
referer = protocol.refer(interfaceClass, refererURL, u);
}
if (referer != null) {
newReferers.add(referer);
}
} if (CollectionUtil.isEmpty(newReferers)) {
onRegistryEmpty(registryUrl);
return;
} // 此处不销毁referers,由cluster进行销毁
registryReferers.put(registryUrl, newReferers);
refreshCluster();
}
netty的部分暂时不进行分析。那么,到这里为止,referer就可以成功生成,这样的话,在前面initRef方法中createClusterSupport的部分就结束了,接下来就是创建代理的部分了。
回顾一下initRef方法
 public synchronized void initRef() {
。。。 
。。。         for (ProtocolConfig protocol : protocols) {
。。。
             String path = StringUtils.isBlank(serviceInterface) ? interfaceClass.getName() : serviceInterface;
             URL refUrl = new URL(protocol.getName(), localIp, MotanConstants.DEFAULT_INT_VALUE, path, params);
             ClusterSupport<T> clusterSupport = createClusterSupport(refUrl, configHandler, registryUrls);
             clusterSupports.add(clusterSupport);
             clusters.add(clusterSupport.getCluster());
             if (proxy == null) {
                 String defaultValue = StringUtils.isBlank(serviceInterface) ? URLParamType.proxy.getValue() : MotanConstants.PROXY_COMMON;
                 proxy = refUrl.getParameter(URLParamType.proxy.getName(), defaultValue);
             }
         }
         ref = configHandler.refer(interfaceClass, clusters, proxy);
         initialized.set(true);
     }
上面50行的内容如下:
使用JDK的方法来创建动态代理。
     public <T> T refer(Class<T> interfaceClass, List<Cluster<T>> clusters, String proxyType) {
         ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getExtension(proxyType);
         return proxyFactory.getProxy(interfaceClass, clusters);
     }
当客户端初始化完毕之后,我们就能正常使用motan进行方法调用了。对接口的调用,实际上是被动态代理了,动态代理的执行入口是哪里呢?RefererInvocationHandler提供了这个入口。主要实现如下:
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
。。。
 
         DefaultRequest request = new DefaultRequest();
         request.setRequestId(RequestIdGenerator.getRequestId());
         request.setArguments(args);
         String methodName = method.getName();
         boolean async = false;
         if (methodName.endsWith(MotanConstants.ASYNC_SUFFIX) && method.getReturnType().equals(ResponseFuture.class)) {
             methodName = MotanFrameworkUtil.removeAsyncSuffix(methodName);
             async = true;
         }
         request.setMethodName(methodName);
         request.setParamtersDesc(ReflectUtil.getMethodParamDesc(method));
         request.setInterfaceName(interfaceName);
         return invokeRequest(request, getRealReturnType(async, this.clz, method, methodName), async);
     }
 Object invokeRequest(Request request, Class returnType, boolean async) throws Throwable {
         RpcContext curContext = RpcContext.getContext();
         curContext.putAttribute(MotanConstants.ASYNC_SUFFIX, async);
 。。。
         // 当 referer配置多个protocol的时候,比如A,B,C,
         // 那么正常情况下只会使用A,如果A被开关降级,那么就会使用B,B也被降级,那么会使用C
         for (Cluster<T> cluster : clusters) {
             String protocolSwitcher = MotanConstants.PROTOCOL_SWITCHER_PREFIX + cluster.getUrl().getProtocol();
             Switcher switcher = switcherService.getSwitcher(protocolSwitcher);
。。。
             Response response;
             boolean throwException = Boolean.parseBoolean(cluster.getUrl().getParameter(URLParamType.throwException.getName(), URLParamType.throwException.getValue()));
             try {
                   // 真正的执行入口在这里
                 response = cluster.call(request);
                 if (async) {
                     if (response instanceof ResponseFuture) {
                         ((ResponseFuture) response).setReturnType(returnType);
                         return response;
                     } else {
                         ResponseFuture responseFuture = new DefaultResponseFuture(request, 0, cluster.getUrl());
                         if (response.getException() != null) {
                             responseFuture.onFailure(response);
                         } else {
                             responseFuture.onSuccess(response);
                         }
                         responseFuture.setReturnType(returnType);
                         return responseFuture;
                     }
                 } else {
                     Object value = response.getValue();
                     if (value != null && value instanceof DeserializableObject) {
                         try {
                             value = ((DeserializableObject) value).deserialize(returnType);
                         } catch (IOException e) {
                             LoggerUtil.error("deserialize response value fail! deserialize type:" + returnType, e);
                             throw new MotanFrameworkException("deserialize return value fail! deserialize type:" + returnType, e);
                         }
                     }
                     return value;
                 }
             } 
。。。
     }
Cluster.call()实际上是重头戏,他在方法执行内部代理给了haStrategy.call(),然后ha策略使用LoadBalance选择一个或一批Referer对象,并根据具体策略调用这个Referer的call()方法。Referer接口在motan协议的默认实现是DefaultRpcReferer,他在初始化的时候通过EndpointFactory创建了一个Client对象,他其实就是NettyClient,然后调用client.request(),从而使rpc请求顺利进入了transport层。(前面这段分析,参考了别的文章,写的比我有水平,自己汗颜,感觉到里面的差距)之后,通过transport层,一步步到达服务端。
参考文章:http://kriszhang.com/motan-rpc-impl/
这篇文章作者站在一个高度对Motan框架进行简明概要的分析,钦佩,学习。
对比自己,自己这只袋鼠仍然在扎根,加强自己知识的深度,至于广度,是未来下阶段的目标。
深入学习Motan系列(四)—— 客户端的更多相关文章
- 深入学习Motan系列(一)——入门及知识zookeeper储备
		背景以及说明: 最近逮到个RPC框架,打算深入学习,框架千千万,只有懂得内部原理,才能应对复杂的业务,进行自定义化系统. 这个系列的Motan文章也是自己慢慢摸索的轨迹,将这个过程记录下来,一是提升自 ... 
- 深入学习Motan系列(五)—— 序列化与编码协议
		一.序列化 1.什么是序列化和反序列化? 序列化:将对象变成有序的字节流,里面保存了对象的状态和相关描述信息. 反序列化:将有序的字节流恢复成对象. 一句话来说,就是对象的保存与恢复. 为什么需要这个 ... 
- 深入学习Motan系列(二)——服务发布
		闯关经验: 袋鼠走过了第一关,顺利搭建出了Demo,信心爆棚.不过之后,心想怎么去研究这个框架呢.查了一下,官方文档,好像没什么东西可以研究啊.后来,又搜了搜博客,因为这是微博的框架嘛,所以搜索时用百 ... 
- 小白学习Spark系列四:RDD踩坑总结(scala+spark2.1 sql常用方法)
		初次尝试用 Spark+scala 完成项目的重构,由于两者之前都没接触过,所以边学边用的过程大多艰难.首先面临的是如何快速上手,然后是代码调优.性能调优.本章主要记录自己在项目中遇到的问题以及解决方 ... 
- 深入学习Motan系列(三)——服务发布
		袋鼠回头看了看文章,有些啰嗦,争取语音简练,不断提高表达力!袋鼠奋起直追! 注:此篇文章,暂时为了以后时间线排序的需要,暂时发表出来,可是仍然有许多地方需要改写.自己打算把服务端发布,客户端订阅都搞定 ... 
- Windows-universal-samples学习笔记系列四:Data
		Data Blobs Compression Content indexer Form validation (HTML) IndexedDB Logging Serializing and dese ... 
- 步步为营 SharePoint 开发学习笔记系列总结
		转:http://www.cnblogs.com/springyangwc/archive/2011/08/03/2126763.html 概要 为时20多天的sharepoint开发学习笔记系列终于 ... 
- C#中的函数式编程:递归与纯函数(二)    学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面
		C#中的函数式编程:递归与纯函数(二) 在序言中,我们提到函数式编程的两大特征:无副作用.函数是第一公民.现在,我们先来深入第一个特征:无副作用. 无副作用是通过引用透明(Referential ... 
- .net reactor 学习系列(四)---.net reactor应用场景
		原文:.net reactor 学习系列(四)---.net reactor应用场景 前面已经学习了.net reactor一些基础知识,现在准备学习下实际的应用场景,只是简单的保护和 ... 
随机推荐
- Go 接口(interface)
			文章转载地址:https://www.flysnow.org/2017/04/03/go-in-action-go-interface.html 1.什么是 interface? 简单的说,i ... 
- 转 flowcanvas
			http://blog.sina.com.cn/s/blog_5fb40ceb0102wveq.html Unity 强大的可视化编程插件,Flowcanvas + Nodecanvas 组合(深度修 ... 
- 如何插入谷歌地图并获取javascript api 秘钥--Google Maps API error: MissingKeyMapError
			参考:https://blog.csdn.net/klsstt/article/details/51744866 Google Maps API error: MissingKeyMapError h ... 
- 文件编码检测.ZC一些资料(包含java的)
			1.IMultiLanguage3 或者 IMultiLanguage2 1.1.怎么判断XML 的编码格式(UTF-8或GB2312等)-CSDN论坛.html(https://bbs.csdn.n ... 
- 使用Rancher的RKE快速部署Kubernetes集群
			简要说明: 本文共涉及3台Ubuntu机器,1台RKE部署机器(192.168.3.161),2台Kubernetes集群机器(3.162和3.163). 先在Windows机器上,将rke_linu ... 
- 将1~n个整数按照字典序进行排序
			题意:给定一个整数n,给定一个整数k,将1~n个整数按字典顺序进行排序,返回排序后第k个元素. 题目链接:HDU6468 多组输入,T<=100,n<=1e6 分析:这个题和之前做的模拟出 ... 
- 词向量编码 word2vec
			word2vec word2vec 是Mikolov 在Bengio Neural Network Language Model(NNLM)的基础上构建的一种高效的词向量训练方法. 词向量 词向量(w ... 
- react-thunk的使用流程
			react-thunk作用:使我们可以在action中返回函数,而不是只能返回一个对象.然后我们可以在函数中做很多事情,比如发送异步的ajax请求. 这就是react-thunk的使用方法.接受一个d ... 
- Vue(八) 数字输入框组件案例
			数字输入框是对普通输入框的扩展,用来快捷输入一个标准的数字,如图: 代码: <div id="app"> <input-number v-model=" ... 
- 用python写一个定时提醒程序
			身体是革命的本钱,身体健康了我们才有更多精力做自己想做的事情,追求女神,追求梦想.然而程序员是一个苦比的职业,大部分时间都对着电脑,我现在颈椎就不好了,有时候眼睛还疼,我还没20阿,伤心...于是乎写 ... 
