一、简介
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。
二、SPI 示例
2.1 Java SPI 示例
SPI,Service Provider Interface,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
你想一下首先市面上的数据库五花八门,不同的数据库底层协议的大不相同,所以首先需要定制一个接口,来约束一下这些数据库,使得 Java 语言的使用者在调用数据库的时候可以方便、统一的面向接口编程。
Java SPI 就是这样做的,约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。
这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。
比如我们看下 MySQL 是怎么做的。
MySQL 就是这样做的,为了让大家更加深刻的理解我再简单的写一个示例。
首先,我们定义一个接口
public interface DubboService {
void sayHello(String msg);
}
接下来定义两个实现类,分别为 DubboServiceImpl和 DubboServiceTwo
public class DubboServiceTwo implements DubboService {
@Override
public void sayHello(String msg) {
System.out.println("这个是一个java spi 测试类第二个:"+msg);
}
}
public class DubboServiceImpl implements DubboService {
@Override
public void sayHello(String msg) {
System.out.println("这个是一个java spi 测试类第一个:"+msg);
}
}
接下来 META-INF/services 文件夹下创建一个文件,名称为 com.study.spi.test.DubboService。文件内容为实现类的全限定的类名,如下:
其内容为:
做好所需的准备工作,接下来编写代码进行测试。
public class MainTest {
public static void main(String[] args) {
System.out.println( "Hello World!" );
//根据ServiceLoader 获取DubboService的实现
ServiceLoader<DubboService> serviceLoader = ServiceLoader.load(DubboService.class);
//这里我们配置了两个实现类 会打印两次
for(DubboService databaseDriver:serviceLoader){
databaseDriver.sayHello("哈哈");
}
}
}
执行结果如下:
从测试结果可以看出,我们的两个实现类被成功的加载,并输出了相应的内容。
2.1.1、实现 SPI 需要遵循的标准
我们如何去实现一个标准的 SPI 发现机制呢?其实很简单,只需要满足以下提交就行了
1. 需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service
2. 在该目录下创建一个 properties 文件,该文件需要满足以下几个条件:
- 2.1 文件名必须是扩展的接口的全路径名称
- 2.2 文件内部描述的是该扩展接口的所有实现类
- 2.3 文件的编码格式是 UTF-8
3. 通过 java.util.ServiceLoader 的加载机制来发现
2.1.2、SPI 的缺点
1. JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,什么意思呢?就是如果你在 META-INF/service 下的文件里面加了 N 个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到, 那么会很浪费资源。
所以说 Java SPI 无法按需加载实现类。
2. 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因。
2.2、Dubbo 优化后的 SPI 机制
Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。
2.2.1、Dubbo 的 SPI 扩展机制,有两个规则
1、Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。
-
META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
-
META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
-
META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。
2、文件名称和接口名称保持一致,文件内容和 SPI 有差异,配置文件里面存放的是键值对,我截一个Chace 的配置
2.2.2、Dubbo SPI 简单实例
1、创建 MyProtocol 协议类
实现自己的协议,我们为了模拟协议产生了作用,修改一个端口
public class MyProcol implements Protocol {
@Override
public int getDefaultPort() {
return 8848;
}
@Override
public <T> Ex