Dubbo 源码分析(一) 初识 SPI 机制


前言

之前简单涉猎过 Dubbo 的核心源码,时间一长,难免有些内容就忘了。因此,这次细读 Dubbo 源码时,决定把相关知识点记录下来,毕竟写下来和自己想想差别挺大的,虽然会花费比较长的时间,这样也会加深印象。本系列文章仅供自己学习使用,如果有任何侵权,欢迎告知。


简介

Dubbo 作为一款优秀的 RPC 开源框架,其具备良好的可拓展性,我们可以拓展负载均衡、注册中心等实现,这都基于 Dubbo SPI 加载机制,而且 Dubbo 源码中也是大量使用 SPI 机制。因此,阅读 Dubbo RPC 核心之前,搞懂 Dubbo SPI 是有一定的必要。

SPI 全称为 (Service Provider Interface) ,是一种服务发现机制。我们可以在配置文件中配置接口实现类,由服务加载器去读取配置并加载实现类,这样在运行时可以动态为接口替换实现类。Java 本身支持 SPI 方式,比如我们常用的 java.sql.Driver 就是基于这一原理,不同数据库厂商可以通过 SPI 方式提供不同的接口实现。但 Dubbo 并未使用 Java 原生的 SPI 机制,而是自己实现了一套 SPI 机制,进行功能增强。


Java SPI 示例

  1. 定义接口
public interface Person {
    void sayHello();
}
  1. 两个实现类
public class Teacher implements Person {
    @Override
    public void sayHello() {
        System.out.println("hello, I am a teacher");
    }
}

public class Student implements Person {
    @Override
    public void sayHello() {
        System.out.println("hello, I am a student");
    }
}
  1. 创建配置文件
    在 META-INF/services 文件夹下创建配置文件,文件名为接口全限定名com.java.example.Person 文件内容为实现类的全限定的类名。
com.java.example.Teacher
com.java.example.Student
  1. 测试类
public class JavaSPITest {
    @Test
    public void sayHello() {
        ServiceLoader<Person> serviceLoader = ServiceLoader.load(Person.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Person::sayHello);
    }
}
运行结果:
Java SPI
hello, I am a teacher
hello, I am a student

Dubbo SPI 示例

  1. 在 META-INF/dubbo 路径新建配置文件,文件名依旧是接口全限定名com.java.example.Person,只不过内容和 Java SPI 不同
teacher=com.java.example.Teacher
student=com.java.example.Student
  1. 在 Person 接口上加上 @SPI 注解
@SPI
public interface Person {
    void sayHello();
}
  1. 创建测试类
public class DubboSPITest {
    @Test
    public void sayHello() {
        System.out.println("Dubbo SPI");
        ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
        Person teacher = extensionLoader.getExtension("teacher");
        teacher.sayHello();
        Person student = extensionLoader.getExtension("student");
        student.sayHello();
    }
}
运行结果:
Dubbo SPI
hello, I am a teacher
hello, I am a student

Java SPI 和 Dubbo SPI 对比

看到这里你是不是觉得 Dubbo SPI 和 Java SPI 好像没有太大的不同之处,为啥 Dubbo 还需要自己实现一套呢,下面我们就开始介绍 Dubbo SPI 对比 Java SPI 功能增强之处。

  1. Java SPI 不能通过指定名称使用具体的实现类,只能通过遍历的方式拿到所有实现。Dubbo SPI 可以通过 getExtension("name") 的方式获取指定实现类。
  2. Java SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  3. Java SPI 很单纯,Dubbo SPI 增加了对拓展点 IOC 和 AOP 的支持。一个扩展点可以直接 setter 注入其它扩展点,并且可以和 Spring 容器进行集成,注入Spring bean。也可以通过 Warpper 类的构造方法,对一个拓展点进行 AOP 前后增强。

Dubbo SPI 进阶使用

下面就对这些 Dubbo SPI 的改进功能进行一一介绍。

  1. 自动包装( AOP 功能)
    ExtensionLoader 在加载拓展时,如果发现这个拓展类包含其他拓展点作为构造函数的参数,则这个拓展类就会被认为是 Wrapper 类。
    1). 定义 Wrapper
    public class PersonWrapper implements Person {
    
       private Person person;
    
       public PersonWrapper(Person person) {
           this.person = person;
       }
    
       @Override
       public void sayHello() {
           System.out.println("before....");
           person.sayHello();
           System.out.println("after....");
       }
    }
    
    2). 修改配置文件,添加如下内容
    personWrapper=com.java.example.PersonWrapper
    
    3). 执行DubboSPITest. sayHello()
    运行结果:
    Dubbo SPI
    before....
    hello, I am a teacher
    after....
    before....
    hello, I am a student
    after....
    

Dubbo 通过这种包装类方式,自动完成了 AOP 的前后增强。

  1. 自动装配(依赖注入功能)
    如果某个拓展类是另一个拓展类的成员属性,并且拥有setter方法,就会自动装配对应的拓展点实例,具体装配哪个实例,可以根据 @Adaptive 类自适应,通过调用 getAdaptiveExtension() 方法装配。
    1). 添加Dao接口和DaoImpl
    @SPI
    public interface Dao {
    
        @Adaptive
        void insert(URL url);
    
    }
    
    public class DaoImpl implements Dao {
        @Override
        public void insert(URL url) {
            System.out.println("dao insert()");
        }
    }
    
    2). 添加 Dao 配置文件在 META-INF/dubbo ,文件名com.java.example.Dao
    daoImpl=com.java.example.DaoImpl
    
    3). 修改 Student 类
    public class Student implements Person {
    
        private Dao dao;
    
        public void setDao(Dao dao) {
            this.dao = dao;
        }
    
        @Override
        public void sayHello(URL url) {
            System.out.println("hello, I am a student");
            dao.insert(url);
        }
    }
    
    4). 测试方法
    @Test
    public void testAdaptive() {
        ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
        Person person = extensionLoader.getExtension("student");
        URL url = new URL("p1", "1.2.3.4", 1010, "path1");
        url = url.addParameters("dao", "daoImpl");
        person.sayHello(url);
    }
    
    运行结果:
    hello, I am a student
    dao insert()
    

Dubbo SPI 中会为 Dao 接口自动生成一个 Dao$Adaptive 代理类,根据 URL 参数动态获取具体的实现。

  1. 拓展点自适应
    动态获取实现类,Dubbo 中主要通过 @Adaptive 注解,@Adaptive 可以标记在类和方法上,调用 ExtensionLoader#getAdaptiveExtension 方法获取动态的实现类,每次只会获得一个实现类。
    1). 标记在某个实现类上时,该实现类会被 cache 到 cachedAdaptiveClasses 中,getAdaptiveExtension 会获取该实现类,这种方式优先级最高。
    2). 标记在接口的方法上,Dubbo 会为接口的方法生成代理类,并且封装了 URL 参数的逻辑,根据 URL 传参调用对应的实现类,可参考2中的 @Adaptive 使用方式。
    自动生成 Dao$Adaptive 代理类代码如下,并且 @Adaptive 注解未定义 value,默认参数为接口名的驼峰规则,自动根据接口名大小写分开,也可以在注解上自定义参数。
package com.java.example;

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

public class Dao$Adaptive implements com.java.example.Dao {
    public void insert(com.alibaba.dubbo.common.URL arg0) {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg0;
        String extName = url.getParameter("dao");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.java.example.Dao) name from url(" + url.toString() + ") use keys([dao])");
        com.java.example.Dao extension = (com.java.example.Dao) ExtensionLoader.getExtensionLoader(com.java.example.Dao.class).getExtension(extName);
        extension.insert(arg0);
    }
}

自适应优先级

  • 实现类标记 @Adaptive 注解
  • 方法上标记 @Adaptive 时,调用时 URL 传参数
  • 方法上标记 @Adaptive 时,调用时未传URL参数,则根据 @SPI 上定义的 value
  1. 自动激活
    @Adaptive 动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现类同时被激活的话,如 Filter 过滤器等,那么就需要用到自动激活。可以在实现类上添加 @Activate(group = {"default_group"}),之后可以通过调用 getActivateExtension(URL url, String[] values, String group) 方法,获得 List<T>,这里就不再展现示例代码了。

至此,我们清楚了 Dubbo SPI 有了哪些改进功能,下一篇我们分析 Dubbo SPI 的实现原理。

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