dubbo spi

Dubbo的扩展机制

在Dubbo的官网上,Dubbo描述自己是一个高性能的RPC框架。今天我想聊聊Dubbo的另一个很棒的特性, 就是它的可扩展性。 如同罗马不是一天建成的,任何系统都一定是从小系统不断发展成为大系统的,想要从一开始就把系统设计的足够完善是不可能的,相反的,我们应该关注当下的需求,然后再不断地对系统进行迭代。在代码层面,要求我们适当的对关注点进行抽象和隔离,在软件不断添加功能和特性时,依然能保持良好的结构和可维护性,同时允许第三方开发者对其功能进行扩展。在某些时候,软件设计者对扩展性的追求甚至超过了性能。
在谈到软件设计时,可扩展性一直被谈起,那到底什么才是可扩展性,什么样的框架才算有良好的可扩展性呢?它必须要做到以下两点:

  1. 作为框架的维护者,在添加一个新功能时,只需要添加一些新代码,而不用大量的修改现有的代码,即符合开闭原则。
  2. 作为框架的使用者,在添加一个新功能时,不需要去修改框架的源码,在自己的工程中添加代码即可。
    Dubbo很好的做到了上面两点。这要得益于Dubbo的微内核+插件的机制。
    接下来的章节中我们会慢慢揭开Dubbo扩展机制的神秘面纱。

java SPI

SPI(service provider interface)机制。这种机制的原理是假如我们定义了服务接口标准,可以让厂商无实现。在jdk中,使用ServiceLoader类来实现spi机制的服务查找功能。

分下面几个步骤来使用:

  1. 创建一个接口文件
  2. 在resources资源目录下创建META-INF/services文件夹
  3. 在services文件夹中创建文件,以接口全名命名
  4. 创建接口实现类
    注意:其中后三步要生成一个jar包
      接下来我们使用一个简单的例子,通过ServiceLoader来实现spi机制。

先定义一个接口:

package domain.spi;

public interface Jdbc {
    boolean isSupportType(String type);
    String getConnection();
}

ServiceLoader会遍历所有的jar去查找META-INF/services/domain.spi.jdbc文件:

在mysql厂商提供的jar包中的META-INF/services/domain.spi.jdbc文件内容为:domain.spi. MysqlJdbc(jdbc实现全路径类名)
在oracle厂商提供的jar包中的META-INF/services/domain.spi.jdbc文件内容为:domain.spi. OracleJdbc(jdbc实现全路径类名)

package domain.spi;

public class MysqlJdbc implements Jdbc {

    @Override
    public boolean isSupportType(String type) {
        return "mysql".equals(type);
    }

    @Override
    public String getConnection() {
        return "mysql connection";
    }
}
public class OracleJdbc implements Jdbc {

    @Override
    public boolean isSupportType(String type) {
        return "oracle".equals(type);
    }

    @Override
    public String getConnection() {
        return "oracle connection";
    }
}

使用ServiceLoader.load加载

ServiceLoader.load(Jdbc.class);读取厂商mysql、oracle提供jar包中的文件,ServiceLoader实现了Iterable接口可通过while for循环语句遍历出所有实现。

一个接口多种实现,就如策略模式一样提供了策略的实现,但是没有提供策略的选择, 使用方可以根据isSupport方法根据业务传入厂商名来选择具体的厂商。

package domain.spi;
import java.util.ServiceLoader;

public class JdbcFactory {
    private static ServiceLoader<Jdbc> jdbcServices = ServiceLoader.load(Jdbc.class);

    public static Jdbc getSpi(String name) {
        for (Jdbc jdbc : jdbcServices) {
            if (jdbc.isSupportType(name)) {
                return jdbc;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        getSpi("mysql");
    }
}

实际上,Java确实定义了一套JDBC的接口,并通过破坏双亲委派模型实现 ,
当使用mysql,就将mysql-jdbc-connector.jar引入
当使用oracle,就将oracle-jdbc-connector.jar引入
系统运行时,从META-INF/services/java.sql.Driver文件中获取具体的实现类名进行加载。

Spring spi

由于spring 容器中有IOC,一般不需要原生java spi,可以借助spring 容器实现

@Order(1)  
@Component
public class MysqlJdbc implements Jdbc {}

@Order(2)  
@Component
public class OracleJdbc implements Jdbc {}
@Autowired
private List<Jdbc> jdbcList; //实现类中加入@Order(value) 注解,值越小越先被初始化越先被放入List

@Autowired  
private Map<String, Jdbc> map;//key 默认为mysqlJdbc oracleJdbc

下面这种试太过麻烦,不过可做参考:
Spring Boot中如何干掉过多的if else! - 51CTO.COM

dubbo SPI

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。

概念

在深入学习Dubbo的扩展机制之前,我们先明确Dubbo SPI中的一些基本概念。在接下来的内容中,我们会多次用到这些术语。

  1. 扩展点(Extension Point)
    是一个Java的接口。
  2. 扩展(Extension)
    扩展点的实现类。
  3. 扩展实例(Extension Instance)
    扩展点实现类的实例。
  4. 扩展自适应实例(Extension Adaptive Instance)
    可以认为是扩展代理类,例如自动生成的LoadBalance$Adaptive类。扩展的自适应实例其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。比如一个IRepository的扩展点,有一个save方法。有两个实现MysqlRepository和MongoRepository。IRepository的自适应实例在调用接口方法的时候,会根据save方法中的参数,来决定要调用哪个IRepository的实现。如果方法参数中有repository=mysql,那么就调用MysqlRepository的save方法。如果repository=mongo,就调用MongoRepository的save方法。和面向对象的延迟绑定很类似。为什么Dubbo会引入扩展自适应实例的概念呢?
    • Dubbo中的配置有两种,一种是固定的系统级别的配置,在Dubbo启动之后就不会再改了。还有一种是运行时的配置,可能对于每一次的RPC,这些配置都不同。比如在xml文件中配置了超时时间是10秒钟,这个配置在Dubbo启动之后,就不会改变了。但针对某一次的RPC调用,可以设置它的超时时间是30秒钟,以覆盖系统级别的配置。对于Dubbo而言,每一次的RPC调用的参数都是未知的。只有在运行时,根据这些参数才能做出正确的决定。
    • 很多时候,我们的类都是一个单例的,比如Spring的bean,在Spring bean都实例化时,如果它依赖某个扩展点,但是在bean实例化时,是不知道究竟该使用哪个具体的扩展实现的。这时候就需要一个代理模式了,它实现了扩展点接口,方法内部可以根据运行时参数,动态的选择合适的扩展实现。而这个代理就是自适应实例。 自适应扩展实例在Dubbo中的使用非常广泛,Dubbo中,每一个扩展都会有一个自适应类,如果我们没有提供,Dubbo会使用字节码工具为我们自动生成一个。所以我们基本感觉不到自适应类的存在。后面会有例子说明自适应类是怎么工作的。

实现原理

  1. @SPI
    @SPI注解作用于扩展点的接口上,表明该接口是一个扩展点。可以被Dubbo的ExtentionLoader加载。如果没有此ExtensionLoader调用会异常。
  2. @Adaptive
    @Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。方法内部会根据方法的参数,来决定使用哪个扩展。
  3. ExtentionLoader
    类似于Java SPI的ServiceLoader,负责扩展的加载和生命周期维护。
  4. 扩展别名
    和Java SPI不同,Dubbo中的扩展都有一个别名,用于在应用中引用它们。比如
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance

其中的random,roundrobin就是对应扩展的别名。这样我们在配置文件中使用random或roundrobin就可以了。

  1. 配置文件路径
    和Java SPI从/META-INF/services目录加载扩展配置类似,Dubbo也会从以下路径去加载扩展配置文件:
    (1)META-INF/dubbo/internal
    (2)META-INF/dubbo
    (3)META-INF/services

使用示例

/**
 * LoadBalance. (SPI, Singleton, ThreadSafe)
 * RandomLoadBalance.NAME=random
 */
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

    //select one invoker in list.
    @Adaptive("loadbalance")
    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}

LoadBalance接口只有一个select方法。select方法从多个invoker中选择其中一个。上面代码中和Dubbo SPI相关的元素有:

  • @SPI(RandomLoadBalance.NAME) @SPI作用于LoadBalance接口,表示接口LoadBalance是一个扩展点。如果没有@SPI注解,试图去加载扩展时,会抛出异常。@SPI注解有一个参数,该参数表示该扩展点的默认实现的别名。如果没有显示的指定扩展,就使用默认实现。RandomLoadBalance.NAME是一个常量,值是”random”,是一个随机负载均衡的实现。
  • random的定义在配置文件META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance中:
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

可以看到文件中定义了4个LoadBalance的扩展实现。由于负载均衡的实现不是本次的内容,这里就不过多说明。只用知道Dubbo提供了4种负载均衡的实现,我们可以通过xml文件,properties文件,JVM参数显式的指定一个实现。如果没有,默认使用随机。


1.png
  • @Adaptive(“loadbalance”)
    @Adaptive注解修饰select方法,表明方法select方法是一个可自适应的方法。Dubbo会自动生成该方法对应的代码。当调用select方法时,会根据具体的方法参数来决定调用哪个扩展实现的select方法。@Adaptive注解的参数loadbalance表示方法参数中的loadbalance的值作为实际要调用的扩展实例。 但奇怪的是,我们发现select的方法中并没有loadbalance参数,那怎么获取loadbalance的值呢?select方法中还有一个URL类型的参数,Dubbo就是从URL中获取loadbalance的值的。这里涉及到Dubbo的URL总线模式,简单说,URL中包含了RPC调用中的所有参数。URL类中有一个Map parameters字段,parameters中就包含了loadbalance。
    Dubbo中获取LoadBalance的代码如下:
    LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
    使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法获取一个ExtensionLoader的实例,然后调用getExtension,传入一个扩展的别名来获取对应的扩展实例。

@Adaptive

三种使用方式:
方式一:Url 中parameters(map)参数中的loadbalance变量值
@Adaptive("loadbalance")
在初始化LoadBalance时,会生成一个LoadBalance$Adaptive类,并会实现select方法,方法里是一些抽象的通用逻辑,通过Url中loadbalance的值调用真正的实现类。
方式二:注解放在实现类上
当 @Adaptive注解放在实现类上时,则整个实现类会直接作用默认实现,不再自动生成代码。
方式三:通过别名获取扩展

LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);

@Activate

Activate注解标识一个扩展是否被激活和使用,适用于有多个扩展点实现,需要根据不同条件被激活的场景,与@Adaptive不同的时,实现可以多个同时激活,如如Filte。所以不会存在@Active与@Adaptive同时使用的情况。
类似于Spring的AOP功能

自定义一个LoadBalance扩展

我们通过一个简单的例子,来自己实现一个LoadBalance,并把它集成到Dubbo中。

  1. 实现LoadBalance接口
    首先,编写一个自己实现的LoadBalance,选择第一个invoker,并在控制台输出一条日志。
package com.dubbo.spi.demo.consumer;
public class DemoLoadBalance implements LoadBalance {
    @Override
    public Invoker select(List>invokers, URL url, Invocation invocation) throws RpcException {
        System.out.println(“DemoLoadBalance:Select the first invoker…”);
        return invokers.get(0);
    }
}
  1. 添加扩展配置文件
    添加文件:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance。
    文件内容如下:demo=com.dubbo.spi.demo.consumer.DemoLoadBalance
  2. 配置使用自定义LoadBalance
    通过上面的两步,已经添加了一个名字为demo的LoadBalance实现,并在配置文件中进行了相应的配置。
    接下来,需要显式的告诉Dubbo使用demo的负载均衡实现。
@Reference(loadbalance = “demo”)
private IHelloService helloService;
  1. 启动Dubbo
    启动Dubbo,调用一次IHelloService,可以看到控制台会输出一条DemoLoadBalance: Select the first invoker…日志。
    说明Dubbo的确是使用了我们自定义的LoadBalance。

引用

聊聊Dubbo - Dubbo可扩展机制实战-云栖社区-阿里云
理解 Dubbo SPI 扩展机制 - OSC知行合一的个人空间 - OSCHINA
聊聊Dubbo - Dubbo可扩展机制源码解析-云栖社区-阿里云

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