Dubbo SPI(一)

ExtensionLoader

这是构成dubbo spi内核的主要类,因此是阅读dubbo源码必须要先了解的类。

getExtensionLoader

ExtensionLoader的构造方法是私有的,唯一得到实例的方法就是这个静态方法。它确保了type是接口,并且通过withExtensionAnnotation方法确保接口上有SPI注解,然后构造实例对象,并放入EXTENSION_LOADERS静态域缓存。

getExtensionClasses

private Map<String, Class<?>> getExtensionClasses() {
    //先从类实例缓存中加载
    Map<String, Class<?>> classes = cachedClasses.get();
    //经典的单例写法
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                //加载扩展类并放入缓存
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

这是ExtensionLoader中一个无参的私有方法,用来加载spi,可以看到在类中的很多方法都会先调用该方法。为什么不在构造方法中直接调用该方法呢?我觉得可能的原因是在真正使用时加载指定接口的扩展,以节约资源。

Holder

注意上面的cachedClasses,他的类型是dubbo的Holder类。

public class Holder<T> {
    //volatile保证值多线程可见
    private volatile T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }

}

对于这个类的用途,不是很理解,并且和AtomicReference感觉有点相似。
偶然有一天看到一个场景,可能是其用途所在,仅个人理解。代码如下:

public void test(boolean flag) throws Exception {
    Thread.sleep(5000);
    if (flag) {
        System.out.println("now is true");
    }
}

flag作为方法的入参,初始值肯定是准确的,但是在线程暂停的5秒内,很有可能外部实际的值已经改变了,但是方法内部判断的依旧是旧值,此时就出现了错误的结果。如果用Holder或者AtomicBoolean包起来,那就可以得到当前的准确值。

  • 注意以上对Holder的理解是错误的,查看Holder的实例是如何使用的就可发现,其目的是在加锁时有一个局部锁!!

loadExtensionClasses

如果在缓存中没有拿到扩展类,那么就会调用该方法,加载所有的扩展类。dubbo指定了三个资源目录,分别为:META-INF/services/;META-INF/dubbo/;META-INF/dubbo/internal/。

private Map<String, Class<?>> loadExtensionClasses() {
    //得到spi注解
    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
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    //加载指定的三个目录的文件名是接口全类名的文件
    //这里replace应该是dubbo加入apache项目后做的适配
    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;
}

loadDirectory

加载指定目录的扩展类。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
    //目录+接口全类名
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls;
        //得到ExtensionLoader的类加载器
        ClassLoader classLoader = findClassLoader();
        if (classLoader != null) {
            //不是bootstrap加载器,直接使用类加载器加载资源
            urls = classLoader.getResources(fileName);
        } else {
            //是bootstrap加载器,调用系统加载器
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            //加载找到的所有的资源文件
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class(interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

loadResource

加载具体的某个资源文件。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            //按行读取文件内容
            while ((line = reader.readLine()) != null) {
                //去掉#之后的注释
                final int ci = line.indexOf('#');
                if (ci >= 0) line = line.substring(0, ci);
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        //根据=得到名字和扩展类的全类名
                        int i = line.indexOf('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            //处理该加载类
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        //缓存加载某个类出现的异常
                        IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class(interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

这里将加载某个扩展类出现的异常缓存了起来,不直接抛出保证了可以继续加载其他扩展类,然后在使用该扩展时从缓存中拿到该异常报错。

loadClass

加载资源文件内的指定扩展类。

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    //判断是否是接口的实现类
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error when load extension class(interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + "is not subtype of interface.");
    }
    //判断接口上是否有Adaptive注解,该注解表示该扩展类是适配类
    //然后放入cachedAdaptiveClass缓存,如果有多个报异常
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("More than 1 adaptive class found: "
                    + cachedAdaptiveClass.getClass().getName()
                    + ", " + clazz.getClass().getName());
        }
        //判断是否是包装扩展类,是就放入cachedWrapperClasses缓存
        //判断是否有该接口类型入参的构造方法,可以查看isWrapperClass方法
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    } else {
        //确保有无参构造方法
        clazz.getConstructor();
        //如果没有用=指定名字就先通过Extension注解拿(不过貌似已经不推荐了)
        //再通过类名拿(类名后缀是接口类名的话会去除)
        if (name == null || name.length() == 0) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }
        //名字支持,分隔
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            //Activate注解表明其是激活扩展,以后会讲到这个注解的作用
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                //缓存激活扩展类
                cachedActivates.put(names[0], activate);
            } else {
                // support com.alibaba.dubbo.common.extension.Activate
                com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
                if (oldActivate != null) {
                    cachedActivates.put(names[0], oldActivate);
                }
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    cachedNames.put(clazz, n);
                }
                //放入缓存
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    extensionClasses.put(n, clazz);
                    //名字重复异常
                } else if (c != clazz) {
                    throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                }
            }
        }
    }
}

本文总结

SPI

接口上必须要有该注解,该注解的值指定了默认的扩展实现的名字。

dubbo加载目录

  1. META-INF/services/
  2. META-INF/dubbo/
  3. META-INF/dubbo/internal/

dubbo类名来源

  1. 资源文件中通过=指定,可以通过,分隔(,两边支持空格)
  2. 通过Extension注解指定(Deprecated)
  3. 通过类名拿到(截去接口类名)

之前我们看到exceptions缓存了加载某个扩展类出现的异常,缓存的键是文件的一行(去掉了注释),后续是通过扩展类名去拿缓存的,所以第二种方式显然不能匹配得到。

扩展类分类

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

推荐阅读更多精彩内容