dubbo的ExtensionLoader源码学习后的架构设计思路总结

最近看了一下dubbo的extension部分,作为微核心重要组成的加载实际类对象的通用组件,类似于spring IOC的基础地位,确实是非常重要的功能。

[b]本文重点介绍dubbo的容器,微核心化,以及微核心如何在运行时,从容器中选择实例,变成真核心的过程。[/b]

[b][color=red][size=small]希望看过的朋友可以留言,不管是什么写的太差,或者哪里错了,或者没看懂,或者点赞。通过反馈来不断提高水平,写更好的文章。[/size][/color][/b]

网上看了一些人的分析,大多数主要功能都分析到了,这个我不写了。我只是在反思自己碰到这样的要求,要怎么做?目前项目中会怎么做?以及如何达到那样的水平?

[b]1. 首先基本功能是什么呢,需要包含些什么内容呢?[/b]

我们已经有基于接口的微核心,在运行时,我们需要用真正的接口的实现来工作,根据接口找到类实例。

这说明,我们要设计一个接口与实现的仓库,放置好所有的接口与实现。这个仓库是一个静态类,或者有些静态属性map,有方法从中取到需要的实现对象。
如同士兵上阵了,根据各个人的功能不同,从军/火库拿自己用的装备。

怎么实现呢?简单的话:一个配置文件中写上接口与实现类,仓库启动时,可以读一下,用map存放接口与类名,获取的时候,让类new 一下就可以了。new 好了,也可以用map缓存下实例对象。下次就不用new了。

这样,首先要有一个仓库,存着每一个接口,这里的每一个接口,要存类,要存一下实例。仓库有功能,从配置中加载接口与类的对应关系文件。

如果知道java的SPI,差不多就是这个思路。我觉得一般应用中,写到这个程序就可以了。后面有时候单独做一个完善的项目时可以考虑。

一般项目中,可以把名称与处理类对象起来,放在一个统一的地方,按需要找到即可。当然你可能会直接用spring的@Autowired Map来弄。我基本对接口编程后,就这么弄的,因为实现的类都是spring里的。

更深入一点,你可以通过配置文件,在spring中创建BeanDefination,它会帮你实例化并放在容器中的。


[b]2. 增加难度,既然是微核心,接口的话,一般都有多个类,那又如何处理呢?[/b]
如果多个类中,随便选择一个,那可以从实现类的对象中,随机选择一个返回(有明确适配类)。
如果多个类中,需要根据方法的参数,从参数中选择一个具体的实现类,那怎么处理呢?(运行时动态适配类)
如果象日志适配,与本地配置有关,那如何得到与配置有关的实现类呢?(启动时明确适配类)

那也好办啊,我们可以配置时,有一个具体类与某个参数对应关系表,先找到相关的实现类,再找与这个参数对应的实现类。如同超级适配器,先按接口找,再按参数找。

可是,为什么dubbo对参数控制的实现类,要动态生成一个个接口适配器类呢?而且所有从ExtentionLoader加载接口,基本上都返回适配类实例(至少日志除外)。动态生成的适配器类的方法中,还是仓库中找真实的实现类啊。

目前只能抽象理解成,领东西的人太多了,按类型设置一个代理人。尤其是下面提到引用,而且多层引用的时候,你用适配器类建立起层层引用关系,它们各自找自己的实现类。否则就很乱了,算是一个逻辑抽象需要吧。要不你选择通讯用某一个实现,选择传输用某一个实现,选择序列化用某一个实现,这样组合太多,因为是具体类只能现场装配引用,很仓促,哪里有问题也很难找。

再想想,如果我在spring容器中想实现一层适配器,怎么办呢?就是在接口与实现类中插一脚。那可以动态代理吧?代理的类拿到类,方法,参数后,然后再从容器中找想要的实现的类,再真正调用。

[b]3. 再增加难度,如果我的接口实现类中,引用了另一个接口,需要配那个接口的实现类呢?[/b]

通过反射机制设置啊,循环方法名,引用的接口再从仓库里找适配的对象啊。为什么不找实际的对象(inject中的getExtension,还是从仓库取的adaptive)?因为上面提到的逻辑抽象需要啊,特别是层层引用的时候。没有适配器层,你无法预先建立起层层的引用关系。什么?“每次用到再配置实例引用”,这太麻烦也太乱了。

同样想想,如果在spring中,已经产生了动态代理类,那么动态代理类之间如何建立引用关系呢?我们知道,接口中可以有静态常量,可以设置为一个名字,动态代理时注入一个动态代理?好象不可以。除非是类,接口好象不可以相互引用吧。

[b]4. 再增加难度,spring用的地方太多了,我的接口是一个spring实现呢?[/b]

spring本身就是一个仓库啊,里面按名字,按类型,都可以找到实现对象的。只要你给我spring的上下文,我就能找到。

上面的仓库里,要不要引用一个spring的仓库呢?,应该可以吧。

但是我们看dubbo的代码,它只是在注入另一个接口时,从适配器中选择SPI或者spring。说明最外面的一层,是不可能从spring中取的啊。

再看看dubbo中使用ExtensionLoader,基本上都返回的是adaptive的实现。说明一个spring的接口实现,外面也是套一层adaptive的。

动态实现的adaptive,里面的方法都是从仓库得到getExtension,非adaptive的。说明同一个接口不会嵌套同类adaptive,否则就是死循环了。

这样,从spi与spring中找,可以统一成一个适配器。每个接口注入引用的时候,都从这个适配器中找,通常不用spring时,还是从本仓库找。

[b]5. 最后说明[/b]

因为都有缓存,所以适配器什么的,只产生一次,再说也是按需加载,不会太慢,也不会一下吃内存。

目前的项目中还停留在第一阶段,就是微核心接口的实现,都从spring中来的。没有适配层的需要,如果有,也会手工写一个实现接口的适配器,不会动态编译产生。如果有引用关系,用set方法设置。

[b]6. dubbo中特色的接口实现:[/b]

1.common包里的compiler
本身有一个适配器,spi有值javassist,表示默认,解析时在loader中设置了默认。不需要动态生成适配器类。因为这个由自己来默认或者设置值决定,而不是被动的调用中由方法参数中决定,但也不是如log中,配置中定了就确定不改了。
mata文件中,只有一个适配器与两个实现。

2.common包里的logger
虽然名字叫loggerAdapter,但不是实现接口的适配,而是包装每一种日志方式的适配。这个接口有4种日志实现。是通过从loggerFacory中读配置来加载实现类,本身实现类上没有适配类。loader加载是getExtension,不是getAdaptiveExtention。

3.common包里的Serialization
这个接口中有方法注了Adaptive,所以是有适配类动态生成的,想想这个由对方告诉我应该如何序列化,也是有道理的。它有好几个实现类。

实现类要对已有的产品,比如已有的日志,已有的序列化工具进行抽象,产生给系统通用的接口与包装类。因为已有的产品长相各不相同,要统一一下。

4.common包里的ThreadPool
这个接口有默认的实现类fix,也有动态生成适配类。根据外部的请求生成适合的。
因为实现接口的几种线程池都是java本身的,不需要每种实现进行额外抽象包装,所以相对简单。

所以完整的有适配器(动态或者已有),有包装抽象接口,有对个性化产品对象的包装抽象接口实现。这些实现就是适配器选择后,得到的真正实现类。

5.rpc包里的Protocol

前面说的几个是非常简单的东东,后面这两个就非常复杂了。首先rpc是远程调用,是应用层,http、rmi,webservice都是已经有的东东。dubbo不仅自己实现了一个协议,还抽象了一层rpc,抽象的都在rpc-api模块里的。

为了把各种已经有的协议包装起来,一方面抽象出一堆接口在api里面,另一方面对每个协议进行封装,放在rpc-***里面。
rpc-api中还有通用的一些本模块的工具,比如proxyFactory,这又是一个SPI接口,又有动态适配产生。真正的实现类有两种:jdk与javassist。实现类中公共的部分产生了抽象类。

6 remote包里的Transporter

传输也有有很多已有的工具的,比如netty,mina等。同样要抽象出一层接口,并对netty与mina进行包装,以实现api。
传输对外提供的接口就是Transporter,必须在api包里,看它确实是SPI,默认是netty。也是动态生成适配器类的。
每种工具都不可能直接用,所以也包装一下,就分别在remote-***包里。
另外,remote都是传输工具,但不是都给rpc用的,也可能给register用,比如zookeeper。ZookeeperRegistryFactory里setZookeeperTransporter(*)这个方法,应该是被loader在inject时装配好的吧。这个还不确定。那dubbo用传输工具时是不是这样呢?DubboInvoker有一个构造中传入了ExchangeClient[] clients。如果是多个可用的客户端,就是clients[index.getAndIncrement() % clients.length]--轮询方式。但是还不知道是什么时候调用,把传输客户端给RPC来用的???
应该是dubbo调用时,根据url中的type,来生成的。找到:getExchanger(url).connect(url, handler);
ExtensionLoader.getExtensionLoader(Exchanger.class).getExtension(type);

再比如netty配置codec时,也会从loader,根据URL中的值来选择。说明一是拆的很开,二是从URL中得到很多值来确定多层次的具体实现类。
String codecName = url.getParameter(Constants.CODEC_KEY, "telnet");
return ExtensionLoader.getExtensionLoader(Codec2.class).getExtension(codecName);

[b]7. 从具体到抽象的思考[/b]
dubbo的结构都是一个api模块,好几个实现模块。就以rpc为例思考一下从具体到抽象的过程。
首先要用真正的实现熟悉这个过程,才能了解都有什么对象参与其中。再进行抽象。这时,rpc是远程方法调用,算是TCP传输层之上的应用层协议(protocol)设计。dubbo的客户端(client),基本上是这个过程。启动中,一个接口要被代理(invoker),代理用来通过传输工具(transport)与远程服务进行连接。调用的时候,把调用有关的请求(invocation)数据,发过去。除了接口,里面还有一些通用的对象与操作,又要弄出抽象类。此工作非常的复杂。

这部分工作由于要清楚多个现成的外部工具,比如netty,mina都搞熟悉了,才可以准确抽取出公共部分。形成一个功能模板(以抽象类,接口为主),这个结果就是产生remote-api模块。接着需要对已有的工具(引入的netty.jar)进行包装,以满足标准模板的样子,结果就是产生相应的实现模块remote-default。这样,其它地方就可以通过配置,以通用的方式操作这些工具了。

我们平时写一些操作,如果有通用的部分,就会想到抽象类中写,就是简单的从具体到抽象的过程。但很复杂的工具中抽取接口,抽象类,通用的模板设计模式。要把工具摸透了,而且抽象思维才行。

[b]8. 从抽象到具体的思考[/b]
主要的接口与关系概念就有了。当你有很多实现可能的时候,又如何从抽象到具体呢?运行的时候,可都是具体的对象在工作啊,接口是不能工作的。而最外层的确定往往是配置文件。
比如定了用dubbo协议,客户端肯定要产生一个动态代理用的invoker,这时明确是dubboInvoker了,可以new出来实例,但返回的一定要是接口。
invoker要把请求内容出去,这时肯定要确定好传输实例是谁了。getExchanger(url).connect(url, handler);,还是从url中找到,默认是header,还是来源于配置传过来的参数URL。
new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
header只是一层包装,如何确定包的真正client是谁?还是从url中找。
上面header中参数里跟踪找到getTransporter().connect(url, handler);//connect是一个adaptive,也是从url中找,找不到就是SPI中默认的了。
当new NettyClient(url, listener);时,又把url传进去,设置为属性了。HeaderExchangeClient包了一个NettyClient,当设置自己的心跳时,又从NettyClient的url中取值了。
我们知道netty要设置序列化的,是不是也从url中确认实例呢?
new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);果然是传进去了。
this.codec = getChannelCodec(url);
发现在new NettyClient过程中不断super时,就有了coder属性,this.codec = getChannelCodec(url);所以new NettyCodecAdapter时,就从getCodec中拿到已经实例好的序列化工具了。里面有:return ExtensionLoader.getExtensionLoader(Codec2.class).getExtension(codecName);,还是用loader从容器仓库中取,但不是adaptive,是一个明确的实例了。
所以启动时,用到的材料都已经在Client生成时明确好了,就等着装配了。而所有的接口的实现类确定,基本上都是从url参数中来的,而且一层层传递到内部的。[b][color=red]注意:这个url是dubbo的,就是定位容器资源的。与java的URL无关。[/color][/b]

[b]从上可以看到,从外部置好以后,一层层不确定的东西都是靠url传进去的,url就是一个实例化手册。手册上找不到的,一定要找到默认的,否则要出错的。[/b]

[b]9. END[/b]
这样分析一遍,LOADER差不多是整个netty的IOC容器啊,与spring容器一样,会从配置中读信息,类似记录下beanDefination一样,记录下loader的接口,默认类,适配器类,还有实例,还进行了缓存。
我目前也写比较简单的微核心功能,不过容器利用spring容器,启动时根据配置,加载到微核心中运行。再次分析dubbo此功能后,感觉整体架构能力好象提高了不少啊。

你可能感兴趣的:(dubbo的ExtensionLoader源码学习后的架构设计思路总结)