Dubbo设计之ExtensionLoader

导读

  • 想要搞懂Dubbo底层实现,ExtensionLoader是不可绕过得门槛,不能深刻理解其扩展点设计,源码阅读部分会很懵逼!!!(当然,即使扩展点懂了,源码也不一定能看懂,哈哈。玩笑话,意思就是Dubbo得源码还是比较难读的,因为有很多概念与设计如果不弄清楚,基本上会被绕晕。)
  • 关键字Dubbo 扩展点设计(SPI)、ExtensionLoader

Dubbo 扩展点定义

  • 从 ExtensionLoader的源码中,我们可以找到 Dubbo SPI加载的目录有三个:1. META-INF/services/ (标准的SPI路径)2. META-INF/dubbo/ 3. META-INF/dubbo/internal/ (内部实现)
  • 扩展点文件定义格式:目录 / 接口全路径
  • 文件内容(常用格式)
       registry=com.apache.dubbo.registry.integration.RegistryProtocol
       dubbo=com.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
       filter=com.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
       listener=com.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
    
       # 没有等号情况
       com.apache.dubbo.registry.integration.XxxProtocol
       com.apache.dubbo.registry.integration.YyyProtocol
    
       # 等号前面有多个值得情况
       zzz,default=com.apache.dubbo.registry.integration.CustomProtocol
    
    

以下解读,作者根据具体的某个扩展点去分析加载过程

Ⅰ. ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtion()
    1. 先判断出当前 type(Protocol.class) 是否有扩展类加载器,没有就创建一个
    1. 然后从缓存的 cachedAdaptiveInstance 适配实例Holder中查找,没有的话就创建一个适配类
      ExtensionLoader#getExtensionLoader
    1. 查找适配类:加载当前扩展点对应的全部扩展实现, META-INF/dubbo/ , META-INF/dubbo/internal/,META-INF/services/几个路径下,扩展点上一定要有@SPI 注解,并且注解的值只能有一个(默认扩展点
    1. 如果扩展实现上 带有 @Adaptive 注解,则当作默认的适配类扩展(只能定义一个扩展适配类实现)
    1. 否则尝试获取包装类扩展(带有扩展点接口的构造器),如果有则添加到 wrappers 集合中。

      此处可以得知(适配类即使是包装类的约束格式,也不会被当作包装类)

    1. 如果没有包装类,则直接获取无参构造器(此处目的是确保扩展实现可以被实例化)
    1. 判断 = 号前面的扩展实现名称,若为空(等号前面没有值),则取扩展实现类除去后缀的部分并且全部小写(例如:XxxProtocol 取 xxx,YyyProtocol 取 yyy)作为名称name
    1. 再将 name 根据逗号拆分,若扩展实现类上有 @Activate 激活注解,则取name[0]作为key,注解作为value,放入cachedActivates 缓存map中。然后边遍历 name ,依次添加到 cachedNames(key = 扩展实现class实例,value = name)中
    1. 最后都放入到 当前类型对应得扩展实现中 cachedClasses
    1. 如果缓存得适配类cachedAdaptiveClass 为空(表示 扩展实现类上有标注 @Adaptive),则需要创建适配类( Protocol 扩展点没有适配类 )
      ExtensionLoader#getAdaptiveExtension

      ExtensionLoader#createAdaptiveExtension
    1. 获取扩展点Protocol 所有方法,判断方法上是否存在 @Adaptive 注解,若不存在则直接抛异常,否则生成动态得适配类。如下:
package com.apache.dubbo.rpc;
import com.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements com.apache.dubbo.rpc.Protocol {
    public void destroy()
    {
        throw new UnsupportedOperationException(
            "method public abstract void com.apache.dubbo.rpc.Protocol.destroy() of interface com.apache.dubbo.rpc.Protocol is not adaptive method!"
        );
    }
    public int getDefaultPort()
    {
        throw new UnsupportedOperationException(
            "method public abstract int com.apache.dubbo.rpc.Protocol.getDefaultPort() of interface com.apache.dubbo.rpc.Protocol is not adaptive method!"
        );
    }
    public com.apache.dubbo.rpc.Exporter  export(com.apache.dubbo.rpc.Invoker arg0) throws com.apache.dubbo.rpc.RpcException
    {
        if(arg0 == null) throw new IllegalArgumentException("com.apache.dubbo.rpc.Invoker argument == null");
        if(arg0.getUrl() == null) throw new IllegalArgumentException(
            "com.apache.dubbo.rpc.Invoker argument getUrl() == null");
        com.apache.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.apache.dubbo.rpc.Protocol) name from url(" + url.toString() +
            ") use keys([protocol])");
        
        // 获取url中指定得协议,默认是 dubbo协议
        com.apache.dubbo.rpc.Protocol extension = (com.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
            com.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        
        return extension.export(arg0); // 调用真正得协议扩展实现得export方法
    }

    public com.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.apache.dubbo.common.URL arg1) throws com.apache.dubbo.rpc.RpcException{
            if(arg1 == null) throw new IllegalArgumentException("url == null");
            com.apache.dubbo.common.URL url = arg1;
            
            // 获取url中指定得协议,默认是 dubbo协议
            String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
            if(extName == null) throw new IllegalStateException(
                "Fail to get extension(com.apache.dubbo.rpc.Protocol) name from url(" + url.toString() +
                ") use keys([protocol])");
        
            // 获取真实得协议扩展实现
            com.apache.dubbo.rpc.Protocol extension = (com.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
                com.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        
            return extension.refer(arg0, arg1); // 调用真正得协议扩展实现得refer方法
        }
}  
    1. 创建完适配类之后,接着再获取 Compiler 得适配类,用于动态生成 Protocol 得适配类(其他动态生成得适配类也是用 Compiler 来生成得)。由于 Compiler存在适配类,即扩展点类加载器对应得缓存cachedAdaptiveClass不为空,直接返回。此处 cachedAdaptiveClass = com.apache.dubbo.common.compiler.support.AdaptiveCompiler
    1. 实例化并调用injectExtension 方法注入其他扩展实例(不适合框架实现得扩展适配类,因为动态生成得适配类基本上不会依赖其他扩展点):判断生成得适配类中 是否存在类似方法:1. set开头得 2. 只有一个参数类型 3. public访问修饰符,若有,则截取set 之后得首字母小姐得名称将其作为扩展名,然后通过 ExtensionFactory工厂获取此扩展名得实例,调用当前setXXX方法,叫之为IOC。由于 AdaptiveCompiler 只有一个set方法但是没有对应得 String.class类型得 defaultCompiler扩展点,所以此处不会进行注入:
      public static void setDefaultCompiler(String compiler) {
        DEFAULT_COMPILER = compiler;
    }
ExtensionLoader#injectExtension
    1. 接着调用适配扩展实现 AdaptiveCompilercompile方法,方法内会获取默认得扩展实现(此处是javassist扩展名)JavassistCompiler,然后调用其compile方法动态生成适配class实例。
Ⅱ. ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName)
上面生成得适配类中,有一步是获取真正调用扩展实现 :

com.apache.dubbo.rpc.Protocol extension = (com.apache.dubbo.rpc.Protocol)
ExtensionLoader.getExtensionLoader(com.apache.dubbo.rpc.Protocol.class).getExtension(extName);

  • 首先还是获取 扩展点Protocol对应得扩展类加载器,接着调用 getExtension 方法获取指定得扩展实现,先查询是否已经缓存了相应得扩展实例,若没查到会调用 createExtension 方法去创建当前指定得扩展实现。
  • 调用createExtension 方法时,先从当前扩展点实现class实例缓存中查找,若是没有这个扩展名,说明传入得参数有问题,会直接抛出异常。然后再从对象缓存实例cachedClasses 中查找是否已经实例化了,没找到,实例化之后放入对象缓存EXTENSION_INSTANCES中。
  • 调用 injectExtension 方法注入其他得扩展点实现(IOC),同上面Step13
  • 获取当前扩展点得所有包装类cachedWrapperClasses ,循环调用包装类得有参构造器(参数就是扩展点类型)实例化,之后也会调用injectExtension 方法为包装类也注入其他得扩展点实现,并返回最后一个包装类得实例(每个Wrapper类都会包装一个之前得扩展实现 即: A -> B(A) -> C(B), 最后返回 C这个包装类)(AOP)。而当前扩展点 Protocol 有两个包装类实现:ProtocolFilterWrapperProtocolListenerWrapper

    所以,当获取适配扩展实现时:

    1. 若是动态生成得,则
      getAdaptiveExtion() -> getExtension("dubbo或者是 url.getProtocol()的值")
      -> injectExtension(T instance) (真正执行调用的扩展实现)
      -> injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance))
      ( A -> B(A) -> C(B) 最后返回 C这个包装类)
    2. 若是定义了适配类实现,则直接走适配类的逻辑
ExtensionLoader#createExtension
Ⅲ. ExtensionLoader#addExtension(String name, Class<?> clazz)
  • 首先加载所有的扩展类实现,若 clazz 没有实现 type扩展点,抛出异常。 (必须要实现扩展点,才可以添加)
  • clazz 是个接口,抛出异常。(扩展实现不能是接口,否则不能实例化)
  • clazz上没有带有 @Adaptive注解
    • 若 扩展名name 为空,抛出异常。 (扩展名不能为空)
    • 若 缓存扩展实现cachedClasses中已经存在当前扩展名,抛出异常。(扩展名不能重复)
    • 若以上两个条件都不满足,则将扩展名与扩展点建立映射关系 , 缓存到cachedNamescachedClasses中。
  • clazz上带有@Adaptive注解
    • 若适配类cachedAdaptiveClass 不为空,抛出异常。(扩展点的适配类只能有一个)
    • 为空的话,将当前 clazz作为扩展点的适配类,赋值给 cachedAdaptiveClass

    添加扩展实现的逻辑还是比较简单的。通过API的方式动态添加扩展实现,可以不通过配置文件的方式(挺实用的)

ExtensionLoader#addExtension
Ⅳ ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(url, key, group) :
  • 找到 ExtensionLoader的 getActivateExtension方法:
//  ExtensionLoader
public List<T> getActivateExtension(URL url, String key, String group = consumer) {
    // 获取 URL 中指定 key对应的值
    // 以 ProtocolFilterWrappe r的 buildInvokerChain 方法为例(任何一个扩展点都可)
    // key = REFERENCE_FILTER_KEY,group = consumer
    // 可以进行如下改造 ( Filter自定义编排化):
    // 1. URL url = invoker.getUrl().addParameter(REFERENCE_FILTER_KEY,"-default");
    // 2. URL url = invoker.getUrl().addParameter(REFERENCE_FILTER_KEY,"-cache,actives");
    // 3. URL url = invoker.getUrl().addParameter(REFERENCE_FILTER_KEY,"-default,actives");
    // 你知道上面几种扩展方式,最后返回的 Filter扩展实现有哪些么 ?
    String value = url.getParameter(key);
    // value值用于控制激活扩展点是否加载,为空表示不指定激活扩展点名称,从所有的激活扩展实现中根据 @Activate 注解跟指定的组进行筛选,若是需要指定,定义的扩展点名称需要以 "," 分隔开
    return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
}

//  ExtensionLoader
    public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<>();
        List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
        // 如果指定的扩展的名称不包括 -default(排除SPI中的实现)标识符,则从所有已经加载的激活扩展点中找到满足条件的,否则只加载指定的激活扩展点
        if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
            getExtensionClasses();
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {// 遍历激活扩展点的所有实现
                String name = entry.getKey();
                Object activate = entry.getValue();

                String[] activateGroup, activateValue;

                if (activate instanceof Activate) {
                    activateGroup = ((Activate) activate).group();
                    activateValue = ((Activate) activate).value();
                } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                } else {
                    continue;
                }
                if (isMatchGroup(group, activateGroup) // 如果当前需要加载的组在激活扩展点实现指定的组中才算匹配
                        && !names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)
                        && isActive(activateValue, url)) { // URL中至少要有 activateValue[] 中的其中一个key,当前激活扩展点实现才会被使用
                    exts.add(getExtension(name));
                }
            }
            exts.sort(ActivateComparator.COMPARATOR); // 按照自然序排序
        }
        List<T> usrs = new ArrayList<>();
        // 若names不为空,表示指定了扩展点名称,找到不是以排除标识符'-'开头并且不包括要排除(-name)的扩展点名称,获取其扩展实现
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(REMOVE_VALUE_PREFIX)
                    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
                if (DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                    usrs.add(getExtension(name));
                }
            }
        }
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }
  • 上述就是Dubbo源码中经常出现的获取扩展实现的代码,了解了其原理之后,后面我们解读源码时,会更清晰自然了。
  1. ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码获得的知识,难免会有疏忽!
  2. 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
  3. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容