什么是SPI

SPI是JDK内置的一种服务提供发现机制。目前市面上很多框架都用它来做服务的扩展发现。简单的说,它是一种动态替换发现的机制。

jdk 实现方式

需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service

在该目录下创建一个 properties 文件,该文件需要满足以下几个条件 :

  • 文件名必须是扩展的接口的全路径名称
  • 文件内部描述的是该扩展接口的所有实现类
  • 文件的编码格式是 UTF-8
  • 通过 java.util.ServiceLoader 的加载机制来发现

实现机制

样例代码

SPI 入口ServiceLoader.load(),先从此处开始分析。

先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,再创建一个 LazyIterator。最后在reload()函数中调用LazyIterator(),而LazyIterator()实现了Iterator接口,在样例中调用了,iterator() 跟进看其实现。





跟进框出的代码,追溯到最终的核心部分,可以分析出,约定好的地方找到接口对应的文件,然后加载文件并且解析文件里面的内容。而nextService()代码干了啥呢?



就是通过文件里填写的全限定名加载类,并且创建其实例放入缓存之后返回实例。整体流程如下:

SPI缺陷

  • JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,什么意思呢?就是如果你在 META-INF/service 下的文件里面加了 N 个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到, 那么会很浪费资源
  • 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因

SPI Demo

项目Demo

Dubbo SPI

Dubbo也用了SPI思想,不过没有用JDK的SPI机制,是自己实现的一套SPI机制。在Dubbo的源码中,很多地方会存在下面这样的三种代码,分别是自适应扩展点、指定名称的扩展点、激活扩展点。

Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。

Dubbo 规范如下:

  • 指定路径在META-INF/dubbo META-INF/internal META-INF/services 下
  • 文件名(全路径)内容 (key,vlaue)
@SPI("dubbo")
public interface Protocol { int getDefaultPort(); @Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); }

Dubbo SPI Demo

首先在 META-INF/dubbo 目录下按接口全限定名建立一个文件,内容如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

然后在接口上标注@SPI 注解,以表明它要用SPI机制。



下面的示例代码即可加载指定的实现类。



运行结果如下:

Dubbo 源码分析

大致流程就是先通过接口类找到一个 ExtensionLoader ,然后再通过 ExtensionLoader.getExtension(name) 得到指定名字的实现类实例。



做了一些判断然后从缓存里面找是否已经存在这个类型的 ExtensionLoader ,如果没有就新建一个塞入缓存。最后返回接口类对应的 ExtensionLoader 。getExtension()这个方法就是从类对应的 ExtensionLoader 中通过名字找到实例化完的实现类。



其中重点是createExtension()



整体逻辑很清晰,先找实现类,判断缓存是否有实例,没有就反射建个实例,然后执行 set 方法依赖注入。如果有找到包装类的话,再包一层。



getExtensionClasses方法进去也是先去缓存中找,如果缓存是空的,那么调用 loadExtensionClasses,我们就来看下这个方法。



loadDirectory里面就是根据类名和指定的目录,找到文件先获取所有的资源,然后一个一个去加载类,然后再通过loadClass去做一下缓存操作。



可以看到,loadClass 之前已经加载了类,loadClass 只是根据类上面的情况做不同的缓存。分别有 Adaptive 、WrapperClass 和普通类这三种,普通类又将Activate记录了一下。至此对于普通的类来说整个 SPI 过程完结了。

再分别看不是普通类的几种东西是干啥用的。

Adaptive 注解 - 自适应扩展

我们先来看一个场景,首先我们根据配置来进行 SPI 扩展的加载,但是我不想在启动的时候让扩展被加载,我想根据请求时候的参数来动态选择对应的扩展。

Dubbo 通过一个代理机制实现了自适应扩展,简单的说就是为你想扩展的接口生成一个代理类,可以通过JDK 或者 javassist 编译你生成的代理类代码,然后通过反射创建实例。

这个实例里面的实现会根据本来方法的请求参数得知需要的扩展类,然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(从参数得来的name),来获取真正的实例来调用。

再来看下源码,到底怎么做的。

这个注解就是自适应扩展相关的注解,可以修饰类和方法上,在修饰类的时候不会生成代理类,因为这个类就是代理类,修饰在方法上的时候会生成代理类。

Adaptive 注解在类上

ExtensionFactory 有三个实现类,其中一个实现类就被标注了 Adaptive 注解。



在 ExtensionLoader 构造的时候就会去通过getAdaptiveExtension 获取指定的扩展类的 ExtensionFactory。



我们再来看下 AdaptiveExtensionFactory 的实现。

可以看到先缓存了所有实现类,然后在获取的时候通过遍历找到对应的 Extension。

我们再来深入分析一波 getAdaptiveExtension 里面到底干了什么。

到这里其实已经和上文分析的 getExtensionClasses中loadClass 对 Adaptive 特殊缓存相呼应上了。

Adaptive 注解在方法上

注解在方法上则需要动态拼接代码,然后动态生成类,我们以 Protocol 为例子来看一下。

Protocol 没有实现类注释了 Adaptive ,但是接口上有两个方法注解了 Adaptive ,有两个方法没有。因此它走的逻辑应该应该是 createAdaptiveExtensionClass



具体在里面如何生成代码的我就不再深入了,有兴趣的自己去看吧,我就把成品解析一下,就差不多了。



可以看到会生成包,也会生成 import 语句,类名就是接口加个$Adaptive,并且实现这接口,没有标记 Adaptive 注解的方法调用的话直接抛错。

我们再来看一下标注了注解的方法,我就拿 export 举例。

WrapperClass - AOP

包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,并且比较不好维护。

因此就搞了个包装类,Dubbo 里帮你自动包装,只需要某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类,然后记录下来,用来包装别的实现类。

injectExtension - IOC

直接看代码,很简单,就是查找 set 方法,根据参数找到依赖对象则注入。

Activate 注解

这个注解我就简单的说下,拿 Filter 举例,Filter 有很多实现类,在某些场景下需要其中的几个实现类,而某些场景下需要另外几个,而 Activate 注解就是标记这个用的。

它有三个属性,group 表示修饰在哪个端,是 provider 还是 consumer,value 表示在 URL参数中出现才会被激活,order 表示实现类的顺序。

总结

先放个上述过程完整的图。

本文参考:

阿里面试真题:Dubbo的SPI机制

【Dubbo】SPI的更多相关文章

  1. 【dubbo】服务提供者运行的三种方式

    [dubbo]服务提供者运行的三种方式 学习了:https://blog.csdn.net/yxwb1253587469/article/details/78712451 1,使用容器: 2,使用自建 ...

  2. 【DUBBO】 Dubbo原理解析-Dubbo内核实现之基于SPI思想Dubbo内核实现

    转载:http://blog.csdn.net/quhongwei_zhanqiu/article/details/41577235 SPI接口定义 定义了@SPI注解 public @interfa ...

  3. 【DUBBO】dubbo的LoadBalance接口

    LoadBalance负载均衡, 负责从多个 Invokers中选出具体的一个Invoker用于本次调用,调用过程中包含了负载均衡的算法,调用失败后需要重新选择 --->类注解@SPI说明可以基 ...

  4. 【DUBBO】zookeeper在dubbo中作为注册中心的原理结构

    [一]原理图 [二]原理图解释 流程:1.服务提供者启动时向/dubbo/com.foo.BarService/providers目录下写入URL2.服务消费者启动时订阅/dubbo/com.foo. ...

  5. 【DUBBO】Dubbo:monitor的配置

    [一]:配置项 <dubbo:monitor protocol="registry"/> [二]:配置解析器-->具体解析器为com.alibaba.dubbo. ...

  6. 【DUBBO】Dubbo:protocol 的配置项

    [一]:配置项 <dubbo:protocol id="标识" port="端口号" name="名字"/> [二]:配置解析器 ...

  7. 【DUBBO】dubbo的registry配置

    [一]:配置项 注册中心地址:zookeeper://ip:端口 <dubbo:registry address="注册中心的地址" check="启动时检查注册中 ...

  8. 【DUBBO】dobbo的application的配置项

    Dubbo:application的配置项[一]:配置项 <dubbo:application name="服务名字" owner="拥有者" organ ...

  9. 【DUBBO】Dubbo原理解析-Dubbo内核实现之SPI简单介绍

    Dubbo采用微内核+ 插件体系,使得设计优雅,扩展性强.那所谓的微内核+插件体系是如何实现的呢!大家是否熟悉spi(service providerinterface)机制,即我们定义了服务接口标准 ...

随机推荐

  1. kubernetes的Deployment, DaemonSet, Job 和 CronJob事例

    k8s kubernetes给node节点添加标签和删除node节点标签 Deployment配置文件exampledeploymentv1.yaml apiVersion: apps/v1 kind ...

  2. WSL2 Ubuntu 图形界面环境搭建(Xfce4 、XServer)

    安装wsl2和Ubuntu 在安装了wsl2后有时候需要传文件到ubuntu上面,比如传一个测试项目什么的.因为wsl里面挂载了本地的磁盘,所以准备安装个图形界面,操作下也挺简单的. 关于wsl2和U ...

  3. PTA沈师数据库原理——DB(10)_SQL实验题

    R10-1 A1-3查询顾客表中所有不重复的城市 (2 分) select distinct Cityfrom customersR10-2 查询学生表所有学生记录 (2 分) select * fr ...

  4. 计算机系统原理:cache容量计算

    Cache容量计算例题: 假定主存地址位数为32位,按字节编址,主存和cache之间采用4-路组相联映射方式,主存块大小为4个字,每字32位,采用直写(Write Throght)方式和LRU替换策略 ...

  5. hdu3018 一笔画问题

    题意:       给你一幅画,这幅画由点和边构成,问你最少几笔能把这幅画画完. 思路:      这个题目的结论比较巧妙,首先我们考虑下,如果给的图是欧拉图,或者是条欧拉回路,那么我们一笔就搞定了, ...

  6. John the Ripper破解密码

    目录 John the Ripper 破解Linux系统密码 破解Windows系统密码 John the Ripper John the Ripper是一个快速的密码破解工具,用于在已知密文的情况下 ...

  7. 『政善治』Postman工具 — 7、Postman中保存请求(Collections集合)

    目录 1.创建Collection 2.保存Request请求 3.查看保存的请求 4.Collection下还可以创建文件夹 5.补充:Postman中的变量 6.总结 1.创建Collection ...

  8. Codeforces Round #691 (Div. 2)

    A. Red-Blue Shuffle 题意:有两个长度为n的数组,数组a和数组b,问那个数组中的数字相比之下比另一个数组中相应位置的元素值更大一些,如果数组a大就输出RED,如果数组b大就输出BLU ...

  9. 利用实体bean对象批量数据传输处理

    利用实体bean对象批量数据传输处理 需求 现在有两方数据库表结构相同,一方A.另一个方B,现想从A处查询出多个表的数据,传输到B地保存起来. 解决方案1 最简单粗暴的方法就是,查询出A处相关表的数据 ...

  10. [Django框架 - 静态文件配置、request对象方法初识、 pycharm链接数据库、ORM实操增删改查、django请求生命周期]

    [Django框架 - 静态文件配置.request对象方法初识. pycharm链接数据库.ORM实操增删改查.django请求生命周期] 我们将html文件默认都放在templates文件夹下 将 ...