dubbo微核心结构与实现类的结合
Posted 弓箭手IN上海
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dubbo微核心结构与实现类的结合相关的知识,希望对你有一定的参考价值。
封面配图 为2018杭州马拉松
文章配图 陕飞公司北区的秋天(标有我家,陕飞摄友的作品)
一、本文目标
第一次写dubbo相关文章是因为看到非常多细小的技术,对于从没接触过这些技术的、从事传统软件开发的人,感觉非常惊艳,于是分类学习整理了部分技术。
第二次写dubbo相关文章是因为用我们自己的中间件,参考mybatis源码换一种方式整合进spring,实现一个rpc远程调用,所谓眼看千遍不如手写一遍。
这次写是因为自己写过了核心功能,对外提供接口,也包装已有的功能,于是抽象化思维与专注核心开发是一个值得思考的问题。平时看很多源码时,经常点到的都是接口,抽象类,调用关系越来越难以理解,然而这却是大型复杂软件必不可少的组织代码的方式。有专门的开发抽象核心的,有做子模块api的,有做第三方工具包装的,合理分工又各司其职,容易插拔又易于升级维护。Dubbo里面有很多对现有第三方多个实现的抽象包装,值得体会,慢慢学着这样的架构方式。还有就是越是牛逼的组织或者企业,就是出标准的,比如XA/JTA,比如JDBC…并不专注于具体的实现。
另外自己写的所谓微核心,接口的实现类是通过静态方式设置进来的的,再看Dubbo设计明显专业、灵活、全面。话说SpringCloud的微服务全家桶是不错,但源码还是喜欢看dubbo。
二、功能概述
Dubbo的功能就是微服务治理,就是微服务的启动、注册/发现、调用、监控等功能。从大的模块看:需要的rpc、tcp、配置中心、序列化、日志都已经有很多很多第三方的工具了,实现这样的目标不可能都另起炉灶,不能重复发明轮子,当然重要的地方要自己改造或者创新。如同造车,我们只需要在某个核心部分上下功夫,其它的部件只要符合标准,都可以配套采购,当然可以提出标准由部件来满足要求,如果是已经通用的部件却不完全符合,就需要加装一层适配器了。如同不同国家的插座不一样,你需要有一个电源适配器,入口是多种结构,出口只有一种,来满足这个电器的使用要求。而每个入口到出口的转换都需要一个设计过程。
Dubbo的源码工程结构看,每一个模块,比如dubbo-rpc中,都有一个*-api的子模块,这就是公共标准api,而后面又是很多具体的已有产品的包装,比如-hessian,-rmi,当然-dubbo是自己的一个具体实现,并实现了公共标准api。
三、接口实现类(实例对象)的存放与加载
微核心的特点就是针对接口与抽象类编程,先不考虑具体实现,不过在运行时,一切都必须是确实的,都要找到真正的实现类。
1. 在spring使用时是如何做的呢?
当controller调用service时,通常会@Autowired 这样会自动byType,装配上一个符合接口类型的实现类。如果有多个实现类时,也可以用@Autowired返回一个list或者map,里面会有所有符合条件的实现类,这样可以在Controller的参数中指定一个参数,用它来从list/map中找到确定的一个实现类。看得出,spring内部就是把spring Ioc当成一个仓库,按要求取用。名字就是容器嘛。早先没有Anotation的时候,都使用xml文件配置引用关系,xml文件就如同一张装配图纸,Ioc中按图纸装配好实现类给你使用。
2. 我的项目中如何做的呢?
我的项目本身与spring是没有关系的,但是作为Web项目中的核心功能组件,对外的接口需要加入的实现类,比如有调用其它系统的接口,比如持久化,比如监听事件接口的实现,通常是用@Serivce 或者 @Component 注解的,也是进入了Ioc容器了。我们的核心功能是不会主动从SpringApplicationContent里面取的,核心功能组件不含spring包,并不与spring整合。所以核心功能组件中提供了一个容器,外部可以用静态方法设置进去,这样核心组件就从中取用了。
3. dubbo中如何做的呢?
Dubbo中如何把实现类串起来的呢?它也有一个仓库,com.alibaba.dubbo.common.extension.ExtensionLoader恰好就是这样一个实现类的仓库,要什么都可以从里面拿,没有的可以创造出一个,随需随加载并缓存下来。下图中可以看到很多从容器中取实现类的地方。
四、dubbo容器的解析
1.容器的基本结构
ExtensionLoader就是dubbo的容器,先看看怎么用吧。通常容器都会提供一个静态方法拿到想用的类,因为这是共用的公共仓库,静态类方法就可以了,要么也应该是单例,都可以方便的找到仓库。比如ApplicationContext.getBean(“beanName”),这里就是例如下面的几个方法:
ExtensionLoader.getExtensionLoader(*.class).getExtension(“name”);
ExtensionLoader.getExtensionLoader(*.class).getSupportedExtensions(“name”);
ExtensionLoader.getExtensionLoader(*.class).getAdaptiveExtension(“name”);
ExtensionLoader.java从设计上来说,分成静态类与对象两部分:
静态部分重点有两个ConcurrentMap,EXTENSION_LOADERS存放接口与ExtensionLoader对象的对应关系;EXTENSION_INSTANCES存放接口与实现接口的对象的关系。后一个很好理解,接口肯定有很多实现类,实现类与类的对象(进行了依赖注入的)都存起来,第一次加载,后面就直接用了。前一个放接口与ExtensionLoader对象?到底ExtensionLoader是啥呢?
下面要分析一下ExtensionLoader的对象部分。当一个对象与它的静态map属性在一起的时候,就想到了享元设计模式。里面的属性部分见下图,已经进行了备注。
一个接口与它所有的实现类,实现类的对象,以及从哪个工厂找都打包在一起。一个接口通常有多个实现类,每个类都实例成对象,也可能有一个适配器类及对象。这些都很好理解,打包好一个接口就放在上面的静态容器中。
比较有意思的是,ExtensionFactory工厂本身也是一个接口,有三个实现,其中一个是适配器实现AdaptiveExtensionFactory,另外两个是SpiExtensionFactory、SpringExtensionFactory,分别表示从meta文件中找实现类或者从spring ioc中找实现类。注意103行工厂接口是没有对象工厂的,不会嵌套死循环。其它接口的对象工厂都是工厂适配器,工厂适配器内部必须是有真正的工厂实现类,而这个实现类又是通过ExtensionLoader以spi的固定方式(getSupportedExtensions)加载的。
2.容器的重要方法
l getExtension(“name”):按名字获取一个接口的实现类的对象。过程如下:
1.如果缓存中没有?就调用createExtension,生成一个。
2.生成前,先调用getExtensionClasses,获取所有的实现类。
3.获取前,先调用loadExtensionClasses,加载所有的实现类。
4.分别从三个位置加载loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
5.然后加载loadResource,从系统资源目录下找到以接口命名的文本文件。
6.从文件中找到实现类的名字与实现类的全名。
7.loadClass(extensionClasses, resourceURL, Class.forName(line, true,classLoader), name);这样符合条件的实现类就找到了。
8.回到上面的第二步,找到类后,就用EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());产生一个类的实例,再进行依赖注入与可能的包装就可以了。
经过以上步骤,一个接口的实现类的实例对象就有了。
l 获取一个接口的适配器实例getAdaptiveExtension()
1. 缓存中没有实例的话,首先用createAdaptiveExtension()来制造一个。
2. 先要得到一个适配器的类并实例化。getAdaptiveExtensionClass().newInstance()。
3. 缓存中要是没有这个类,就createAdaptiveExtensionClass()来制造一个类;
4. 这个类通过在代码中产生java源文件来生成的,先用createAdaptiveExtensionClassCode();产生java源文件。
5. 再找到classLoader(类加载器),并从容器中找一个编译器动态产生一个类,compiler.compile(code, classLoader);
6. 看到Compiler也是一个接口,它的实现类也是用.getAdaptiveExtension();来获取的。这不就嵌套调用了吗?实现上,并不是所有的接口的适配器类都是动态代码来生成的,如果一个接口有明确的AdaptiveExtension类,在上一个方法中就已经设置到适配器缓存类里了。走到上面的步骤3就会返回。前面说的工厂接口就有AdaptiveExtensionFactory,而这里的编译器接口也有明确的适配器类:com.alibaba.dubbo.common.compiler.support .AdaptiveCompiler。也有明确的实现类,比如JavassistCompiler与JdkCompiler。这些工具在动态代理/aop中都很常见。
7. 没有明确适配器类的接口,如果有@Adaptive注解,需要动态产生的比如protocal接口,下面就是动态产生的适配器类代码。因为在运行时都不知道具体用哪个接口的实现类实例,只是在调用接口的特定方法时,需要从方法参数中拿到一个名字值,再从容器中找到实现类来调用。
顺便说一下,容器中的实现类是随用随加载,当然加载后就存缓存中。而且是可插拔的,每个实现类自己的资源文件记录实现类的。不细展开了。
五、如何串起接口实现类
1.从配置文件到注册服务的过程
<dubbo:registryprotocol="zookeeper" address="192.168.1.128:2181" />
<!-- 用dubbo协议在20880端口暴露服务,默认:20880 -->
<dubbo:protocolname="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:serviceinterface="接口的全限定名[com.xyh.service.TestService]"ref="testServiceImpl[实现类对象]"timeout="600000" />
Dubbo标签的解析就不说了,服务暴露时,解析后的BeanDefination中主要是这个类:
com.alibaba.dubbo.config.spring.ServiceBean,它实现了InitializingBean接口,会在这个类对象产生并注入好相关引用后,执行afterPropertiesSet方法,这就是启动一个service对外暴露并注册到配置中心的开始。这个方法中收集所有的配置后,执行export(),内部执行doExport(),内部执行doExportUrls(),因为可以暴露到多个地方,所以内部会循环执行doExportUrlsFor1Protocol(protocolConfig, registryURLs);
略过很多简单代码后,核心有这么两句:
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class)interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY,url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = newDelegateProviderMetaDataInvoker(invoker, this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
解释:
主要就是用要暴露出去的servece对象ref,产生一个invoker对象,用并注册的协议方法export把服务invoker暴露出去。其中的协议接口与代理工厂接口的实现类是这么两句,就是从ExtensionLoader容器中找的适配器类。
private static final Protocol protocol =ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
private static final ProxyFactory proxyFactory =ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
前面说了protocol接口的适配器是动态产生的,调用方法时,从参数中拿到真正要用的实现类,那参数就是wrapperInvoker里的registryURL。
这个registryURL是什么样子呢?如下:
registry://127.0.0.1:9098/com.alibaba.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.5.4-SNAPSHOT&export=dubbo%3A%2F%2F192.168.0.102%3A20880%2Fcom.alibaba.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26dubbo%3D2.5.4-SNAPSHOT%26generic%3Dfalse%26interface%3Dcom.alibaba.dubbo.demo.DemoService%26loadbalance%3Droundrobin%26methods%3DsayHello%26owner%3Dliujunaaaaa%26pid%3D7084%26side%3Dprovider%26timestamp%3D1415712331601&owner=liujunaaaaa&pid=7084®istry=zookeeper×tamp=1415711791506
看看com.alibaba.dubbo.common.URL代码,如何拆解这个字符串
i =url.indexOf("://"); protocol =url.substring(0, i);
newURL(protocol, username, password, host, port, path, parameters);
上面的那个串就是协议是registry,主机端口、路径、参数…仔细看export参数,是一个编码过的URL,内容就是要暴露出去的服务,它也有主机端口、路径、参数等信息。主审一个嵌套的URL。
再看后一句protocol.export(wrapperInvoker);这个就是Protocol 接口的AdaptiveExtension适配器来暴露这个invoker。前面介绍过动态生成的Adaptive会在调用时从参数中找到一个值,根据它来调用真正的实现类。registryURL中的registry就是这个参数,那自然是调用RegistryProtocol。
如下图示:RegistryProtocol中有几个属性,特别注意内部还有一个protocal属性,前面介绍过一个接口的实现类实例化后会进行依赖注入的(injectExtension),所以这几个属性都会注入实现类。RegistryProtocol在export的时候会调用一个doLocalExport,这里面有一句:protocol.export,就会用内部的protocol实例再进行export操作。
从原始的registryURL中就嵌套着一个dubbo的URL,正好对应着RegistryProtocol内部的Protocol,而这个内部Protocol也是一个适配器,会从内部的URL中的参数得到真正的比如dubboProtocol进行export。适配器之间串起来,参数也串起来。
内部如果是dubboProtocol进行export,实际上就是在本地启动一个TCP服务监听。这个后面再介绍。
接着就是要把这个启动好的服务进行真正注册了,RegistryProtocol的主要功能。我们知道,服务可以注册到很多地方,
public void register(URL registryUrl, URLregistedProviderUrl) {
Registry registry = registryFactory.getRegistry(registryUrl);
registry.register(registedProviderUrl);
}
registryFactory又是inject进来的动态适配器对象,也是从参数中找真正的实现者,上面的URL中有啊,registry=zookeeper。所以会找到ZookeeperRegistryFactory,它里面又有一个ZookeeperTransporter属性,现看还是一个动态适配器类对象,用zookeeper注册当然要有zookeeper的客户端了,通常有两个:zkClient与curator。看来可以在URL中指定,也可以默认用curator。Dubbo对这两个都进行了包装,返回的是ZookeeperClient接口的对象。后面就不展开了,专门介绍如何包装第三方的各种实现。
可以看出,运行时都用适配器对象进行功能组合,组合成一个虚结构体。但直到调用具体方法时,才从参数中拿到真正的对象。串起来的实现类都是一群各种功能的代理商组成的,开工的时候再找外包来干活。
2.注册中进行服务暴露的过程
上一节中暂时跳过了【dubboProtocol进行export,实际上就是在本地启动一个TCP服务监听】,这里详细介绍一下tcp之上的服务就是rpc。注册中选择RPC,而Rpc又是如何选择tcp的呢?
暴露服务是前面注册过程中嵌套的功能,可以根据嵌套的url中的协议选择真正的暴露协议,比如就默认的dubboProtocol了(@SPI("dubbo"))。
先看看dubbo-rpc-dubbo模块中的DubboProtocol。它有一个export方法,内部会openServer(url);内部会调用createServer(url),看到url紧紧跟随着。里面有这么两句:
url.getParameter(Constants.SERVER_KEY,Constants.DEFAULT_REMOTING_SERVER);
!ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)
第一句的后者参数是netty,第二句会进行检测,如果是配置了netty,却没有netty实现的Transporter接口的话,会出错的。当然一般不会在配置中指定netty吧,先略过。接下来就持续跟踪到这么几句:
server =Exchangers.bind(url, requestHandler);
getExchanger(url).bind(url,handler);
public static Exchanger getExchanger(URLurl) {
String type =url.getParameter(Constants.EXCHANGER_KEY, Constants.DEFAULT_EXCHANGER);//”header”
return getExchanger(type);
}
public static Exchanger getExchanger(Stringtype) {
returnExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);
}
注意上面的type默认是”header”,从容器中getExtension拿到的就不是动态适配器类,就是具体的一个类了,而且这里使用的还不是传输接口transport,是交换接口exchange。所以从rpc到exchange与前面的regitster到rpc是不一样的。
看上图,找到这个确定的交换接口实现类,在它构造时,才真正加入了Transporters接口,看到url也传进去了。跟踪这个Transporters.bind。getTransporter().bind(url,handler);。找到:returnExtensionLoader.getExtensionLoader(Transporter.class).getAdaptiveExtension();
这里又是一个适配器实现类,看接口上有@SPI("netty")。默认是用netty进行传输的。如果url中指定了,也不一定用netty了。
NettyTransporter中就new NettyServer(url, listener);了。URL又传进去了。
简单跟踪几下:localAddress = getUrl().toInetSocketAddress();
this.codec =getChannelCodec(url);
this.connectTimeout= url.getPositiveParameter
Exchanger层位于传输层之上,虽然也是在remoting这个名字下,这个层还有重要的Request对象与Response对象。上面的rpc层(主要处理Exporter与invocker对象)如果想发的任务message,都在这层包装成Request对象,再用下层的transport的工具发出去。这一层还有心跳功能。
3. 串起各层接口实现总结
从前两节的分析进行一下总结:
1. 从最外面的spring触发serviceBean的afterPorpetySet开始,它内部有一个注册protocal适配器。而注册适配器的实现类中又有一个rpc 协议适配器,还有注册工厂适配器。Rpc内的实现类又有一个交换层的实现,交换层内又有一个传输适配器。
2. 上面的层层引用关系,很多时候是靠来自spring的配置产生的url,在调用时确定真正的实现类的。这个url是串起真正调用的一条线。
3. 交换层比较特殊,它可以产生适配器类,但直接选择了headerExchange。另一个实现是mock的。再下面的传输层又是接口的适配器类。所以这个交换是在一个dubbo-remoting-api工程中,是remoting的抽象层中的类,不是一个remoting具体实现工程中的类。
分析启动过程中各个层次的调用,都有不同的调用设计,这个设计实现了最大的灵活性,通过外层的配置可以改变很多东西,真正的微核心体现。
五、已有实现类的封装
前面提到过dubbo-*-api是模块的抽象。dubbo-*-*是对已有的同类工具进行封装,是抽象与具体之间的转换过程。前面是标准,以接口与抽象类为主,后面是把已有的东西包装成标准的东西。这样不同层之间就可以用公共的标准打交道。实现类可以方便的插拔,可能不用,要是有新的工具,只需要换标准包装一下,降低各实现类的耦合性。Dubbo从大的方面看,从工程名字看,有对config、container、register、remoteing、Rpc、serialization等的封装。小的封装比如上面提到的两种zookeeper客户端的封装。就举几个栗子吧:
Common包里compiler有两种实现,分别是JdkCompiler和JavassistCompiler,需要对外提供一个接口,这需要从外部来看,从调用者的角度看,标准应该提供什么功能。
实际上只有一个Class<?> compile(String code, ClassLoader classLoader);//把java源代码变成一个类即可。
JavassistCompiler实现类就专注于使用Javassist框架产生这样一个类。
JdkCompiler把源代码写入资源目录,用ToolProvider.getSystemJavaCompiler()编译后产生类。
Logger中对现有的4种日志进行了封装,包括jcl jdk log4jslf4j。具体选择哪个在加载静态类时确定:System.getProperty("dubbo.application.logger");
Rpc层有10个实现,包括dubbo,hessian,http、injvm、redis、rim、thrift等。这些复杂的包装需要非常了解各个实现工具,又要了解上层对这些工具的通用需求。比如要抽象出client、server、channel等重要接口,还有实现这些接口的抽象类,以前有分析过,不再细讲了,有很多细节有空了还要好好看看。
六、结尾
看这样复杂的代码既是享受也是折磨。第三次写dubbo后,理解更全面更深入了。仓促成文,作为学习笔记记录下来。估计好久都不会看dubbo源码了,现在spring cloud全家桶作微服务比较厉害。除了源码分析(spring/mybatis),还有一大堆要系统学习一下:包括并发编程(valitile/aqs/cas/各种锁/线程池源码)、分布式中间件(nio/nio2/netty/zookeeper/rocketmq/redis/mongodb/mysql(innodb/b+tree/index)/nginx/mycat/sharding-jdbc/分表分库)、大数据(hdfs/hive/hbase/elastic search-ELK/flume/yarn/spark/kafka)、各种优化(tcp/tomcat/jvm/mysql/)、还有各种技术(asm/javaasist/aop/cglib/jdkProxy/classLoader的过程/分布式锁/分布式事务)、还有各种工具(git/maven/ideal)的熟悉。
欢迎讨论dubbo源码,如果有写的不对的不准确的地方请指出,谢谢!
以上是关于dubbo微核心结构与实现类的结合的主要内容,如果未能解决你的问题,请参考以下文章