这又是什么呢,这个可以很好的支持一些有特殊需求的三方的接入,可以自定义扩展,自主定制二次开发,良好的扩展性对于框架来说是很重要的。
简单了解下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.SpiImpl1
- com.tech.dayu.spi.SpiImpl2
测试
- public class Test {
- public static void main(String[] args) {
- Iterator
providers = Service.providers(SPIService.class); - ServiceLoader
load = ServiceLoader.load(SPIService.class); -
- while(providers.hasNext()) {
- SPIService ser = providers.next();
- ser.execute();
- }
- System.out.println("###################");
- Iterator
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一样直接全部加载,那样可能会造成大量的资源浪费的,甚至可能还会做无用功
【编辑推荐】