关于dubbo:Dubbo的SPI机制

2次阅读

共计 5116 个字符,预计需要花费 13 分钟才能阅读完成。

前言

上一篇 Dubbo 文章敖丙曾经带了大家过了一遍整体的架构,也提到了 Dubbo 的胜利离不开它 采纳微内核设计 +SPI 扩大,使得有非凡需要的接入方能够自定义扩大,做定制的二次开发。

良好的扩展性对于一个框架而言尤其重要,框架顾名思义就是 搭好外围架子 ,给予用户简略便捷的应用,同时也须要 满足他们定制化的需要

Dubbo 就依附 SPI 机制实现了插件化性能,简直将所有的性能组件做成基于 SPI 实现,并且默认提供了很多能够间接应用的扩大点,实现了面向性能进行拆分的对扩大凋谢的架构

什么是 SPI

首先咱们得先晓得什么叫 SPI。

SPI (Service Provider Interface),次要是用来在框架中应用的,最常见和莫过于咱们在拜访数据库时候用到的 java.sql.Driver 接口了。

你想一下首先市面上的数据库形形色色,不同的数据库底层协定的大不相同,所以首先须要 定制一个接口,来束缚一下这些数据库,使得 Java 语言的使用者在调用数据库的时候能够不便、对立的面向接口编程。

数据库厂商们须要依据接口来开发他们对应的实现,那么问题来了,真正应用的时候到底用哪个实现呢?从哪里找到实现类呢?

这时候 Java SPI 机制就派上用场了,不晓得到底用哪个实现类和找不到实现类,咱们通知它不就完事了呗。

大家都约定好将实现类的配置写在一个中央,而后到时候都去哪个中央查一下不就晓得了吗?

Java SPI 就是这样做的,约定在 Classpath 下的 META-INF/services/ 目录里创立一个 以服务接口命名的文件 ,而后 文件外面记录的是此 jar 包提供的具体实现类的全限定名

这样当咱们援用了某个 jar 包的时候就能够去找这个 jar 包的 META-INF/services/ 目录,再依据接口名找到文件,而后读取文件外面的内容去进行实现类的加载与实例化。

比方咱们看下 MySQL 是怎么做的。

再来看一下文件外面的内容。

MySQL 就是这样做的,为了让大家更加粗浅的了解我再简略的写一个示例。

Java SPI 示例

而后我在 META-INF/services/ 目录下建了个以接口全限定名命名的文件,内容如下

com.demo.spi.NuanNanAobing
com.demo.spi.ShuaiAobing

运行之后的后果如下

Java SPI 源码剖析

之前的文章我也提到了 Dubbo 并没有用 Java 实现的 SPI,而是自定义 SPI,那必定是 Java SPI 有什么不不便的中央或者劣势。

因而丙带着大家先深刻理解一下 Java SPI,这样能力晓得哪里不好,进而再和 Dubbo SPI 进行比照的时候会更加的清晰其劣势。

大家看到源码不要怕,丙曾经给大家做了正文,并且逻辑也不难的,想要变强源码不可或缺。为了让大家更好的了解,丙在源码剖析完了之后还会画个图,帮大家再理一下思路。

从下面我的示例中能够看到 ServiceLoader.load() 其实就是 Java SPI 入口,咱们来看看到底做了什么操作。

我用一句话概括一下,简略的说就是先找以后线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,而后革除一下缓存,再创立一个 LazyIterator。

那当初重点就是 LazyIterator 了,从下面代码能够看到咱们调用了 hasNext() 来做实例循环,通过 next() 失去一个实例。而 LazyIterator 其实就是 Iterator 的实现类。咱们来看看它到底干了啥。

不论进入 if 分支还是 else 分支,重点都在我框出来的代码,接下来就进入重要时刻了!

能够看到这个办法其实就是在约定好的中央找到接口对应的文件,而后加载文件并且解析文件外面的内容。

咱们再来看一下 nextService()。

所以就是通过文件里填写的全限定名加载类,并且创立其实例放入缓存之后返回实例。

整体的 Java SPI 的源码解析曾经结束,是不是很简略?就是约定一个目录,依据接口名去那个目录找到文件,文件解析失去实现类的全限定名,而后循环加载实现类和创立其实例。

我再用一张图来带大家过一遍。

想一下 Java SPI 哪里不好

置信大家一眼就能看进去,Java SPI 在查找扩大实现类的时候遍历 SPI 的配置文件并且 将实现类全副实例化,假如一个实现类初始化过程比拟耗费资源且耗时,然而你的代码外面又用不上它,这就产生了资源的节约。

所以说 Java SPI 无奈按需加载实现类。

Dubbo SPI

因而 Dubbo 就本人实现了一个 SPI,让咱们想一下按需加载的话首先你得给个名字,通过名字去文件外面找到对应的实现类全限定名而后加载实例化即可。

Dubbo 就是这样设计的,配置文件外面寄存的是键值对,我截一个 Cluster 的配置。

并且 Dubbo SPI 除了能够按需加载实现类之外,减少了 IOC 和 AOP 的个性,还有个自适应扩大机制。

咱们先来看一下 Dubbo 对配置文件目录的约定,不同于 Java SPI,Dubbo 分为了三类目录。

  • META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI。
  • META-INF/dubbo/ 目录:该目录寄存用户自定义的 SPI 配置文件。
  • META-INF/dubbo/internal/ 目录:该目录寄存 Dubbo 外部应用的 SPI 配置文件。

Dubbo SPI 简略实例

用法很是简略,我就拿官网上的例子来展现一下。

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

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

而后在接口上标注 @SPI 注解,以表明它要用 SPI 机制,相似上面这个图(我就是拿 Cluster 的图举个例子,和这个示例代码定义的接口不一样)。

接着通过上面的示例代码即可加载指定的实现类。

再来看一下运行的后果。

Dubbo 源码剖析

此次剖析的源码版本是 2.6.5

置信通过下面的形容大家曾经对 Dubbo SPI 曾经有了肯定的意识,接下来咱们来看看它的实现。

从下面的示例代码咱们晓得 ExtensionLoader 如同就是重点,它是相似 Java SPI 中 ServiceLoader 的存在。

咱们能够看到大抵流程就是先通过接口类找到一个 ExtensionLoader,而后再通过 ExtensionLoader.getExtension(name) 失去指定名字的实现类实例。

咱们就先看下 getExtensionLoader() 做了什么。

很简略,做了一些判断而后从缓存外面找是否曾经存在这个类型的 ExtensionLoader,如果没有就新建一个塞入缓存。最初返回接口类对应的 ExtensionLoader。

咱们再来看一下 getExtension() 办法,从景象咱们能够晓得这个办法就是从类对应的 ExtensionLoader 中通过名字找到实例化完的实现类。

能够看到重点就是 createExtension(),咱们再来看下这个办法干了啥。

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

到这步为止我先画个图,大家理一理,还是很简略的。

那么问题来了 getExtensionClasses() 是怎么找的呢?injectExtension() 如何注入的呢(其实我曾经说了 set 办法注入)?为什么须要包装类呢?

getExtensionClasses

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

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

能够看到,loadClass 之前曾经加载了类,loadClass 只是依据类下面的状况做不同的缓存。别离有 AdaptiveWrapperClass 和一般类这三种,一般类又将 Activate 记录了一下。至此对于一般的类来说整个 SPI 过程完结了。

接下来咱们别离看不是一般类的几种货色是干啥用的。

Adaptive 注解 – 自适应扩大

在进入这个注解剖析之前,咱们须要晓得 Dubbo 的自适应扩大机制。

咱们先来看一个场景,首先咱们依据配置来进行 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 举例。

就像我后面说的那样,依据申请的参数,即 URL 失去具体要调用的实现类名,而后再调用 getExtension 获取。

整个自适应扩大流程如下。

WrapperClass – AOP

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

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

简略又奇妙,这就是 AOP 了。

injectExtension – IOC

间接看代码,很简略,就是查找 set 办法,依据参数找到依赖对象则注入。

这就是 IOC。

Activate 注解

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

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

总结

先放个上述过程残缺的图。

而后咱们再来总结一下,明天丙先带大家理解了下什么是 SPI,写了个简略示例,并且进行了 Java SPI 源码剖析。

得悉了 Java SPI 会一次加载和实例化所有的实现类。

而 Dubbo SPI 则本人实现了 SPI,能够通过名字实例化指定的实现类,并且实现了 IOC、AOP 与 自适应扩大 SPI。

整体而言不是很难,也不会很绕,大家看了文章之后如果本人再过一遍播种会更大。

我是敖丙,你晓得的越多,你不晓得的越多,咱们下期见!

人才 们的 【三连】 就是敖丙创作的最大能源,如果本篇博客有任何谬误和倡议,欢送人才们留言!

正文完
 0