Java中的SPI

SPI,Service Provider Interface,java中提供的一种使程序可扩展的方式,系统定义好接口规范,供其他服务提供方实现,服务提供方将自己jar包META-INF/services下新建一个以接口全名称定义的文件,里面内容写上自己服务的实现的类名,每一行代表一个实现,服务使用方可以通过ServiceLoader.load加载所有的服务,然后判断可以使用的服务。具体可以参考reference

dubbo中的SPI

dubbo也使用了类似Java中的SPI机制,不过服务发现等都是dubbo自己实现的。

dubbo中的ServiceLoader:com.alibaba.dubbo.common.extension.ExtensionLoader

dubbo中的扩展声明配置文件的位置:

private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

dubbo中的扩展声明配置文件的内容(比如com.alibaba.dubbo.rpc.Protocol):

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol

文件里面都是name=value的格式,name就是该扩展的名称,value是实现的扩展类的全限定名城,以#开始的都是注释

注解

dubbo为了支持可扩展的SPI机制,有三个相关重要的注解

/**
* 写在可扩展的接口上面,表明该接口是支持SPI扩展的,有一个属性value表示默认的扩展类名称(就是配置文件中的name)
* 比如:Protocol上的@SPI("dubbo"),说明默认的Protocol的扩展是name为dubbo的扩展,
* 查找配置文件META_INFO/dubbo.internal/com.alibaba.dubbo.rpc.Protocol
* 对应的扩展类是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
*/
com.alibaba.dubbo.common.extension.SPI /**
* 写在类上面表示该类是默认的adapter,写在方法上表示该方法需要被adapt,
* 属性value表示需要被adapt成的扩展的key(在url中配 置的key)
* 比如com.alibaba.dubbo.common.threadpool.ThreadPool#getExecutor方法上面的注解:@Adaptive({Constants.THREADPOOL_KEY})
* 表示该方法返回的Executor是url中key是threadpool指定的扩展,如果url中没有配置则使用默认的扩展
*/
com.alibaba.dubbo.common.extension.Adaptive /**
* 指定默认激活的扩展,属性group和value用来过滤(判断什么时候激活)
* 比如:com.alibaba.dubbo.rpc.filter.ExceptionFilter上的@Activate(group = Constants.PROVIDER)
* 该filter只有在provider一侧的时候才会被激活,也就是服务提供方的时候才会使用该filter
*/
com.alibaba.dubbo.common.extension.Activate

获取扩展

dubbo获取扩展的方式都是先获取extensionLoader,然后通过loader去加载对应的扩展

获取对应的extensionLoader的方法,每个扩展接口都有自己的extensionLoader实例,获取之后缓存在EXTENSION_LOADERS中

com.alibaba.dubbo.common.extension.ExtensionLoader#getExtensionLoader

获取扩展的方法主要有:

// 获取适配后的扩展
com.alibaba.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension
// 获取active的扩展
com.alibaba.dubbo.common.extension.ExtensionLoader#getActivateExtension
// 获取默认配置的扩展
com.alibaba.dubbo.common.extension.ExtensionLoader#getDefaultExtension
// 根据name(配置文件中的key)获取扩展
com.alibaba.dubbo.common.extension.ExtensionLoader#getExtension

加载扩展的调用路径

com.alibaba.dubbo.common.extension.ExtensionLoader#getExtensionClasses
com.alibaba.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
com.alibaba.dubbo.common.extension.ExtensionLoader#loadFile

getAdaptiveExtension

获取指定扩展接口的adaptive类,可以是动态生成的,也可以使用注解@Adaptivate指定

主要步骤
  1. 先尝试从缓存中get,没有再去load
  2. load的时候,先判断是否有SPI注解,如果有注解判断value值配置是否正确(因为value指定的是默认扩展,不允许指定多个默认值),如果正确的话设置字段cachedDefaultName为配置的默认扩展
  3. 然后从指定的三个位置(就是上面配置文件的三个位置)查找并加载所有的扩展
  4. 主要的load工作由loadFile完成
  5. 找到class之后,调用newInstance实例化
  6. 判断该类是否有类字段是其他扩展,如果有则从所有的扩展中查找到对应的扩展并注入到该实例中
  7. 如果第4步没有找到配置了的adaptiveClass,需要动态生成一个adaptiveClass,调用的是createAdaptiveExtensionClassCode方法生成动态代码
  8. 获取classLoader,默认是加载ExtensionLoader的类加载器,用来加载动态编译后的class
  9. 获取com.alibaba.dubbo.common.compiler.Compiler.class的AdaptiveExtension,用来编译动态生成的代码
  10. 返回进过类加载器加载的动态编译后的Class
loadFile的主要过程
  1. 查找classLoader,默认使用ExtensionLoader对应的类加载器
  2. 使用classLoader查找指定目录下所有以扩展接口全限定名命名的配置文件,如果找到了继续,如果没有找到直接返回
  3. 循环加载每个查找到的配置文件
  4. 针对每个配置文件,读入每一行配置,忽略空行和注释("#")
  5. 如果类上面有Adaptive注解,设置为cachedAdaptiveClass
  6. 判断该扩展类是否有包含扩展接口类型的构造方法,如果有,将该class加入到cachedWrapperClasses,将来会使用wrapper包裹其他
  7. 如果没有上面这样的构造方法,判断该类是否有Activate注解,如果有则加入cachedActivates
  8. 并将配置的该class添加到extensionClasses
createAdaptiveExtensionClassCode生成动态代码

该方法会生成一个实现了扩展接口的adaptive类,该类实现了接口的所有方法,对于没有Adaptive注解的方法注解抛出UnsupportedOperationException,如果有该注解会从url中找到需要适配到的扩展,然后调用适配到的扩展的对应的方法。比如ProxyFactory接口的getProxy方法,默认会去url中找key为proxy对应的value,如果没有配置proxy的话,默认的value是javassist,然后根据该value加载对应的扩展,默认扩展JavassistProxyFactory,然后调用JavassistProxyFactory#getProxy

  1. 判断给定扩展接口是否有被Adaptive注解的方法,如果没有就报错,如果有则继续
  2. 动态拼接包名、import等代码
  3. 循环每一个方法进行进行适配adapt
  4. 如果没有Adaptive注解,则在适配方法内部抛出UnsupportedOperationException
  5. 如果有Adaptive注解,主要找到需要使用哪一个扩展来适配,下面的代码主要在找这个扩展的key
  6. 先判断该方法的入参有没有URL类型的参数,如果有则使该url,
  7. 如果没有URL类型的参数,判断该方法的参数有没有get方法的返回值是URL,如果没有则报错
  8. 如果有返回值是URL类型入参,做一些NPE的校验,使用get方法获取url
  9. 上面已经获取到url,接下来是从url中获取配置该类扩展的名称(配置文件中name=value中的name),需要先知道url中对应的key
  10. 如果adaptive注解上配置了value属性则直接使用作为key
  11. 如果没有配置,则使用扩展点接口名的点分隔作为key
  12. 找到key之后就是在url中找到key对应的value,分以下几种情况:
    • key是protocol,如果有默认扩展名称配置cachedDefaultName并且url.getProtocol()为空则使用cachedDefaultName,否则直接通过url.getProtocol()获取对应的扩展的name
    • key不是protocol,如果参数中没有Invocation则直接冲url中获取;如果有Invocation需要获取方法级的配置(因为比如loadbalance,如果方法级有单独的配置,需要按照方法级的配置获取扩展)
    • 注意:只有一个配置的时候才会考虑默认配置cachedDefaultName,如果有多个配置,以最后一个获取到的配置为准
  13. 取到扩展的name之后,调用ExtensionLoader#getExtension获取扩展
  14. 调用扩展的对应方法

下面是动态生成的接口com.alibaba.dubbo.rpc.ProxyFactory的适配类

import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class ProxyFactory$Adpative implements com.alibaba.dubbo.rpc.ProxyFactory {
    public com.alibaba.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, com.alibaba.dubbo.common
        .URL arg2) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg2 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg2;
        String extName = url.getParameter("proxy", "javassist");
        if (extName == null) throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" + url.toString() +
                ") use keys([proxy])");
        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(
            com.alibaba.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getInvoker(arg0, arg1, arg2);
    }
    public java.lang.Object getProxy(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.getParameter("proxy", "javassist");
        if (extName == null) throw new IllegalStateException(
                "Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" + url.toString() +
                ") use keys([proxy])");
        com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(
            com.alibaba.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getProxy(arg0);
    }
}

getActivateExtension

该方法主要获取两种扩展:

  1. 符合条件的配置了@Activate注解的扩展,从cachedActivates中查找
  2. 指定name的扩展

找到的所有扩展是有顺序的,如果用户有配置按照用户配置的先后顺序(默认activate的扩展的排序规则ActivateComparator)

从cachedActivates中查找的主要步骤是:

  1. getExtensionClasses加载所有的扩展,其中会将类上面有Activate的class加入cachedActivates
  2. 如果配置了"-default",表示不使用默认有Activate注解扩展,如果没有该配置,冲cachedActivates中查找合适的扩展眼,根据以下条件过滤默认activate的扩展,下面条件都符合才会添加到最终的返回值中
    • 根据注解上配置的group是否和指定的一致
    • url中配置不包含该扩展,url中也不包含去除该扩展name的配置,Activate注解配置了value并且url中的配置包含该value
  3. 将从cachedActivates中找到的扩展排序

查找指定name扩展的步骤是:

  1. 配置的name不是用来排除扩展的,并且没有没有排除该扩展的配置
  2. 如果指定的配置中包含默认配置的"default",将default前面的配置扩展放在list的前面,default后面的配置正常放在list的后面,保证按照用户配置的顺序获取对应的扩展

getDefaultExtension

获取默认的扩展,也是先加载该接口所有的扩展,这个过程中会将SPI注解配置了value——也就是默认的使用的扩展,赋值给cachedDefaultName,然后调用getExtension加载该nam对应的扩展

getExtension

根据给定的name来获取扩展的class,返回对应的实例对象

  1. 对name判空
  2. 如果name是true则getDefaultExtension获取默认扩展
  3. 尝试从缓存中获取holder,如果没有则先添加holder
  4. double check holder中是否有对应的实例对象,没有的话就调用createExtension创建,创建成功之后set到holder中

createExtension

根据名称获取扩展

  1. 加载该接口对应的所有扩展,查找该name对应扩展的class,如果没有该name的扩展则报错
  2. 从缓存中获取class,如果没有则newInstance,并加入缓存
  3. 属性注入:如果该实例对象的字段是扩展接口类型,查找所有扩展中该扩展的并将注入到实例对象中
  4. 使用wrapper包装:在loadFile加载扩展的时候会把符合条件(条件就是扩展类包含扩展接口作为入参的构造方法)的wrapper加入cachedWrapperClasses,将所有的wrapper应用到instance,并调用injectExtension注入wrapper中属性

总结

dubbo按照java中的SPI机制来保证扩展性,按照约定将对应的配置文件放在指定的目录下,通过读取配置件的方式来获取接口的扩展实现,可以使用动态编译的方法可以动态获取扩展的适配类。将所有实例化的类缓存起来可以保证单例而且加快速度,同时dubbo有简单的属性装配功能,在实例化扩展的时候会将属性也为扩展接口的字段进行注入。

dubbo源码—SPI的更多相关文章

  1. 【Dubbo 源码解析】02_Dubbo SPI

    Dubbo SPI:(version:2.6.*) Dubbo 微内核 + 插件 模式,得益于 Dubbo SPI .其中 ExtentionLoader是 Dubbo SPI 最核心的类,它负责扩展 ...

  2. dubbo源码解析-spi(4)

    前言 本篇是spi的第四篇,本篇讲解的是spi中增加的AOP,还是和上一篇一样,我们先从大家熟悉的spring引出AOP. AOP是老生常谈的话题了,思想都不会是一蹴而就的.比如架构设计从All in ...

  3. dubbo源码解析-spi(3)

    前言 在上一篇的末尾,我们提到了dubbo的spi中增加了IoC和AOP的功能.那么本篇就讲一下这个增加的IoC,spi部分预计会有四篇,因为这东西实在是太重要了.温故而知新,我们先来回顾一下,我们之 ...

  4. dubbo源码解析-spi(一)

    前言 虽然标题是dubbo源码解析,但是本篇并不会出现dubbo的源码,本篇和之前的dubbo源码解析-简单原理.与spring融合一样,为dubbo源码解析专题的知识预热篇. 插播面试题 你是否了解 ...

  5. 【Dubbo源码阅读系列】之 Dubbo SPI 机制

    最近抽空开始了 Dubbo 源码的阅读之旅,希望可以通过写文章的方式记录和分享自己对 Dubbo 的理解.如果在本文出现一些纰漏或者错误之处,也希望大家不吝指出. Dubbo SPI 介绍 Java ...

  6. Dubbo源码分析之 SPI(一)

    一.概述 dubbo SPI 在dubbo的作用是基础性的,要想分析研究dubbo的实现原理.dubbo源码,都绕不过 dubbo SPI,掌握dubbo SPI 是征服dubbo的必经之路. 本篇文 ...

  7. Dubbo源码解析之SPI(一):扩展类的加载过程

    Dubbo是一款开源的.高性能且轻量级的Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用.智能容错和负载均衡,以及服务自动注册和发现. Dubbo最早是阿里公司内部的RPC框架,于 ...

  8. Dubbo源码剖析六之SPI扩展点的实现之Adaptive功能实现原理

    接Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)继续分析Adaptive功能实现原理.Adaptive的主 ...

  9. Dubbo源码剖析六之SPI扩展点的实现之getExtension

    上文Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中分析了getExtensionLoader,本文继续分 ...

随机推荐

  1. idea 配置tomcat(包含tomcat Server找不到的配置方法)

    在配置tomcate时有时候按照网上说的找不到tomcat Server,不知不觉花了很长时间这时我们在这里配置就可以了如图所示 1.这是有tomcat Server的配置(后面是没有的情况下的配置) ...

  2. bash脚本条件测试总结

    一.if语句的结构 分为以下三种:单分支.双分支.多分支 单分支if语句 if CONDITION is True: then 分支 fi 双分支if语句 if CONDITION is True: ...

  3. java中自动装箱带来的性能问题

    之前没有特别注意自动封装所带来的性能问题,今天看了effective java,实验了一下,结果大吃一惊: 考虑下面这段代码: public static void main(String args[ ...

  4. windows服务器下iis的性能优化 服务器

    IIS性能优化 1.调整IIS高速缓存 HKEY_LOCAL_MACHINE SystemCurrentControlSetServicesInetInfoParametersMemoryCacheS ...

  5. 使用composer更新thinkphp5或则yii2的版本

    更新thinkphp5或则yii2的版本,我目前采用的是用composer去更新,小伙伴们如果有其他更好的办法更新,可以直接评论给我,不胜感激啊. 如果还没有安装 Composer ,你可以按 Com ...

  6. Python学习二:词典基础详解

    作者:NiceCui 本文谢绝转载,如需转载需征得作者本人同意,谢谢. 本文链接:http://www.cnblogs.com/NiceCui/p/7862377.html 邮箱:moyi@moyib ...

  7. 最小化安装linux CentOS_7操作系统

    实验环境为VMware虚拟机安装操作系统. 1.打开VMware Workstation 虚拟机,选择创建新的虚拟机: 2.选择linux-CentOS 64位操作系统: 3.为虚拟机命名,并选择安装 ...

  8. Spring JDBC 示例

    在使用普通的 JDBC 数据库时,就会很麻烦的写不必要的代码来处理异常,打开和关闭数据库连接等.但 Spring JDBC 框架负责所有的低层细节,从开始打开连接,准备和执行 SQL 语句,处理异常, ...

  9. java 之 简单工厂模式(大话设计模式)

    以前只是看设计模式,每次看完都去理解一次,并没有手动去写代码,所以理解的还不是很深刻,最近查看框架源码,发现很多地方用到的都是设计模式,因为对设计模式理解的不够深刻,所以源码查看进度很慢!现在决定来温 ...

  10. 流式处理的新贵 Kafka Stream - Kafka设计解析(七)

    原创文章,转载请务必将下面这段话置于文章开头处. 本文转发自技术世界,原文链接 http://www.jasongj.com/kafka/kafka_stream/ Kafka Stream背景 Ka ...