什么是SPI:
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。使用SPI机制的优势是实现解耦,使得第三方服务模块的装配控制逻辑与调用者的业务代码分离。
JDK中的SPI:
java中如果想要使用SPI功能,先要提供标准的服务接口,然后再提供相关接口实现和服务调用者。通过SPI机制中约定的信息进行查询相应接口的实现,如下图:
SPI遵循的约定如下:
- 当服务提供者提供了一个接口的一种具体实现后,在META-INF/services 目录下创建一个以 “接口全限定名” 为命名的文件,内容为实现类的权限定名
- 接口实现类所在的jar包,放在主程序的classpath中
- 主程序通过 java.util.ServiceLoader 动态加载实现模块,它通过扫描META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到JVM
- SPI的实现类必须要有一个无参构造
JDK的SPI示例:
1、创建一个maven项目,用来存放服务接口:
2、在创建一个maven项目,用来实现上面创建的接口,这里的pom文件需要依赖上面创建的接口maven工程
创建完接口,下一步开始编写配置文件:
- 创建 META-INF/services 目录
- 在该目录下创建与接口的权限定名项目的文件: cn.zsm.spi.HelloSPI
- 配置文件的内容是任意接口的实现类的全限定名,可以写其中某一个或某几个。这里我两个都写了进去,如下图:
3、新建一个maven工程,用于测试SPI功能:
public static void main(String[] args) {
ServiceLoader<HelloSPI> load = ServiceLoader.load(HelloSPI.class);
for(HelloSPI helloSPI : load){
System.out.println(helloSPI.getClass().getName() + " , result is : "+helloSPI.helloSPI());
}
}
输出结果:
SPI源码简单介绍:
从上面SPI示例中可以看出,JSK的SPI功能的关键是ServiceLoader,下面我们简单介绍一下ServiceLoader的工作过程:
有兴趣的同学可以拔一下JDK中的源码,因为源码比较长,这里就不再赘述。
Dubbo中的SPI:
dubbo中使用了大量的SPI来作为扩展点,通过实现统一接口的前提下,可以进行定制自己的实现类。常见的有potocol协议、负载均衡算法等都可以通过SPI的方式进行定制化。后面我们讲述源码的时候会有涉及。
java的spi机制有着如下的弊端:
- 只能遍历所有的实现,并全部实例化。
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
- 扩展如果依赖其他的扩展,做不到自动注入和装配。
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持。
dubbo的spi有如下几个概念:
- 扩展点:一个接口。
- 扩展:扩展(接口)的实现。
- 扩展自适应实例:其实就是一个Extension的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。dubbo会根据接口中的参数,自动地决定选择哪个实现。
- @SPI:该注解作用于扩展点的接口上,表明该接口是一个扩展点。
- @Adaptive:@Adaptive注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo在为扩展点生成自适应实例时,如果方法有@Adaptive注解,会为该方法生成对应的代码。
总结一下,dubbo的SPI实现与JDK的实现有一下几点不同:
- pom文件需要引入dubbo
- dubbo的SPI服务的接口需要有 @SPI注解
- 接口可以用@Adaptive注解标识自适应方法, 并可以选择要使用的扩展点
- 配置文件的路径是: META-INF/dubbo
- 配置文件中的内容,除了有接口的实现类的全限定名外,每个实现类还要指定一个KEY
- SPI服务获取,使用ExtensionLoader.getExtensionLoader(****.class) 方法
dubbo的Adaptive功能,主要解决的问题是如何动态选择具体的扩展点。通过getAdaptiveExtension 统一对指定的接口对应的所有扩展点进行封装,通过URL 的方式对扩展点进行动态选择(dubbo中所有的注册信息都是通过URL的形式处理的)。