Dubbo SPI (service provider interface)

最近拜读dubbo 的源码,其构架设计是很精彩,基于插件式的构架方式,灵活性可见一般。此外dubbo 框架难得基 服务降级,服务路由,服务发现与注册,服务loadbalance 和 支持多种协议于一身的优秀开源框架,很是值得学习和研究细细读来的。此外整个框架的class 加载是根据统一的URL参数。各个层次结构分明,清晰,可扩展性特别好。读次源码感叹一下!

曾经我再封装一些框架的时候,总是加入一些不必要的依赖,比如封装ribbon,支持多个配置中心的时候,就会发现,会把多个配置中心的依赖都加入进去。开发支持多个校验引擎的时候,心理也是在琢磨着如何更好的切换到不同的校验引擎。Dubbo SPI 提供一种很好的思路。根据统一的URL 配置信息,通过代理类按需加载!

前提:

(1). 理解 jdk SPI 

(2). 理解AVAssist 的产生class 字节码,Dubbo 中默认使用AVAssist,主要用来生成代理类

(3). 反射和动态代理(dubbo 的自适应扩张,就是依靠动态代理)

好了,我们来看看dubbo SPI具体的特性。

(1): dubbo SPI 根据 protocol=com.XX.XXX.dubboProtocol, 的方式加载

(2): dubbo SPI 的IOC 特性,dubboProtocol类的setXXX 方法,会通过反射的方式,将相应的class instance, inject 到 dubboProtocol, 通过setXXX的方法。 后面将会有源码。

(3):dubbo SPI 的AOP 特性, ProtocolFilterWrapper 和 ProtocolListenerWrapper这两个类含有protocol 单构造器,放置在loader的私有属性cachedWrapperClasses。ProtocolFilterWrapper在服务的暴露与引用的过程中,根据key 是provider还是consumer来构建服务提供者和消费者调用过滤链。因此具有AOP 的性质。ProtocolListenerWrapper也是在服务暴露与引用的过程中调用listener链。

首先我们需要理解ExtensionLoader, ExtensionFactory这个类。ExtensionFactory 的实现类有 SpiExtensionFactory, SpringExtensionFactory. 默认会加载AdaptiveExtensionFactory (@Adaptive 的注解)实现类。

(1): SpiExtensionFactory:加载有@SPI 的注解的接口实现类。

(2):SpringExtensionFactory: 加载spring context bean 的beans。

默认的实现类AdaptiveExtensionFactory,依赖 List<ExtensionFactory> factories. 当调用 ExtensionFactory 时候,会循环SpiExtensionFactory和SpringExtensionFactory,获得 Class type 的 extension。

@SPI

public interface ExtensionFactory {

    T getExtension(Class type, String name);

}


接下来就是进入到 ExtensionLoader,SpiExtensionFactory 的 getExtension 服务,会调用ExtensionLoader的 getExtensionLoader方法。然后通过调用 getAdaptiveExtension,加载自适应点。好了,我们进入ExtensionLoader, getExtensionLoader 去看看会发生什么。

但是再这之前,我们看看加载类的配置。配置文件的路径如下:

private static final StringSERVICES_DIRECTORY ="META-INF/services/";

private static final StringDUBBO_DIRECTORY ="META-INF/dubbo/";

private static final StringDUBBO_INTERNAL_DIRECTORY =DUBBO_DIRECTORY +"internal/";

ExtensionLoader    会加载所有classpath 下,该目录的实现类。其implement 的接口都必须有@SPI的注解。


里面的每个类的结构都是 “name” = “com.XXX.XXXX.registryProtocol”的方式来实现的。首先我们来看下ExtensionLoader主要逻辑:

@SuppressWarnings("unchecked")

public static ExtensionLoadergetExtensionLoader(Class type) {

if (type ==null)

throw new IllegalArgumentException("Extension type == null");

    if (!type.isInterface()) {

throw new IllegalArgumentException("Extension type(" + type +") is not interface!");

    }

if (!withExtensionAnnotation(type)) {

throw new IllegalArgumentException("Extension type(" + type +

") is not extension, because WITHOUT @" +SPI.class.getSimpleName() +" Annotation!");

    }

ExtensionLoader loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);

    if (loader ==null) {

EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));

        loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);

    }

return loader;

}

先从EXTENSION_LOADERS cache 中取出 type 类型的 ExtensionLoader, 比如protocol。 如果没有就会new 一个ExtensionLoader. 我们来看一下new ExtensionLoader的逻辑。 

private ExtensionLoader(Class type) {

this.type = type;   

 objectFactory = (type == ExtensionFactory.class ?null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());

}

如果不是ExtensionFactory类型的,就获取一个自适应扩张类。getAdaptiveExtension()。

那么getAdaptiveExtension()是什么逻辑呢?

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);

                    }

}

}

}else {

throw new IllegalStateException("fail to create adaptive instance: " +createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);

        }

}

return (T) instance;

}

首先也是从cachedAdaptiveInstance 获取, 如果获取不到,通过调用createAdaptiveExtension()。 我们看看createAdaptiveExtension的逻辑。

private T createAdaptiveExtension() {

try {

return injectExtension((T) getAdaptiveExtensionClass().newInstance());

    }catch (Exception e) {

throw new IllegalStateException("Can not create adaptive extension " +type +", cause: " + e.getMessage(), e);

    }

}

首先是通过getAdaptiveExtensionClass,然后实列化,通过injectExtension 来注入,injectExtension就是上面提到的 IOC 特性,会通过setXXX() 方法,注入对象实列。我们先看

private Map>getExtensionClasses() {

Map> classes =cachedClasses.get();

    if (classes ==null) {

synchronized (cachedClasses) {

classes =cachedClasses.get();

            if (classes ==null) {

classes = loadExtensionClasses();

                cachedClasses.set(classes);

            }

}

}

return classes;

}

首先从cachedClasses 缓存中获取class,如果获取不到,就调用loadExtensionClasses();,加载extension classes. 

private Map>loadExtensionClasses() {

final SPI defaultAnnotation =type.getAnnotation(SPI.class);

    if (defaultAnnotation !=null) {

String value = defaultAnnotation.value();

        if ((value = value.trim()).length() >0) {

String[] names =NAME_SEPARATOR.split(value);

            if (names.length >1) {

throw new IllegalStateException("more than 1 default extension name on extension " +type.getName()

+": " + Arrays.toString(names));

            }

if (names.length ==1)cachedDefaultName = names[0];

        }

}

Map> extensionClasses =new HashMap>();

    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);

    loadDirectory(extensionClasses, DUBBO_DIRECTORY);

    loadDirectory(extensionClasses, SERVICES_DIRECTORY);

    return extensionClasses;

}

首先是设置cachedDefaultName 的默认名字, 然后去load 各个目录下的class, key = value 的形式。

Map> extensionClasses =new HashMap>();

loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);

loadDirectory(extensionClasses, DUBBO_DIRECTORY);

loadDirectory(extensionClasses, SERVICES_DIRECTORY);

然后进入loadResource 的方法,我们就明白key value 的形式是如何解析的。最后我们调用loadClass 方法,将加载好的class 放入到对应的缓存中。带有adaptive 注解的放入cachedAdaptiveClass, wrapper的class,放入到 cachedWrapperClasses中去。标记有@Active 的类注入到cachedActivates 缓存中去。

将所有的key  = value 形式加载完毕之后,我们要看看IOC 是如何作用的。

privateTinjectExtension(T instance){

    try {

        if (objectFactory != null) {

            // 遍历目标类的所有方法            for (Method method : instance.getClass().getMethods()) {

                // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public                if (method.getName().startsWith("set")

                    && method.getParameterTypes().length == 1                    && Modifier.isPublic(method.getModifiers())) {

                    // 获取 setter 方法参数类型                    Class<?> pt = method.getParameterTypes()[0];

                    try {

                        // 获取属性名,比如 setName 方法对应属性名 name                        String property = method.getName().length() > 3 ?

                            method.getName().substring(3, 4).toLowerCase() +

                            method.getName().substring(4) : "";

                        // 从 ObjectFactory 中获取依赖对象                        Object object = objectFactory.getExtension(pt, property);

                        if (object != null) {

                            // 通过反射调用 setter 方法设置依赖                            method.invoke(instance, object);

                        }

                    } catch (Exception e) {

                        logger.error("fail to inject via method...");

                    }

                }

            }

        }

    } catch (Exception e) {

        logger.error(e.getMessage(), e);

    }

    return instance;

}

通过 Object object = objectFactory.getExtension(pt, property) 和 method.invoke(instance, object) 将adaptive 的类注入的 带有setXXX方法类中。其中objectFactory就是spiExtensionFactory。 

那么有没有想过到底是如何使用 自适应扩展点的? 你看,上面通过IOC 和 spiExtensionFactory 将所有的key = value 形式的类都加载好了,存储的形式是map <key, class<?> class>. 那我们什么时候用哪个接口的实现类呢? 比如说:

package com.alibaba.dubbo.rpc; 下的 protocol。 实现类有


那应该使用哪个实现类呢? 这就是自适应扩展点。首先会产生自适应扩展点的代理,然后 在通过URL 的配置信息,传入name, 获取相应的加载类,这就是key value 的原因。 

代理类是什么样呢?代理类是通过javassit 产生的, 请看下面:

package com.alibaba.dubbo.rpc;

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

public class Protocol$Adaptive 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 com.alibaba.dubbo.rpc.RpcException {

if (arg1 == null) throw new IllegalArgumentException("url == null");

com.alibaba.dubbo.common.URL url = arg1;

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.RpcException {

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();

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);

}}

String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); 就是从 URL 获取 name, 如果获取不到,就默认使用dubbo,通过 getExtension(extName),就可以获取到 加载类: DubboProtocol

那URL 是啥样子呢:dubbo://172.27.238.135:20880/com.mastercard.api.service.DemoService?anyhost=true&application=dubbo-provider&bind.ip=172.27.238.135&bind.port=20880&default.timeout=5000&dubbo=2.6.2&generic=false&interface=com.mastercard.api.service.DemoService&methods=sayHello&pid=71712&revision=1.0.0&side=provider&timestamp=1549356336726&version=1.0.0

其实里面的很多内容很精彩,需要自己慢慢品味。后面具体分析下自适应扩展点。

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

推荐阅读更多精彩内容