dubbo源码解析之SPI扩展机制(一)

源码分析基于dubbo 2.7.1-release

看dubbo源码不得不了解dubbo spi机制,因为你常常看到如下代码,而debug又不知所踪

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

dubbo spi机制增强了Java 原生的 SPI 机制。简单来说,ExtensionLoader支持某个扩展接口存在多个扩展类(如Protocol,ProxyFactory),并通过键值对的方式进行配置,dubbo优先使用用户配置扩展类,如果用户没有配置,就使用默认扩展类。

先看看ExtensionLoader.getExtensionLoader

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    ...
    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,同样查询缓存,缓存查询失败则调用createAdaptiveExtension生成一个扩展类Extension

private T createAdaptiveExtension() {
    try {
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        ...
    }
}

injectExtension负责调用扩展类Extension的set方法,注入对应属性。
getAdaptiveExtensionClass获取扩展类的Class,再通过newInstance实例化一个对象。

查看getAdaptiveExtensionClass方法

private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {  // 检查缓存
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getExtensionClasses会检查cachedClasses,如果cachedClasses为空,它会调用loadExtensionClasses,加载用户配置的扩展类,并存放到cachedClasses中。

private Map<String, Class<?>> loadExtensionClasses() {
    ...
        Map<String, Class<?>> extensionClasses = new HashMap<>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
}

变量DUBBO_DIRECTORY的值为META-INF/dubbo/,我们自定义的扩展配置就是放到该路径下。
type.getName().replace("org.apache", "com.alibaba")是因为dubbo成为apace项目后,修改pageage。
loadDirectory会读取指定目录下的配置文件,加载class后存放到cachedClasses。
如dubbo-rpc\dubbo-rpc-dubbo\src\main\resources\META-INF\dubbo\internal\org.apache.dubbo.rpc.Protocol
内容为:

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

cachedClasses中的map中会添加dubbo:Class<DubboProtocol>的键值对。

createAdaptiveExtensionClass会生成一个通用的ExtensionClass

private Class<?> createAdaptiveExtensionClass() {
    // 生成代码字符串
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    ClassLoader classLoader = findClassLoader();
    // 编译成class
    com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    // 编译为Class
    return compiler.compile(code, classLoader);
}

AdaptiveClassCodeGenerator会拼凑生成代码字符串,并通过Compiler编译成class。

AdaptiveClassCodeGenerator这里不深入,主要是通过接口的@SPI,@Adaptive注解获取对应的默认配置。
如Protocol接口

@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    ...
}

生成的代码如下

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    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() ),这里通过invoker.url中的protocol,再通过getExtension方法找到对应的实现类。如dubbo://...registry://...分别对应DubboProtocolRegistryProtocol
默认使用dubbo协议,这个默认值正是从Protocol接口的@SPI("dubbo")注解中获取

这是一个代理类,当我们调用该类的export方法,会通过ExtensionLoader.getExtension获取真正逻辑类,并调用逻辑类export方法。

最后看看getExtension

public T getExtension(String name) {
    ...
    // 获取Holder
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    // 获取instance 
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

最后看看createExtension

private T createExtension(String name) {
    // 获取ExtensionClass
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        // 初始化ExtensionClass
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 注入属性
        injectExtension(instance);
        // 获取wrapperClasse
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && wrapperClasses.size() > 0) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}

getExtensionClasses会查询cachedClasses,前面已经将配置中的Extension信息添加到这里了。

这里要注意,如果存在wrapperClasses,会创建对应的wrapperClasses来装饰目标类。wrapperClasses就是典型的装饰模式。
如Protocol就有ProtocolListenerWrapper,ProtocolFilterWrapper,QosProtocolWrapper等装饰类。

ExtensionLoader加载扩展类时,如果一个扩展类有一个构造方法,参数是它的扩展接口,就会加入到cachedWrapperClasses中。

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

推荐阅读更多精彩内容