前言

最近三周基本处于9-10-6与9-10-7之间,忙碌的节奏机会丢失了自己。除了之前干施工的那段经历,只看参加软件开发以来,前段时间是最繁忙的了。忙的原因,不是要完成的工作量大,而是各种环境问题,各种沟通协调问题。从这个项目,我是体会到了人一多,花在沟通协调上的成本真的会不成比例的放大,制度好,再加上协调好,会极大的提高整体工作效率。怪不得当年华为跟IBM学完工作组织管理制度之后能爆发出如此强劲的战斗力。从另一个角度,也能发觉出为什么大公司招人都比较注重员工的个人实力与团队协作能力,因为如果是多人协作的工作,一旦有人跟不上会极大的拖延整体进度,而且相对而言技术能力强的人更容易沟通交流达成共识,工作协作成本会比能力弱的人低很多。大道千条,我选其一,多提升个人能力才是王道。

闲话少叙,下面继续Dubbo源码的学习。上一节说的是Dubbo用SPI机制来进行Bean的管理与引用,类似于Spring中BeanFactory对bean的管理。SPI实现了类似于 "在容器中管理Bean"的功能,那么问题来了,如果我想在程序运行时调用SPI中管理的类的方法,再通过运行时的参数来确定调用哪个实现类,这么矛盾的场景应该怎么实现?这时就要靠Dubbo的自适应扩展机制了。

正文

实现的思路其实不难,我们先一起来分析一下。首先程序运行时直接调用的SPI管理类中的方法不是通过SPI加载的类,因为这时候还未加载,所以此时只能先通过代理类代理,在代理类的方法中再进行判断,看需要调用哪个实现类,再去加载这个实现类并调用目标方法。即最先调用的那个方法只是最终要调用的实现类方法的一个代理而已。添加了一个代理层,就实现了一个看似矛盾的场景,从这也可以看出软件开发的一个重要的思想武器-分层。

但要真正实现这个思路,将它落地,还是比较复杂的。首先要确定,哪些方法需要生成代理类进行代理?Dubbo中是通过@Adaptive注解来标识类与方法实现的。其次,代理类如何生成?Dubbo中先拼接出一段java代码的字符串,然后默认使用javassit编译这段代码加载进JVM得到class对象,再利用反射生成代理类。最后,代理类生成后,通过什么来确认最终要加载调用的实现类?Dubbo中对此进行了规范,统一从URL对象中获取参数找到最终调用的实现类。注意此处的URL是Dubbo中自己定义的一个类,类路径为  org.apache.dubbo.common.URL。

一、@Adaptive注解

此注解是自适应扩展的触发点,可以加在类上跟方法上。加在类上,表示该类是一个扩展类,不需要生成代理直接用即可;加在方法上则表示该方法需生成代理。Dubbo中此注解加载类上的情况,只有两个类:AdaptiveCompiler和AdaptiveExtensionFactory。以AdaptiveExtensionFactory为例,源码如下所示:

 @Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory { private final List<ExtensionFactory> factories; public AdaptiveExtensionFactory() {
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
} @Override
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
} }

可见其getExtension方法自己进行了实现,属性factories中放的就是两个类: SPIExtensionFactory跟SpringExtensionFactory,分别是Dubbo自身的SPI扩展工厂以及Spring的相关扩展工厂。

注解加在方法上的情况,以Protocol接口为例:

 @SPI("dubbo")
public interface Protocol { int getDefaultPort(); @Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); }

可见其中的export方法跟refer方法都加上了@Adaptive注解

二、代理如何生成

下面以Protocol接口中的export服务导出方法为例,看看Dubbo源码中是如何实现的代理生成。

在ServiceConfig类中的doExportUrlsFor1Protocol方法中,有一段这样的代码:

 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
3 Exporter<?> exporter = protocol.export(wrapperInvoker);

此处就是往远程导出服务的触发点。先生成了invoker,然后生成invoker的包装类可以看到在第三行调用了protocol接口的export方法。protocol属性为:

 private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

追踪getAdaptiveExtension()方法,最终找到生成代理类代码的地方:org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generate

 public String generate() {
// no need to generate adaptive class since there's no adaptive method found.
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
} StringBuilder code = new StringBuilder();
code.append(generatePackageInfo());
code.append(generateImports());
code.append(generateClassDeclaration()); Method[] methods = type.getMethods();
for (Method method : methods) {
code.append(generateMethod(method));
}
code.append("}"); if (logger.isDebugEnabled()) {
logger.debug(code.toString());
}
return code.toString();
}

整个代码拼接的过程比较复杂,按照java语法拼装各个部分,最终得到一个代理类的代码。具体的代码实现如果感兴趣可以自行查看,太多太麻烦,此处就不一一例举了。

然后在ExtensionLoader中编译,加载,得到Class类,方法如下所示:

 private Class<?> createAdaptiveExtensionClass() {
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}

最后用反射实例化,得到代理类对象。

三、根据URL加载指定的SPI实现类,调用方法

此步是在代理类的代码拼接中实现的。追踪上述generate()方法中的generateMethod(method)方法:

 private String generateMethod(Method method) {
String methodReturnType = method.getReturnType().getCanonicalName();
String methodName = method.getName();
String methodContent = generateMethodContent(method);
String methodArgs = generateMethodArguments(method);
String methodThrows = generateMethodThrows(method);
return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}

可见此处将一个方法分成了五部分:方法返回值、方法名、方法内容、方法参数、方法异常。分别获得这5部分后再拼接,组成一个完整的方法。

其余都比较简单,主要关注方法内容的获取 generateMethodContent()方法。

 private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
return generateUnsupported(method);
} else {
int urlTypeIndex = getUrlTypeIndex(method); // found parameter in URL type
if (urlTypeIndex != -1) {
// Null Point check
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// did not find parameter in URL type
code.append(generateUrlAssignmentIndirectly(method));
} String[] value = getMethodAdaptiveValue(adaptiveAnnotation); boolean hasInvocation = hasInvocationArgument(method); code.append(generateInvocationArgumentNullCheck(method)); code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value)); code.append(generateExtensionAssignment()); // return statement
code.append(generateReturnAndInvocation(method));
} return code.toString();
}

由于Dubbo统一规定通过URL来获取动态加载的类的key,所以我们要带着这样的设计前提来看这个方法。

首先getUrlTypeIndex这个方法是用来判断当前方法的参数中有没有URL,如果有的话返回值就是URL参数在整个参数列表中的下标位置,没有的话返回-1。

由于Protocol的export方法参数中没有URL,所以此处应该进入else中的方法 generateUrlAssignmentIndirectly() 中。此方法是去找到参数中的getUrl方法,然后获取到。执行完此方法后得到的内容为:

 if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
org.apache.dubbo.common.URL url = arg0.getUrl();

执行generateExtNameAssignment方法后得到的结果为:

 String extName = url.getProtocol();

执行generateExtensionAssignment方法得到的结果为:

 org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);

执行generateReturnAndInvocation方法得到的结果为:

 return extension.export(arg0);

这样,代理类的代码便拼凑出来了,后面通过编译类编译、加载进JVM、得到实例对象就可一气呵成的完成了。

尾声

至此,便完成了Dubbo的自适应扩展机制。可以发现,整个过程没有什么难的地方,大都是平常用过或者见过的用法,但是经优秀的阿里中间件工程师们之手一组合,就可以实现如此的功能,很让人佩服。下一期是Dubbo的服务导出功能解读,敬请关注。

Dubbo源码学习之-Adaptive自适应扩展的更多相关文章

  1. Dubbo源码剖析六之SPI扩展点的实现之Adaptive功能实现原理

    接Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)继续分析Adaptive功能实现原理.Adaptive的主 ...

  2. Dubbo源码学习(二)

    @Adaptive注解 在上一篇ExtensionLoader的博客中记录了,有两种扩展点,一种是普通的扩展实现,另一种就是自适应的扩展点,即@Adaptive注解的实现类. @Documented ...

  3. Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题

    Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题 相关文章: Dubbo源码学习文章目录 前言 主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题 ...

  4. Dubbo源码学习--服务是如何引用的

    ReferenceBean 跟服务引用一样,Dubbo的reference配置会被转成ReferenceBean类,ReferenceBean实现了InitializingBean接口,直接看afte ...

  5. Dubbo源码学习--注册中心分析

    相关文章: Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 注册中心 关于注册中心,Dubbo提供了多个实现方式,有比较成熟的使用zookeeper 和 redis 的 ...

  6. Dubbo源码学习--服务是如何发布的

    相关文章: Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 ServiceBean ServiceBean 实现ApplicationListener接口监听Conte ...

  7. Dubbo源码学习--集群负载均衡算法的实现

    相关文章: Dubbo源码学习文章目录 前言 Dubbo 的定位是分布式服务框架,为了避免单点压力过大,服务的提供者通常部署多台,如何从服务提供者集群中选取一个进行调用, 就依赖Dubbo的负载均衡策 ...

  8. Dubbo源码学习文章目录

    目录 Dubbo源码学习--服务是如何发布的 Dubbo源码学习--服务是如何引用的 Dubbo源码学习--注册中心分析 Dubbo源码学习--集群负载均衡算法的实现

  9. Dubbo源码剖析六之SPI扩展点的实现之getExtension

    上文Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中分析了getExtensionLoader,本文继续分 ...

随机推荐

  1. SYN591-A型 计数器

       SYN591-A型 计数器 秒表计数器累计计数器电机测速表使用说明视频链接: http://www.syn029.com/h-pd-248-0_310_44_-1.html 请将此链接复制到浏览 ...

  2. 【转载】Chrome使用自定义协议打开本地程序并运行IE打开网页

    部分内容转载自: http://blog.sina.com.cn/s/blog_e2b8213a0102wqby.html 项目中遇到某需求:chorme要运行IE并打开网页.解决方案之一就是通过自定 ...

  3. win7 docker安装文件及安装问题

    最近在玩爬虫,需要装docker,但是官网对于win7版本,只支持docker tool box,在官网找了半天才找到安装包,特此上传百度网盘,方便各位下载 链接:https://pan.baidu. ...

  4. 20 如何通过pycharm快速的创建一个html页面

    1.打开pycharm并且新建一个html页面,如下图所示. 2.删除html页面中默认的内容,之后在页面中输入!,之后点击tab即可完成一个html页面的框架新增.

  5. python bmp image injection

    1. 将原BMP文件的第三,第四字节替换为\x2F\x2A, 对应js中的注释符号/*BMP文件的第三.四.五.六字节表示BMP文件的大小2. 在BMP文件末尾添加(1)\xFF(2)\x2A\x2F ...

  6. SkyWorking基础:6.2版本安装部署

    就在今天,SkyWorking发布了6.2版本. 概述 什么是SkyWorking SkyWalking是观察性分析平台和应用性能管理系统. 提供分布式追踪.服务网格遥测分析.度量聚合和可视化一体化解 ...

  7. 视频编解码的理论和实践2:Ffmpeg视频编解码

    近几年,视频编解码技术在理论及应用方面都取得了重大的进展,越来越多的人想要了解编解码技术.因此,网易云信研发工程师为大家进行了归纳梳理,从理论及实践两个方面简单介绍视频编解码技术. 相关阅读推荐 &l ...

  8. 一道关于String的面试题,新鲜出炉,刚被坑过,趁热!!

    很多人都会答错的一道关于String的题目,究竟有什么难度? 我们一起来看一道关于String的面试题,准确说是改编的面试题! 准备好啦?在放大招之前先来一个小招式 String s1 = new S ...

  9. Hadoop起步之图解SSH、免密登录原理和实现

    1. 前言 emmm….最近学习大数据,需要搭建Hadoop框架,当弄好linux系统之后,第一件事就是SSH免密登录的设置.对于SSH,我觉得使用过linux系统的程序员应该并不陌生.可是吧,用起来 ...

  10. Codeforces 1133E - K Balanced Teams - [DP]

    题目链接:https://codeforces.com/contest/1133/problem/C 题意: 给出 $n$ 个数,选取其中若干个数分别组成 $k$ 组,要求每组内最大值与最小值的差值不 ...