Dubbo的SPI机制是啥啊?

263 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

​前言

之前大致的把Dubbo的运作流程简单的分析了一遍了,Dubbo还有一个很大的优点,就是采用的微内核+SPI扩展设计

这又是什么呢,这个可以很好的支持一些有特殊需求的三方的接入,可以自定义扩展,自主定制二次开发,良好的扩展性对于框架来说是很重要的

简单了解下SPI,全称为 Service Provider Interface,是一种服务发现机制。

它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。

举个例子,比如你有个接口,现在这个接口有 3 个实现类,那么在系统运行的时候对这个接口到底选择哪个实现类呢?这就需要SPI了,需要根据指定的配置或者是默认的配置,去找到对应的实现类加载进来,然后用这个实现类的实例对象。

Java中JDK自身实现了SPI机制,基于策略模式来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现

但是呢,存在一定的缺点,比如不能按照需要加载,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源;不支持AOP和依赖注入,实现类的方式也不够灵活,只能通过 Iterator 形式获取

你不够强,或者说你做的不符合我的需求,我就替换你

于是呢,dubbo重新实现了一套功能更强的 SPI 机制, 支持了AOP与依赖注入,并且 利用缓存提高加载实现类的性能,同时支持实现类的灵活获取

Java中的SPI

Java中JDK自身实现了SPI机制,基于策略模式来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现

首先,我们需要定义一个接口,SPIService

public interface SPIService {    void execute();}

然后,定义两个实现类,没别的意思,只输入一句话。

public class SpiImpl1 implements SPIService{    public void execute() {        System.out.println("SpiImpl1.execute()");    }}​public class SpiImpl2 implements SPIService{    public void execute() {        System.out.println("SpiImpl2.execute()");    }}

最后呢,要在ClassPath路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。内容就是实现类的全限定类名:

com.tech.dayu.spi.SpiImpl1com.tech.dayu.spi.SpiImpl2

测试

public class Test {    public static void main(String[] args) {            Iterator<SPIService> providers = Service.providers(SPIService.class);        ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);​        while(providers.hasNext()) {            SPIService ser = providers.next();            ser.execute();        }        System.out.println("###################");        Iterator<SPIService> iterator = load.iterator();        while(iterator.hasNext()) {            SPIService ser = iterator.next();            ser.execute();        }    }}

两种方式的输出结果是一致的:

SpiImpl1.execute()SpiImpl2.execute()--------------------------------SpiImpl1.execute()SpiImpl2.execute()

我们来看下源码,位于java.util包下。我们就以ServiceLoader.load为例,通过源码看看它里面到底怎么做的

ServiceLoader.load()其实就是 Java SPI 入口

看到最后调用的是reload,最后生效的是在这个LazyIterator的内部,等同于是一个迭代器的遍历,遍历相应的文件中的service的实现类,就是我们上面命名的那些

这里无论是if还是else最后调用的都是nextService()方法,点进去看

可以看到无非就是通过名字获取到文件路径,获取全限定名来加载类,并且创建其实例放入到相应的缓存之后并且返回实例,这大体就是整个的实现逻辑,应该不难吧,咱们自己来实现个这个应该也是分分钟的事

好了,Java的SPI源码分析的差不多了,问题也随之而来,比如不能按照需要加载,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源;不支持AOP和依赖注入,实现类的方式也不够灵活,只能通过 Iterator 形式获取

接下来咱们来分析Dubbo的SPI

Dubbo中的SPI

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。

Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下。

Dubbo要判断一下,在系统运行时,应该选用这个Protocol接口的哪个实现类。它会去找一个你配置的Protocol,将你配置的Protocol实现类,加载进JVM,将其实例化,微内核,可插拔,大量的组件,Protocol负责RPC调用的东西,你可以实现自己的RPC调用组件,实现Protocol接口,给自己的一个实现类就可以啦

Dubbo里很多都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现就可以啦

我们随便来看一下其中的

并且 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的源码

在Dubbo中ExtensionLoader类似 Java SPI 中 ServiceLoader 的存在。大致流程就是先通过接口类找到ExtensionLoader ,然后再通过 ExtensionLoader.getExtension(name) 得到指定名字的实现类实例。

其实也是很简单的,就是通过一顿判断然后在缓存中检查是否存在这个类型的ExtensionLoader ,没有的话就新建一个放进去缓存,最后返回接口类的对应的ExtensionLoader

getExtension() 方法,从现象我们可以知道这个方法就是从类对应的 ExtensionLoader 中通过名字找到实例化完的实现类

内部的createExtension()方法,我就不截图了,比较长,就是先找实现类,判断是否有该类的缓存,没有的话就通过反射新建一个实例对象,然后放进去到这里其实就差不多了分析的,拿到实例对然后就可以执行了Dubbo的SPI主要就是为了增加框架的可拓展性,可以在其基础上进行二次开发,还有一个更重要的点就是不会像Java的SPI一样直接全部加载,那样可能会造成大量的资源浪费的,甚至可能还会做无用功结尾

好了,以上就是全部内容了,我是船长,你们的学习成长小伙伴

我希望有一天能够靠写字养活自己,现在还在磨练,这个时间可能会有很多年,感谢你们做我最初的读者和传播者。请大家相信,只要给我一份爱,我终究会还你们一页情的。

再次感谢大家能够读到这里,我后面会持续的更新技术文章以及一些记录生活的灵魂文章,如果觉得不错的,觉得【船长】有点东西的话,求点赞、关注、分享三连

哦,对了!后续的更新文章我都会及时放到这里,欢迎大家点击观看,都是干货文章啊,建议收藏,以后随时翻阅查看

github.com/DayuMM2021/…