🍎 Dubbo SPI 之 Adaptive 自适应类

翻看 Dubbo 的源码,不难发现,框架到处都在用 SPI 机制进行扩展。这是由于 Dubbo 框架对各种层做了很多的实现方式,然后由用户自己去选择具体的实现方式。比如 ProtocolDubbo 提供的实现就有 dubbohessianthriftredisinJvm 等等。这么多的实现方式,Dubbo 是如何做到动态的切换的呢?

接口自适应与 @Adaptive 注解

Dubbo 没有采用 JDK 提供的 SPI 机制,而是自己实现了一套,增强了很多功能。具体细节可以浏览网上其他博客。

Dubbo 充分利用面向对象思想,每一个组件内引入其他组件都是以接口的形式进行依赖,动态的 inject 实现类。所以这种思想上也用到了 AOPDI 的思想。
我们定义一个接口 Registry,假定它的功能是将本地服务暴露到注册中心,以及从注册中心获取可用服务,属于 RPC 框架中的服务注册与发现组件。

@SPI("zookeeper")
public interface Registry {
    /**
     * 注册服务
     */
    @Adaptive()
    String register(URL url, String content);
    /**
     * 发现服务
     */
    @Adaptive()
    String discovery(URL url, String content);
}
  • @SPI 注解标注这是一个可扩展的组件,注解内的内容后文详细介绍。
  • 该接口定义了两个方法 registerdiscovery,分别代表注册服务和发现服务。两个方法中都有 URL 参数,该类是 Dubbo 内置的类,代表了 Dubbo 整个执行过程中的上下文信息,包括各类配置信息,参数等。content 代表备注信息。
  • @Adaptive()注解表明这两个方法都是自适应方法,具体作用后文分析。

下面分别是两个实现类 ZookeeperRegistryEtcdRegistry

ZookeeperRegistry

public class ZookeeperRegistry implements Registry {
    private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);

    @Override
    public String register(URL url, String content) {
        logger.info("服务: {} 已注册到zookeeper上,备注: {}", url.getParameter("service"), content);

        return "Zookeeper register already! ";
    }

    @Override
    public String discovery(URL url, String content) {
        logger.info("zookeeper上发现服务: {} , 备注: {}", url.getParameter("service"), content);

        return "Zookeeper discovery already! ";
    }
}

EtcdRegistry

public class EtcdRegistry implements Registry {

    private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);

    @Override
    public String register(URL url, String content) {
        logger.info("服务: {} 已注册到 Etcd 上,备注: {}", url.getParameter("service"), content);

        return "Etcd register already! ";
    }

    @Override
    public String discovery(URL url, String content) {
        logger.info("Etcd 上发现服务: {} , 备注: {}", url.getParameter("service"), content);

        return "Etcd discovery already! ";
    }
}

我们可以将服务注册信息注册到老牌注册中心 zookeeper 上,或者使用新兴流行轻量级注册中心 etcd 上。

配置扩展点实现信息到 Resource 目录下

Resource 下的 META-INF.dubbo 下新建 以Registry 全限定名为名的文件,配置实现类信息,以 key-value 的形式。(这里要注意与 JDK 默认的 SPI 机制的区别)

spi.png

文件内内容如下:

etcd=com.maple.spi.impl.EtcdRegistry
zookeeper=com.maple.spi.impl.ZookeeperRegistry

测试 Dubbo SPI 自适应类

public class Main {
    public static void main(String[] args) {
        URL url = URL.valueOf("test://localhost/test")
                      .addParameter("service", "helloService");

        Registry registry = ExtensionLoader.getExtensionLoader(Registry.class)
                                           .getAdaptiveExtension();
        String register = registry.register(url, "maple");

        System.out.println(register);
    }
}

该程序首先通过 Registry 接口得到它专属的 ExtensionLoader 实例,然后调用 getAdaptiveExtension 拿到该接口的自适应类。Dubbo 会判断是否有实现类(即实现了 Registry 接口) 上有注解 @Adaptive,如果没有就会动态生成。本例子将会动态生成。

直接运行程序结果如下,程序最终选择的实现类是 ZookeeperRegistry,控制台结果如下:

09-20 23:43:13 323 main INFO - 服务: helloService 已注册到zookeeper上,备注: maple
Zookeeper register already! 

上面代码我们没有看到任何实现类的信息,Dubbo SPI 机制会为动态的去调用实现类。

我们重点分析 getAdaptiveExtension方法找到的是 Registry 的自适应类,可以理解为是 Registry 的一个 适配器和代理类。如果该适配器类不存在,Dubbo 会通过动态代理方式在运行时自动生成一个自适应类。

打开 DEBUG 日志,在控制台我们看到了 Dubbo 生成的类的源码如下:

package com.maple.spi;

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

public class Registry$Adaptive implements com.maple.spi.Registry {
    public java.lang.String register(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("registry", "zookeeper");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");
        com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);
        return extension.register(arg0, arg1);
    }

    public java.lang.String discovery(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("registry", "zookeeper");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");
        com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);
        return extension.discovery(arg0, arg1);
    }
}

看代码,该适配器类的作用是类似于 AOP 的功能,再调用具体的实现类之前,先通ExtensionLoader.getExtensionLoader(Registry.class).getExtension(extName);
根据 extNameloader 具体的实现类,然后再去调用实现类的相应的方法。

分析上面代码中的一句:

 String extName = url.getParameter("registry", "zookeeper");

extName 可以通过 url 进行传递,默认值为 zookeeper, 该默认值即为我们定义的接口上的注解 @SPI 里的内容,上文我们定义的 @SPI("zookeeper"),所以这里的默认值为 zookeeper, 当 url 中没有对应的参数时,我们会去拿默认值。

我们可以修改 Main 测试程序,增加 keyregistryparameter

public static void main(String[] args) {
        URL url = URL.valueOf("test://localhost/test").addParameter("service", "helloService")
                .addParameter("registry","etcd");

        Registry registry = ExtensionLoader.getExtensionLoader(Registry.class).getAdaptiveExtension();

        String register = registry.register(url, "maple");

        System.out.println(register);

    }

URL 中增加 Key,并设置值为 etcd,运行程序,结果如下:

09-20 23:44:00 009 main INFO - 服务: helloService 已注册到 Etcd 上,备注: maple
Etcd register already! 

实现类已经切换为 EtcdRegistry 了。

@Adaptive 注意细节

@Adaptive 源码

public @interface Adaptive {
    String[] value() default {};
}

细心的读者已经发现了 @Adaptivevalue这个属性。上文在接口方法上定义的 @Adaptive 是没有设置值的。如果没有定义值,Dubbo 默认会使用一种策略生成。这种策略是将类名定义的驼峰法则转换为小写,并以 .号区分。 例如上文的接口名为 Registry,那么这个Key 值就是 registry。如果接口名为 HelloWorldKey 值就为 hello.world

当然如果 @Adaptive 是有值的话,优先按里面的这个值来作为 Key,例如 Dubbo 框架中的接口 RegistryFactory ,该接口的自适应类将会从 URLprotocolkey 来找实现类的 extName

@SPI("dubbo")
public interface RegistryFactory {
   @Adaptive({"protocol"})
    Registry getRegistry(URL url);
}

总结

Dubbo Adaptive 模式在整个框架中运用十分广泛,如果用户没有在 URL 中进行自定义,Dubbo 默认会去加载扩展点接口上 @SPI 标注的内容,如果此注解没有值,那么我们就必须要在 URL 中进行值的传递了。如果我们想覆盖 Dubbo 默认的实现策略。可以通过在 URL 中增加 key-value 的形式来改变。

这样一个组件充分体现了设计模式,对修改关闭,对扩展开放的原则。当我们想自己实现 Dubbo 中的某个组件时,我们完全可以通过 Dubbo Adaptive 来动态的切换程序使用我们提供的组件。

彻底理解 Dubbo SPI 模式,以及 Adaptive 自适应类, Activate 激活类等,是看 Dubbo 源码的基础。如果我们想十分流畅的去分析 Dubbo 内部其他组件的实现机制,第一道要跨过的坎便是 Dubbo SPI

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

推荐阅读更多精彩内容