理解 Dubbo SPI 扩展机制

写在前面

最近接触了 gRPC 体会到虽然众多 RPC 框架各有各的特点但是他们提供的特性和功能有很多的相似之处 , 这就说明他们面对同样的分布式系统带来的问题。从 2016 年左右开始接触到 dubbo ,基本停留在使用的层面,对 dubbo 的设计以及着重要解决的问题都没有系统的研究过,通过对 dubbo 和其他类似 RPC 产品的系统学习 ,学习分布式系统中面临的共同问题以及解决之道。

微内核架构

微内核架构 (Microkernel architecture) 模式也被称为插件架构 (Plugin architecture) 模式。原本与内核集成在一起的组件会被分离出来,内核提供了特定的接口使得这些组件可以灵活的接入,这些组件在内核的管理下工作,但是这些组件可以独立的发展、更改(不会对现有系统造成改动),只要符合内核的接口即可。典型的例子比如 , Eclipse , IDEA 。

image.png

Dubbo 的微内核设计

根据我个人对 Dubbo 微内核设计的理解,以及阅读源码后总结。视觉总是最直观的,可以让大脑最快速度的有一个最直观的认识,一开始就一头深入到源码的细节中只会让人迷糊。不理解 Dubbo 的微内核设计架构的话,学习起来会走不少弯路。

image.png

dubbo 内核对扩展是无感的 , 完全不知道扩展的存在 , 内核代码中不会出现使用具体扩展的硬编码。

术语说明 :

  • SPI : Service Provider Interface 。

  • 扩展点 : 称 Dubbo 中被 @SPI 注解的 Interface 为一个扩展点。

  • 扩展 : 被 @SPI 注解的 Interface 的实现称为这个扩展点的一个扩展。

Dubbo SPI 约定

扩展点约定:扩展点必须是 Interface 类型 , 必须被 @SPI 注解 , 满足这两点才是一个扩展点。

扩展定义约定 : 在 META-INF/services/扩展点接口的全限定名,META-INF/dubbo/扩展点接口的全限定名,META-INF/dubbo/internal/扩展点接口的全限定名,这些路径下定义的文件名称为扩展点接口的全限定名,文件中以键值对的方式配置扩展点的扩展实现。例如文件 META-INF/dubbo/internal/com.alibaba.dubbo.common.extension.ExtensionFactory 中定义的扩展:

adaptive=com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=com.alibaba.dubbo.common.extension.factory.SpiExtensionFactory
spring=com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory

默认适应扩展 : 被 @SPI("abc") 注解的 Interface , 那么这个扩展点的缺省适应扩展就是 SPI 配置文件中 key 为 "abc" 的扩展。如果存在被 @Adaptive 注解在类上的扩展点接口实现 ,那么这个类就作为扩展点的缺省适应扩展, 一个扩展点只能有一个缺省适应扩展也就是说多个扩展中只能有一个在类上被 @Adaptive 注解,如果有多个 dubbo 会抛出 IllegalStateException("More than 1 adaptive class found : ")。

@SPI 、@Adaptive 、@Activate 作用

@SPI (注解在类上):@SPI 注解标识了接口是一个扩展点 , 属性 value 用来指定默认适配扩展点的名称。

@Activate (注解在类型和方法上): @Activate 注解在扩展点的实现类上 ,表示了一个扩展类被获取到的的条件,符合条件就被获取,不符合条件就不获取 ,根据 @Activate 中的 group 、 value 属性来过滤 。具体参考 ExtensionLoader 中的 getActivateExtension 函数。

@Adaptive (注解在类型和方法上):@Adaptive 注解在类上 , 这个类就是缺省的适配扩展。@Adaptive 注解在扩展点 Interface 的方法上时 , dubbo 动态的生成一个这个扩展点的适配扩展类(生成代码 ,动态编译实例化 Class ),名称为 扩展点 Interface 的简单类名 + $Adaptive ,例如 : ProxyFactory$Adpative 。这么做的目的是为了在运行时去适配不同的扩展实例 , 在运行时通过传入的 URL 类型的参数或者内部含有获取 URL 方法的参数 ,从 URL 中获取到要使用的扩展类的名称 ,再去根据名称加载对应的扩展实例 ,用这个扩展实例对象调用相同的方法 。如果运行时没有适配到运行的扩展实例 , 那么就使用 @SPI 注解缺省指定的扩展。通过这种方式就实现了运行时去适配到对应的扩展。

运行时动态生成的适配扩展类代码:

package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;

        // 从 URL 中获取扩展名称 , "dubbo" 是从 ExtensionLoader 对象中的 cachedDefaultName 
        // 属性获取到的 , cachedDefaultName 是扩展点上 @SPI 注解中 value 属性指定的 
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");

        // 通过扩展名称获取扩展实例对象 , 调用扩展实例对象的相同方法 
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();

        // 从 URL 中获取扩展名称 
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");

        // 通过扩展名称获取扩展实例对象 , 调用扩展实例对象的相同方法
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
}
package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class ProxyFactory$Adpative implements com.alibaba.dubbo.rpc.ProxyFactory {
    public java.lang.Object getProxy(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();

        // 从 URL 中获取扩展名称 
        String extName = url.getParameter("proxy", "javassist");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");

        // 通过扩展名称获取扩展实例对象 , 调用扩展实例对象的相同方法
        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getProxy(arg0);
    }

    public com.alibaba.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, com.alibaba.dubbo.common.URL arg2) throws java.lang.Object {
        if (arg2 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg2;

        // 从 URL 中获取扩展名称 
        String extName = url.getParameter("proxy", "javassist");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");

        // 通过扩展名称获取扩展实例对象 , 调用扩展实例对象的相同方法
        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getInvoker(arg0, arg1, arg2);
    }
}

这些代码都是模板代码 , 最核心的代码就只有一行 , 这行代码是去获取一个指定名称的扩展实例对象 :

ExtensionLoader.getExtensionLoader(Xxx.class).getExtension(extName);

在使用 dubbo 生成的源码时要注意 , 它生成的代码是有错误的, 比如 Protocol$Adpative 类的中的 refer 和 export 方法签名 throws 的不是 Protocol 接口中方法定义抛出的 RpcException , 而是 Class ,和 Invoker 。 这个问题存在于 dubbo 2.5.4 之前的版本中,在 2.5.4 版本中修复了。

扩展加载器 ExtensionLoader

扩展加载器绝对是一个核心组件了 ,它控制着 dubbo 内部所有扩展点的初始化、加载扩展的过程。这个类的源码是很有必要深入学习的。从 Dubbo 内核设计简图可以看到,现在的学习还没有接触到 dubbo 的内核。

image.png

ExtensionLoader 中会存储两个静态属性 , EXTENSION_LOADERS 保存了内核开放的扩展点对应的 ExtensionLoader 实例对象 (说明了一种扩展点有一个对应的 ExtensionLoader 对象)。EXTENSION_INSTANCES 保存了扩展类型 (Class) 和扩展类型的实例对象。

ExtensionLoader 对象中的属性 :

Class<?> type;

ExtensionFactory objectFactory;

ConcurrentMap<Class<?>, String> cachedNames;

Holder<Map<String, Class<?>>> cachedClasses;

Map<String, Activate> cachedActivates;

Class<?> cachedAdaptiveClass;

ConcurrentMap<String, Holder<Object>> cachedInstances;

String cachedDefaultName;

Holder<Object> cachedAdaptiveInstance;

Throwable createAdaptiveInstanceError;

Set<Class<?>> cachedWrapperClasses;

Map<String, IllegalStateException> exceptions;

type : 被 @SPI 注解的 Interface , 也就是扩展点。

objectFactory : 扩展工厂,可以从中获取到扩展类型实例对象 ,缺省为 AdaptiveExtensionFactory。

cachedNames : 保存不满足装饰模式(不存在只有一个参数,并且参数是扩展点类型实例对象的构造函数)的扩展的名称。

cachedClasses : 保存不满足装饰模式的扩展的 Class 实例 , 扩展的名称作为 key , Class 实例作为 value。

cachedActivates : 保存不满足装饰模式 , 被 @Activate 注解的扩展的 Class 实例。

cachedAdaptiveClass : 被 @Adpative 注解的扩展的 Class 实例 。

cachedInstances : 保存扩展的名称和实例对象 , 扩展名称为 key , 扩展实例为 value。

cachedDefaultName : 扩展点上 @SPI 注解指定的缺省适配扩展。

createAdaptiveInstanceError : 创建适配扩展实例过程中抛出的异常。

cachedWrapperClasses : 满足装饰模式的扩展的 Class 实例。

exceptions : 保存在加载扩展点配置文件时,加载扩展点过程中抛出的异常 , key 是当前读取的扩展点配置文件的一行 , value 是抛出的异常。

附: dubbo 开放的扩展点

com.alibaba.dubbo.cache.CacheFactory
com.alibaba.dubbo.common.compiler.Compiler
com.alibaba.dubbo.common.extension.ExtensionFactory
com.alibaba.dubbo.common.logger.LoggerAdapter
com.alibaba.dubbo.common.serialize.Serialization
com.alibaba.dubbo.common.status.StatusChecker
com.alibaba.dubbo.common.store.DataStore
com.alibaba.dubbo.common.threadpool.ThreadPool
com.alibaba.dubbo.container.Container
com.alibaba.dubbo.container.page.PageHandler
com.alibaba.dubbo.monitor.MonitorFactory
com.alibaba.dubbo.registry.RegistryFactory
com.alibaba.dubbo.remoting.Codec2
com.alibaba.dubbo.remoting.Dispatcher
com.alibaba.dubbo.remoting.exchange.Exchanger
com.alibaba.dubbo.remoting.http.HttpBinder
com.alibaba.dubbo.remoting.p2p.Networker
com.alibaba.dubbo.remoting.telnet.TelnetHandler
com.alibaba.dubbo.remoting.Transporter
com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter
com.alibaba.dubbo.rpc.cluster.Cluster
com.alibaba.dubbo.rpc.cluster.ConfiguratorFactory
com.alibaba.dubbo.rpc.cluster.LoadBalance
com.alibaba.dubbo.rpc.cluster.Merger
com.alibaba.dubbo.rpc.cluster.RouterFactory
com.alibaba.dubbo.rpc.Filter
com.alibaba.dubbo.rpc.InvokerListener
com.alibaba.dubbo.rpc.Protocol
com.alibaba.dubbo.rpc.protocol.thrift.ClassNameGenerator
com.alibaba.dubbo.rpc.ProxyFactory
com.alibaba.dubbo.validation.Validation

总结一下,Dubbo SPI有以下的特点:

  • 对Dubbo进行扩展,不需要改动Dubbo的源码
  • 自定义的Dubbo的扩展点实现,是一个普通的Java类,Dubbo没有引入任何Dubbo特有的元素,对代码侵入性几乎为零。
  • 将扩展注册到Dubbo中,只需要在ClassPath中添加配置文件。使用简单。而且不会对现有代码造成影响。符合开闭原则。
  • Dubbo的扩展机制支持IoC,AoP等高级功能
  • Dubbo的扩展机制能很好的支持第三方IoC容器,默认支持Spring Bean,可自己扩展来支持其他容器,比如Google的Guice。
  • 切换扩展点的实现,只需要在配置文件中修改具体的实现,不需要改代码。使用方便。

参考:

理解 Dubbo SPI 扩展机制

聊聊Dubbo - Dubbo可扩展机制实战

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343