SPI机制

一、基础

1.什么是spi机制?

SPI (Service Provider Interface),主要用于扩展的作用。

举个例子来说,假如有一个框架有一个接口,他有自己默认的实现类,但是在代码运行的过程中,你不想用他的实现类或者想扩展一下他的实现类的功能,但是此时你又不能修改别人的源码,那么此时该怎么办?

这时spi机制就有了用武之地。一般框架的作者在设计这种接口的时候不会直接去new这个接口的实现类,而是在Classpath路径底下将这个接口的实现类按作者约定的格式写在一个配置文件上,然后在运行的过程中通过java提供的api,从所有jar包中读取所有的这个指定文件中的内容,获取到实现类,用这个实现类,这样,如果你想自己替换原有的框架的实现,你就可以按照作者规定的方式配置实现,这样就能使用你自己写的实现类了。

spi机制其实体现了设计思想中的解耦思想,方便开发者对框架功能进行扩展。

自己理解:抛弃了接口必有实现类的思想,而是通过扫描配置文件中的类去实现对应的接口,以达到扩展的接口实现的目的。

二、应用

1.java的spi机制 -- ServiceLoader

1.实现方式

java中最常见的spi机制应用就是数据库驱动的加载,java其实就是定义了java语言跟数据库交互的接口,但是具体的实现得交给各大数据库厂商来实现,那么java怎么知道你的数据库厂商的实现了?

这时就需要spi机制了,java约定好了在 Classpath 路径下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后内容是该数据库厂商的实现的接口的全限定名,这样数据库厂商只要按照这个规则去配置,java就能找到。

我以mysql来举例,看一下mysql是怎么实现的。

内容:com.mysql.jdbc.Driver

java是通过ServiceLoader类来实现读取配置文件中的实现类的。大家有兴趣可以看一下里面的代码,其实就是读取到每个jar包底下的文件,读取里面的内容。

自己理解:Java加载了所有目录文件下的类,但是使用的时候要指定具体是哪一个类去实现。

2.java的spi机制的缺点?

从我们分析java的spi机制可以看出,java约定了文件名为接口的名称,内容为实现。

不知道大家有没有想过这里面有个很严重的问题,就是虽然我获取到了所有的实现类,但是无法对实现类进行分类,也就是说我无法确定到底该用哪个实现类,并且java的spi机制会一次性给所有的实现类创建对象,如果这个对象你根本不会使用,那么此时就会白白浪费资源,也就是说无法做到按需加载。

因此,dubbo就自己实现了一套spi机制,不仅解决了以上的痛点,同时也加入了更多的特性。

2.spring中的spi机制 -- SpringFactoriesLoader

相信spring大家都不陌生,在spring扩展也是依赖spi机制完成的,只不过spring对于扩展文件约定在Classpath 路径下的 META-INF目录底下,所有的文件名都是叫spring.factories,文件里的内容是一个以一个个键值对的方式存储的,键为类的全限定名,值也为类的全限定名,如果有多个值,可以用逗号分割,有一点得注意的是,键和值本身约定并没有类与类之间的依赖关系(当然也可以有,得看使用场景的约定),也就是说键值可以没有任何关联,键仅仅是一种标识,代表一种场景,最常见的自动装配的注解,@EnableAutoConfiguration,也就是代表自动装配的场景,当你需要你的类被自动装配,就可以以这个注解的权限定名键,你的类为名,这样springboot在进行自动装配的时候,就会拿这个键,找到你写的实现类来完成自动装配。

这里我贴出了自动装配时加载类的源码。

这里其实就是通过@EnableAutoConfiguration的全限定名从spring.factories中加载这个键对应的所有的实现类的名称,这样就能拿到所有需要自动装配的类的全限定名了。

3.dubbo的spi机制 -- ExtensionLoader

ExtensionLoader是dubbo的spi机制所实现的类,通过这个类来加载接口所有实现类,获取实现类的对象。同时每一个接口都会有一个自己的ExtensionLoader。

1.spi机制约束

dubbo的配置文件约束

dubbo会从四个目录读取文件META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/services/、META-INF/dubbo/external/,文件名为接口的全限定名,内容为键值对,键为短名称(可以理解为spring中的对象的名称),值为实现类。

@SPI 注解的约束

dubbo中所有的扩展接口,都需要在接口上加@SPI注解,不然在创建ExtensionLoader的时候,会报错。代码体现在这里

2.实现类的加载

先说各种特性之前,先说一下这些实现类是如何加载的,类的加载是非常重要的一个环节,与后面的spi特性有重要的关系。

类加载默认都是先调用getExtensionClasses这个方法的,当cachedClasses没有的时,才会去加载实现类,然后再把实现类放到cachedClasses中。真正实现加载的是loadExtensionClasses 方法,接下来我们详细看这个方法的源码。

checkDestroyed();

方法没什么东西,其实就是一个检查的作用。

cacheDefaultExtensionName();

缓存默认实现类的短名称。其实很简单,就是从@SPI注解中取出名称,就是默认的实现类的名称,缓存起来,ExtensionLoader有个getDefaultExtension方法,其实就是通过这个短名称对应的实现类的对象。

接下来会遍历LoadingStrategy,根据LoadingStrategy加载指定目录的文件。

我们先来看看LoadingStrategy的实例是怎么加载的。我们进入loadLoadingStrategies方法,

惊讶的发现竟然是使用了java的spi机制加载LoadingStrategy,那我们就去Classpath 路径下的 META-INF/services/路径下找这个LoadingStrategy接口的全限定名的文件,看看有哪些实现。有四个实现,也就是会按照这四个的加载策略来读取实现类。

其中有个方法directory,就是指定加载的目录,这也就是我们前面说的那几个dubbo会加载的目录,其实是从这个方法返回的,你可以自己去看看这四个实现类对于这个方法的实现。其实我们也可以实现这个接口,指定我们自己想加载的目录。

这里会循环加载每个目录,我们进去loadDirectory方法。

这个其实就是拿出LoadingStrategy来调用重载的loadDirectory方法。

这里注意会调用两次loadDirectory,下面的那个其实是适配以前老版本的,不用关心。

接下来进去重载的loadDirectory方法。

可以看出,fileName就是LoadingStrategy所指定的目录 + 接口的全限定名,这里就解释了为什么实现类需要写在类全限定名的文件里。其实就是从每个jar底指定的目录类全限定名为名称的文件,得到每个jar底下的文件的URL。然后遍历每个URL,加载类,我们进入loadResource方法来看看具体是怎么加载的。

通过URL打开一个输入流,然后读取文件内容,取出每一行,以 = 进行分割(因为规定的是以键值对存的),键就是短名称,值就是实现类的名称,然后再进入loadClass方法,这个方法很重要,其实是对实现类进行一个分类,后面dubbo的特性实现的前提就是对这些实现类的分类操作。

标红的两处是这个意思

如果你加了@Adaptive注解,那么就将赋值到cachedAdaptiveClass属性上。我们叫这个类为自适应类。什么是自适应,其实说白了这个类本身并没有实际的意义,它是根据你的入参动态来实现找到真正的实现类来完成调用。getAdaptiveExtension其实就是获取到这个自适应实现类对应的对象。

如果你的实现类是有一个该类型为参数的构造方法,那么就将这个实现类放到cachedWrapperClasses中,并且我们称这个类为包装类,什么叫包装,其实跟静态代理有点像,就是将目标对象进行代理,可以增强功能。

这处标红的意思是判断是不是实现类是不是加了@Activate注解,是的话就将短名称和注解放入cachedActivates中,我们称这类实现类为自动激活的类,所谓的自动激活,就是可以根据你的入参,动态选择实现一批符合条件的实现类

saveInExtensionClass就是将这个实现类放入extensionClasses中,该目录下的实现类就加载完成了。

接下来会继续循环,加载不同的目录底下,都会进行分类,并放到extensionClasses中。

当LoadingStrategy循环玩之后,最后将extensionClasses放入cachedClasses中,此时就完成了对于指定目录下实现类的加载和分类。至此,实现类的加载和分类就完成了。

3.实现类对象构造

看实现类对象构造过程之前,先看获取,因为获取不到才构造,也就是java中spi没有的功能,按需加载。

获取实现类对象的方法是getExtension方法,传入的name参数就是短名称,也就是spi文件的键,wrap是是否包装的意思,true的意思就是对你获取的目标对象进行包装(具体什么是包装,如何包装后面会讲),wrap默认是true

接下来我们就着重分析getExtension方法

前面两个if我说一下,

第一个if比较简单,就是简单的参数校验,name参数不能为空

第二个if判断name是不是字符串true,是的话就调用getDefaultExtension,getDefaultExtension这个方法通过名称也能看出来就是获取接口默认的实现,什么是默认实现?默认的实现就是@SPI注解中的名称对应的实现类。

前面两个if之后就是真正获取实现了。在获取之前,先根据你是否包装构建缓存的键值,如果没有包装,就会在短名称后加上 _origin ,这主要是为了区分包不包装,然后进入getOrCreateHolder方法

里面其实就是通过缓存名称从cachedInstances获取一个Holder,获取不到就new一个Holder然后放到cachedInstances中,然后返回。Holder其实本身并没有什么意义,可以理解为一个空壳,里面放的才是真正最终返回的对象。

第一次,不用说Holder肯定没有,那么这个Holder肯定是刚new出来的。

跳出getOrCreateHolder方法,继续往下看。

从Holder中获取实现类,此时肯定是null,接下来就是synchronized,然后又是非空判断。这里其实是典型的单例模式中的双重检查机制,保证并发安全。其实从这里可以看出Holder的作用。这里是为了减少锁冲突的,因为一个实现类对象对应一个Holder对象,这样不同的实现类在创建的时候,由于Holder的不同,synchronized就不是同一个锁对象,这就起到了并发时候减少锁冲突的作用,从这可以看出dubbo设计的时候的细节是很到位的。

第一次都是null,接下来进入createExtension方法,构建对象的过程

先从实现类的缓存中获取到短名称对应的实现类,上面提到,实现类加载之后会放到内部的一个缓存中。

这个if条件判断一般肯定是false的,但是有些情况,就比如第一次构建对象抛出异常,此时第二次来构建这个对象,那么不用说肯定也会有问题,dubbo为了快速知道哪些实现类对象构造的时候会出异常,就在第一次构建对象抛异常的时候缓存了实现类的短名称到unacceptableExceptions中,当第二次来构建的时候,能够快速知道,抛出异常,减少资源的浪费。


接下来就会从extensionInstances中获取实例,这个实例是没有包装的实例,也就是说如果你获取的不带包装的实例,就是这个实例。我们看看这个实例是怎么构建出来的,这里我根据构建的不同阶段进行划分为以下几个步骤。

参考资料

1.面试常问的dubbo的spi机制到底是什么? - 三友的java日记 - 博客园

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值