Dubbo源码-SPI实现分析

JDK的SPI?

       SPI是jdk5引进的新特性,说到dubbo的spi机制,就不得不提jdk自带的SPI,SPIdubbo的spi机制借鉴了jdk的spi.本质思想上是一样的.这里只对JDK做个简单的介绍.后面会写专门的内容进行介绍

JDK的SPI的一点介绍

       SPI的全称是Services Provide Interface,简单来说就是一种服务发现机制.比较具体的定义可以查看官方文档,它的具体实现思路如下:
1.新建Spi的目标接口(例如我的接口为:com.json.blog.spi.ISpi)
2.自定义改接口的实现类.(com.json.blog.spi.SpiImpl1,com.json.blog.spi.SpiImpl2)
3.在classpath路径下新建META-INF/services文件夹.
4.在文件夹内新建文件,文件名接口的全路径为名称.(文件名:com.json.blog.spi.ISpi)
5.在文件内写入实现类的路径名+类名,多个实现类需要进行换行.(com.json.blog.spi.SpiImpl1 com.json.blog.spi.SpiImpl2).

使用场景

1.数据库驱动加载,JDBC等接口实现类的加载
2.日志面向接口实现类的加载
3.Spring等开源框架的大量使用
3.Dubbo使用其思想对其进行了改造

优点

实现了java代码的解耦

缺点

1.多个并发多线程使用ServiceLoader类的实例是不安全的.
2.虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍.
3.破坏了java的双亲委派模型.

Dubbo的SPI

Dubbo的改进

1.原本jdk只读取META-INF/services目录下,而dubbo会多读取META-INF/dubbo/和META-INF/dubbo/internal/目录下.
2.dubbo只会获取SPI注解中key对应的class,然后产生实例,不像JDK.会实例化全部的class.降低了系统负担.

Dubbo的SPI源码分析

1.程序的入口为我自己性的一个测试类.Dubbo使用ExtensionLoader实现dubbo的SPI机制的加载.ExtensionLoader是私有的构造函数.只能通过静态函数getExtensionLoader()方法获得指定的接口的ExtensionLoader的实现类.然后通过getAdaptiveExtension()方法获得对应接口自适应扩展点.或者通过getExtension("name")获得名称为name的实现类.

public class extentionLoadTest {
    public static void main(String[] args) throws IOException {
        Protocol protocol = ExtensionLoader.
                //根据一个类型<CLass>获得一个ExtentionLoading
                getExtensionLoader(Protocol.class)
                //获得一个自适应扩展点
                .getAdaptiveExtension();
        System.out.println(protocol.getDefaultPort());
    }
}

2.为了防止重复加载,对于对应接口的ExtensionLoader在EXTENSION_LOADERS进行了缓存.如果在缓存中没有获取到ExtensionLoader,则对其进行实例化.获得ExtensionLoader对象之后将其放入到EXTENSION_LOADERS缓存中.下次需要相同接口的ExtensionLoader则直接从缓存中获取.

    /**
     * 静态类,通过传入的class,返回对应的实现类
     */
    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {  //T--->protocol
       /******************************部分非空检查的代码已被删除*******************************/
        //先从静态缓存中取对应ExtensionLoader.
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            //T的类型为protocol
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

3.ExtensionLoader的构造函数

 /**
     * 为什么此函数要私有构造函数?
     *
     * 自己的一点理解:SPI的思想在dubbo中使用十分广泛,如果不对其私有化,任意的实例化,对于系统的运行可能会造成沉重的负担.当期私有之后.需要对该类.
     * 可以通过getExtensionLoader获取对应的实例.并且在其中加入的EXTENSION_LOADERS作为缓存.对每种类型的ExtensionLoader只有一个实例.防止多线程并
     * 发,代码中大量使用的双重检查锁的代码,以确保代码的健壮性.为了确保双重检查锁有效.加锁的对象应该用volatile修饰
     * @param type
     */
    private ExtensionLoader(Class<?> type) {
        this.type = type;
        //首先创建ExtensionLoader的objectFactory对象
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

到此为止,Extendloader的实例化就已经完成,现在我们开始研究很眼熟的getAdaptiveExtension().他已经出现了两次.

4.在Extendloader实例化完成之后,便调用对应的getAdaptiveExtension()方法.获取默认的实现类.在这个过程中.dubbo根据@Adaptive注解.如果某个实现类有该注解.则返回该直接实现类.如果所有的实现类均没有该注解.则查找前头getExtensionLoader(T)中T接口含有@Adaptive注解的方法生成代理方法.如果没有方法拥有注解.则直接抛出异常

 @SuppressWarnings("unchecked")
    public T getAdaptiveExtension() {
        //首先现从缓存中去取
        Object instance = cachedAdaptiveInstance.get();
        /**
         * 使用双重检查锁提高效率并且防止高并发,第一个if提高性能,第二个if确保高并发  --double check
         */
        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("Failed to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            } else {
                throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }

5.代码很简单,就是调用创建自适应类的实例然后注入属性,最后返回结果

 /**
     * 获得自适应的扩展点
     */
    private T createAdaptiveExtension() {
        try {
            //ExtensionFactory
            /**
             * injectExtension:为生成对象的内容的进行注入属性
             * getAdaptiveExtensionClass:获取到自适应的class.拥有@Adaptive的拥有非常高的优先级.直接返回该对象,
             *     没有的话,动态生成class类.然后编译返回对应结果
             * newInstance:反射生成对应的class实例
             */
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }


6.判断是否有@Adaptive注解过得类,如果有则直接返回实例class信息.如果没有则调用createAdaptiveExtensionClass()方法为该接口创建动态实现类

 private Class<?> getAdaptiveExtensionClass() {
        //T--->ExtensiveFactory/Protocol
        getExtensionClasses();
        //判断spi注解的实现类中有没有被@adaptive注解过.有的话直接返回.否则通过javasisst操作字节码进行动态创建的实现类
        //cachedAdaptiveClass是在getExtensionClasses()方法中查找到被@Adaptive过得类,而且所有的实现类中只能有一个类被
        //@Adaptive注解,否则也会报错
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

7.没什么说的,又是double check,可见在自己代码中可以为了提高代码质量,也可以使用这个思想

//加载扩展点的实现类
    private Map<String, Class<?>> getExtensionClasses() {
        //cachedAdaptiveClass怎么来的?
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    //加载系统所有的接口实现类
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

8.jdk的SPI只是读取META-INF/Services文件下的内容,而Dubbo则加载多个路径下的内容.这是dubbo做的扩展之一.

  /**
     * 加载接口所有的实现类
     * @return
     */
    private Map<String, Class<?>> loadExtensionClasses() {
        //cachedAdaptiveClass怎么来的?
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();
        //META-INF/dubbo/internal/
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
        //META-INF/dubbo/internal/(由于该版本的代码alibaba捐献给apache孵化,全量替换,容错)
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        //META-INF/dubbo/
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
        //META-INF/dubbo/(由于该版本的代码alibaba捐献给apache孵化,全量替换,容错)
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        //META-INF/services/
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
        //META-INF/services/(由于该版本的代码alibaba捐献给apache孵化,全量替换,容错)
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        //返回所有加载到的class
        return extensionClasses;
    }

9.上面虽然调用6次loadDirectory()方法,但只是路径不同.但是每个方法内容是一样的.

 /**
     *
     * @param extensionClasses
     * @param dir  META-INF/services/dubbo META-INF/dubbo/internal   META-INF/service
     * @param type  classname
     */
    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
        String fileName = dir + type;
        try {
            Enumeration<java.net.URL> urls;
            //获取类加载器
            ClassLoader classLoader = findClassLoader();
            //获取到完成的文件的文件路径
            if (classLoader != null) {
                //获取的文件全路径
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
            if (urls != null) {
                //遍历加载每个文件对应的class
                while (urls.hasMoreElements()) {
                    java.net.URL resourceURL = urls.nextElement();
                    //加载每个文件的实现
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

10.主要是对获取到的不同路径接口文件,然后解析接口文件里的内容,之后又调用loadClass()方法.加载该类.

 /**
     *
     * @param extensionClasses SPI注解实现类的类名
     * @param classLoader 类加载器
     * @param resourceURL 文件的全路径
     */
    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
        try {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                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;
                            //采用key=value方式进行分割
                            int i = line.indexOf('=');
                            if (i > 0) {
                                name = line.substring(0, i).trim();//SPI扩展中的key
                                line = line.substring(i + 1).trim();//SPI扩展中的value
                            }
                            if (line.length() > 0) {
                                //加载对应的class
                                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);
                        }
                    }
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", class file: " + resourceURL + ") in " + resourceURL, t);
        }
    }

11.加载目录下接口文件中定义的class文件.并且对其做校验

/**
     *
     * @param extensionClasses
     * @param resourceURL
     * @param clazz
     * @param name SPI对应的文件中的key
     * @throws NoSuchMethodException
     */
    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 occurred when loading extension class (interface: " +
                    type + ", class line: " + clazz.getName() + "), class "
                    + clazz.getName() + " is not subtype of interface.");
        }
        //判断是否有@Adaptive注解
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            //有的话为cacheAdaptiveClass赋值.表示该Type有自定义的适配器类.不需要动态创建,直接返回该类
            //为cachedAdaptiveClass赋值.如果有重复的值会进行报错处理.
            cacheAdaptiveClass(clazz);
            //判断构造参数是否合法
        } else if (isWrapperClass(clazz)) {
            cacheWrapperClass(clazz);
        } else {
            //动态创建对应的适配器
            clazz.getConstructor();
            //如果SPI文件的中的格式key为空,可以通过Extension为其赋值key,目前来看,这种情况很少
            //我们可以不在文件中用key=value格式定义.直接写入类的全路径,然后在类中给Extension注解,赋值名称,效果相同
            if (StringUtils.isEmpty(name)) {
                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 (ArrayUtils.isNotEmpty(names)) {
                cacheActivateClass(clazz, names[0]);
                for (String n : names) {
                    cacheName(clazz, n);
                    saveInExtensionClass(extensionClasses, clazz, n);
                }
            }
        }
    }
PS:代码只是跟随主要的流程分支,部分分支可能被忽略.

12.文件的加载已经完成,此时在返回getAdaptiveExtensionClass()方法.如果cachedAdaptiveClass为空.则需要对接口中@adaptive注解的方法动态生成适配器.

/**
     * 基于动态字节码创建适配器扩展点
     * @return
     */
    private Class<?> createAdaptiveExtensionClass() {
        //生成的适配器扩展点代码
        String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
        //获得类加载器
        ClassLoader classLoader = findClassLoader();
        //编译对应的代码
        org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        //返回编译后的class
        return compiler.compile(code, classLoader);
    }

13.此处只分析generate()方法.个人感觉虽然代码重构后整体看的比较舒服,但是真正读起来对细节把握容易错乱.

/**
     * generate and return class code
     */
    public String generate() {
        // no need to generate adaptive class since there's no adaptive method found.
        if (!hasAdaptiveMethod()) {
            throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
        }

        StringBuilder code = new StringBuilder();
        code.append(generatePackageInfo());
        code.append(generateImports());
        code.append(generateClassDeclaration());

        Method[] methods = type.getMethods();
        for (Method method : methods) {
            code.append(generateMethod(method));
        }
        code.append("}");

        if (logger.isDebugEnabled()) {
            logger.debug(code.toString());
        }
        return code.toString();
    }

整体而言,所有的方法都是为了拼出一个该接口的适配器代码.大部分比较简单.后文只对generateMethodContent()进行分析.私以为这是最核心的部分.

14.生成被@Adaptive注解的方法

/**
     * generate method content
     */
    /**
     * 生成代码的逻辑:
     * 1.判断是否有Adaptive方法,如果没有,则直接抛出异常,表示拒绝创建该适配器代码;
     * 2.创建package,import,class代码;
     * 3.遍历该接口所有的方法,为其生成代码
     * 4.如果方法没有被Adaptive注解,抛出UnsupportedOperationException异常,该方法生成结束,
     *   被Adaptive注解的方法执行第5步
     * 5.获取到方法的参数,如果参数是URL,则继续执行第7步,否则执行第8步
     * 6.如果不是URl,从参数中尝试获取URL.获取到,则继续向下执行,否者代码抛出异常.流程退出
     * 7.获取到URL后.尝试获取URL,如果获取到,则继续向后执行,否则生成的代码中抛出异常.
     * 8.获去到Adaptive注解中的参数,如果没有参数,则直接使用接口名称(转为小写)
     * 9.判断是否Invocation参数.
     * 10.根据adaptive注解的参数和Invocation形参,确定动态扩展点的key值来源,
     *    分别有url.getMethodParameter()/url.getParameter()/url.getProtocol()
     * 11.生成extendloader代码加载
     * 12.生成返回值
     *
     *
     * @param method
     * @return
     */
    private String generateMethodContent(Method method) {
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        if (adaptiveAnnotation == null) {
            return generateUnsupported(method);
        } else {
            /**
             * 此版本已经对方法进行了封装,不像alibaba版本易懂
             * 生成的<br>代码</br>的逻辑
             * 尝试从参数中中获得URL的对象,如果参数值URL那么完美,直接使用.否则反射遍历参数的所有方法.判断是否有getUrl方法.有的话生
             * 成XXX.getURl();获取到url.如果获取不到.反射报错
             */
            int urlTypeIndex = getUrlTypeIndex(method);

            // found parameter in URL type
            //urlTypeIndex!=-1表示形参为URL,否则尝试获取参数中的URL
            if (urlTypeIndex != -1) {
                // Null Point check
                //生成非空检查的代码
                code.append(generateUrlNullCheck(urlTypeIndex));
            } else {
                // did not find parameter in URL type
                //尝试从参数中获取URL
                code.append(generateUrlAssignmentIndirectly(method));
            }

            //后去adaptive注解的参数
            String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

            //判断是参数是不时Invocation
            boolean hasInvocation = hasInvocationArgument(method);

            //如果有Invocation参数,生非空检验
            code.append(generateInvocationArgumentNullCheck(method));
            //根据adaptive注解的参数和Invocation形参,确定动态扩展点的key值来源,分别有url.getMethodParameter()/url.getParameter()/url.getProtocol()
            code.append(generateExtNameAssignment(value, hasInvocation));
            // 生成extName非空校验
            code.append(generateExtNameNullCheck(value));
            //生成extendloader代码加载
            code.append(generateExtensionAssignment());

            // return statement
            code.append(generateReturnAndInvocation(method));
        }
        //返回生成的代码
        return code.toString();
    }

此时代码已经成,返回后通过jdk的编译器,编译后返回字节码.目前已经生成的扩展类已经进入尾声.再次返回createAdaptiveExtension()方法,将返回的字节码通过injectExtension()方法为生成的代理类的参数进行注入参数如.此时动态生成扩展点结束.

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

推荐阅读更多精彩内容