Dubbo SPI 机制解析

从上一篇 Java SPI 机制解析 可以知道 Java SPI 的一些劣势。Dubbo 的扩展点加载从 Java SPI 扩展点发现机制加强而来。
Dubbo 改进了 Java SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

本文从以下几个方面,深入解析 Dubbo SPI 机制:

  • Dubbo SPI 特性
  • Dubbo SPI 的一些定义
  • Dubbo SPI 源码解析

Dubbo SPI 特性

扩展点自动包装

自动包装扩展点的 Wrapper 类。ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。Wrapper类内容:

package com.alibaba.xxx;
 
import com.alibaba.dubbo.rpc.Protocol;
 
public class XxxProtocolWrapper implements Protocol {
    Protocol impl;
 
    public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }
 
    // 接口方法做一个操作后,再调用extension的方法
    public void refer() {
        //... 一些操作
        impl.refer();
        // ... 一些操作
    }
}

Wrapper 类同样实现了扩展点接口,但是 Wrapper 不是扩展点的真正实现。它的用途主要是用于从 ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外。即从 ExtensionLoader 中返回的实际上是 Wrapper 类的实例,Wrapper 持有了实际的扩展点实现类。

扩展点的 Wrapper 类可以有多个,也可以根据需要新增。通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点。

扩展点自动装配

加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作。

示例:有两个为扩展点 CarMaker(造车者)、WheelMaker (造轮者)

public interface CarMaker {
    Car makeCar();
}
 
public interface WheelMaker {
    Wheel makeWheel();
}

CarMaker 的一个实现类:

public class RaceCarMaker implements CarMaker {
    WheelMaker wheelMaker;
 
    public setWheelMaker(WheelMaker wheelMaker) {
        this.wheelMaker = wheelMaker;
    }
 
    public Car makeCar() {
        // ...
        Wheel wheel = wheelMaker.makeWheel();
        // ...
        return new RaceCar(wheel, ...);
    }
}

ExtensionLoader加载 CarMaker的扩展点实现RaceCar时,setWheelMaker方法的 WheelMaker也是扩展点则会注入WheelMaker的实现。

这里带来另一个问题,ExtensionLoader要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker的实现中要注入哪个。这个问题在下面一点扩展点自适应 中说明。

扩展点自适应

ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是一个扩展点实现。

Dubbo 使用 URL 对象(包含了Key-Value)传递配置信息。扩展点方法调用会有URL参数(或是参数有URL成员)。这样依赖的扩展点也可以从URL拿到配置信息,所有的扩展点自己定好配置的Key后,配置信息从URL上从最外层传入。URL在配置传递上即是一条总线。

当上面执行:

// ...
Wheel wheel = wheelMaker.makeWheel(url);
// ...

注入的 Adaptive 实例可以提取约定 Key 来决定使用哪个 WheelMaker 实现来调用对应实现的真正的 makeWheel 方法。如提取 wheel.type, key 即 url.get("wheel.type") 来决定 WheelMake 实现。Adaptive 实例的逻辑是固定,指定提取的 URL 的 Key,即可以代理真正的实现类上,可以动态生成。

在 Dubbo 的 ExtensionLoader 的扩展点类对应的 Adaptive 实现是在加载扩展点里动态生成。指定提取的 URL 的 Key 通过 @Adaptive 注解在接口方法上提供。

Dubbo SPI 的一些定义

@SPI注解,被此注解标记的接口,就表示是一个可扩展的接口。
@Adaptive注解,有两种注解方式:一种是注解在类上,一种是注解在方法上。
1、注解在类上,而且是注解在实现类上,目前dubbo只有AdaptiveCompiler和AdaptiveExtensionFactory类上标注了此注解,这是些特殊的类,ExtensionLoader需要依赖他们工作,所以得使用此方式。
2、注解在方法上,注解在接口的方法上,除了上面两个类之外,所有的都是注解在方法上。ExtensionLoader根据接口定义动态的生成适配器代码,并实例化这个生成的动态类。被Adaptive注解的方法会生成具体的方法实现。没有注解的方法生成的实现都是抛不支持的操作异常UnsupportedOperationException。被注解的方法在生成的动态类中,会根据url里的参数信息,来决定实际调用哪个扩展。
ExtensionLoader,是dubbo的SPI机制的查找服务实现的工具类,类似与Java的ServiceLoader,可做类比。dubbo约定扩展点配置文件放在classpath下的/META-INF/dubbo,/META-INF/dubbo/internal,/META-INF/services目录下,配置文件名为接口的全限定名,配置文件内容为配置名=扩展实现类的全限定名。

比如说这段代码:

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

当上面代码执行的时候,我们其实还不知道要真正使用的Protocol是什么,可能是具体的实现DubboProtocol,也可能是其他的具体实现的Protocol,那么这时候protocol到底是什么呢?protocol其实是在调用getAdaptiveExtension()方法时候,自动生成的一个类,代码如下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements Protocol {
  public Invoker refer(Class arg0, URL arg1) throws Class {
    if (arg1 == null) throw new IllegalArgumentException("url == null");

    URL url = arg1;
    String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );

    if(extName == null) throw new IllegalStateException("Fail to get extension(Protocol) name from url(" + url.toString() + ") use keys([protocol])");

    Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);

    return extension.refer(arg0, arg1);
  }

  public Exporter export(Invoker arg0) throws Invoker {
    if (arg0 == null) throw new IllegalArgumentException("Invoker argument == null");

    if (arg0.getUrl() == null) throw new IllegalArgumentException("Invoker argument getUrl() == null");URL url = arg0.getUrl();

    String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );

    if(extName == null) throw new IllegalStateException("Fail to get extension(Protocol) name from url(" + url.toString() + ") use keys([protocol])");

    Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);

    return extension.export(arg0);
  }

  public void destroy() {
    throw new UnsupportedOperationException("method public abstract void Protocol.destroy() of interface Protocol is not adaptive method!");
  }

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

可以看到被@Adaptive注解的方法都生成了具体的实现,并且实现逻辑都相同。而没有被注解的方法直接抛出不支持操作的异常。

当我们使用protocol调用方法的时候,其实是调用生成的类Protocol$Adpative中的方法,这里面的方法根据url中的参数配置来找到具体的实现类,找具体实现类的方式还是通过dubbo的扩展机制。比如url中可能会有protocol=dubbo,此时就可以根据这个dubbo来确定我们要找的类是DubboProtocol。可以查看下生成的代码中getExtension(extName)这里是根据具体的名字去查找实现类。

Dubbo SPI 源码解析

了解源码结构,建立一个全局认识。结果图如下:


下面以Protocol分析扩展点的加载

//先获取ExtensionLoader实例,然后加载自适应的Protocol扩展点
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
//发布服务
protocol.export(Invoker<T> invoker);

获取ExtensionLoader实例

getExtensionLoader(Protocol.class),根据要加载的接口Protocol,创建出一个ExtensionLoader实例,加载完的实例会被缓存起来,下次再加载Protocol的ExtensionLoader的时候,会使用已经缓存的这个,不会再新建一个实例:

    @SuppressWarnings("unchecked")
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        //扩展点类型不能为空
        if (type == null)
            throw new IllegalArgumentException("Extension type == null");
        //扩展点类型只能是接口类型的
        if(!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        //只有注解了@SPI的才会解析
        if(!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type + 
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }
        
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

获取自适应实现

上面返回一个ExtensionLoader的实例之后,开始加载自适应实现,加载是在调用getAdaptiveExtension()方法中进行的:


第一步,从cache中获取自适应扩展点。可以关注一下,这里用了双重校验锁,Dubbo源码很多地方都用了这种方式。

@SuppressWarnings("unchecked")
    public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get();
       
        if (instance == null) {
            if(createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            instance = createAdaptiveExtension();
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            }
    }

第二步,缓存中不存在自适应扩展的实例,则调用createAdaptiveExtension()方法创建自适应扩展点。大家一定了解过适配器设计模式,而这个自适应扩展点实际上就是一个适配器。

private T createAdaptiveExtension() {
    try {
        //先通过getAdaptiveExtensionClass获取AdaptiveExtensionClass
        //然后获取其实例
        //最后进行注入处理
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {}
}

第三步,getAdaptiveExtensionClass()获取自适应扩展点

private Class<?> getAdaptiveExtensionClass() {
    //加载当前Extension的所有实现(这里举例是Protocol,只会加载Protocol的所有实现类),如果有@Adaptive类型的实现类,会赋值给cachedAdaptiveClass
    //目前只有AdaptiveExtensionFactory和AdaptiveCompiler两个实现类是被注解了@Adaptive
    //除了ExtensionFactory和Compiler类型的扩展之外,其他类型的扩展都是下面动态创建的的实现
    getExtensionClasses();
    //加载完所有的实现之后,发现有cachedAdaptiveClass不为空
    //也就是说当前获取的自适应实现类是AdaptiveExtensionFactory或者是AdaptiveCompiler,就直接返回,这两个类是特殊用处的,不用代码生成,而是现成的代码
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    //没有找到Adaptive类型的实现,动态创建一个
    //比如Protocol的实现类,没有任何一个实现是用@Adaptive来注解的,只有Protocol接口的方法是有注解的
    //这时候就需要来动态的生成了,也就是生成Protocol$Adaptive
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

第四步,getExtensionClasses()加载所有的扩展类(注意:这里加载的是类,不是初始化类的实例):

private Map<String, Class<?>> getExtensionClasses() {
    //从缓存中获取,cachedClasses也是一个Holder,Holder这里持有的是一个Map,key是扩展点实现名,value是扩展点实现类
    //这里会存放当前扩展点类型的所有的扩展点的实现类
    //这里以Protocol为例,就是会存放Protocol的所有实现类
    //比如key为dubbo,value为com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
    //cachedClasses扩展点实现名称对应的实现类
    Map<String, Class<?>> classes = cachedClasses.get();
    //如果为null,说明没有被加载过,就会进行加载,而且加载就只会进行这一次
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                //如果没有加载过Extension的实现,进行扫描加载,完成后缓存起来
                //每个扩展点,其实现的加载只会这执行一次
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

loadExtensionClasses()方法,该函数的作用就是扫描classpath底下的配置文件,加载该interface对应的所有的扩展点,并将扩展点进行分类(Adaptive,Activate),以及生成包装类等。在扫描的过程中,如果发现该扩展类为Adaptive类型,则将该class缓存到cachedAdaptiveClass中。如果所有的扩展类均不是Adaptive类型,则调用createAdaptiveExtensionClass生成一个Adaptive类型的扩展类。

private Map<String, Class<?>> loadExtensionClasses() {
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        //当前Extension的默认实现名字
        //比如说Protocol接口,注解是@SPI("dubbo")
        //这里dubbo就是默认的值
        String value = defaultAnnotation.value();
        //只能有一个默认的名字,如果多了,谁也不知道该用哪一个实现了。
        if(value != null && (value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if(names.length > 1) {
                throw new IllegalStateException();
            }
            //默认的名字保存起来
            if(names.length == 1) cachedDefaultName = names[0];
        }
    }

    //下面就开始从配置文件中加载扩展实现类
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    //从META-INF/dubbo/internal目录下加载
    loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    //从META-INF/dubbo/目录下加载
    loadFile(extensionClasses, DUBBO_DIRECTORY);
    //从META-INF/services/下加载
    loadFile(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

从各个位置的配置文件中加载实现类,对于Protocol来说加载的文件是以com.alibaba.dubbo.rpc.Protocol为名称的文件,文件的内容是:

registry=com.alibaba.dubbo.registry.integration.RegistryProtocol

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

com.alibaba.dubbo.rpc.protocol.http.HttpProtocol

injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol

memcached=memcom.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol

redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol

rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol

thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol

com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol

loadFile()这个函数非常长,它会加载Class类。根据不同的类型加载到不同的缓存,大概的处理流程是对配置文件中的各个扩展点进行如下操作 :

  1. 判断该扩展点是否是要加载的interface的子类,如果不是则忽略
  2. 如果该class带有Adaptive的注解,则缓存到cachedAdaptiveClass中
  3. 如果该class具有拷贝构造函数,则缓存到cachedWrapperClasses中
  4. 如果该class带有Activate注解,则缓存到cachedActivates中
  5. 将所有的扩展点缓存到cachedClasses中

第五步,createAdaptiveExtensionClass() ,动态创建自适应扩展点Protocol$Adaptive。

    //创建一个适配器扩展点。(创建一个动态的字节码文件)
    private Class<?> createAdaptiveExtensionClass() {
        //生成字节码代码
        String code = createAdaptiveExtensionClassCode();
        //获得类加载器
        ClassLoader classLoader = findClassLoader();
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        //动态编译字节码(默认情况下使用的是javassist)
        return compiler.compile(code, classLoader);
    }

第六步,创建完Protocol$Adaptive后,injectExtension()自动注入到容器。


Protocol$Adaptive的主要功能 :
1、 从url或扩展接口获取扩展接口实现类的名称
2、根据名称,获取实现类ExtensionLoader.getExtensionLoader(扩展接口类).getExtension(扩展接口实现类名称),然后调用实现类的方法。
需要明白一点dubbo的内部传参基本上都是基于Url来实现的,也就是说Dubbo是基于URL驱动的技术。所以,适配器类的目的是在运行期获取扩展的真正实现来调用,解耦接口和实现。

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

推荐阅读更多精彩内容