SPI英文全称为Service Provider Interface,顾名思义,服务提供者接口,它是jdk提供给“服务提供厂商”或者“插件开发者”使用的接口;
在开发过程中,经常要用到第三方提供的SDK来完成一些业务扩展功能,比如调用第三方的发短信、图片验证码、人脸识别等等功能,但问题是,第三方SDK只是提供了标准的功能实现,某些场景下,开发者还想基于这些SDK做一些个性化的定制和扩展,那要怎么办呢?
于是,一些优秀的SDK就通过SPI的机制,将一些接口扩能开放出来,开发者就可以基于这些SPI接口做自身的业务扩展了;
总结一下SPI思想:在系统的各个模块中,往往有不同的实现方案,例如日志模块的方案、xml解析的方案等,为了在装载模块的时候不具体指明实现类,我们需要一种服务发现机制,java spi就提供这样一种机制。有点类似于IoC的思想,将服务装配的控制权移到程序之外,在模块化设计时尤其重要
Java 的SPI机制在很多框架,中间件等都有着广泛的使用,如springboot,Dubbo中均有采用,属于高级Java开发知识点,有必要掌握
下面用一张简图说明下SPI机制的原理
案例展示
案例业务背景:
从工程的结构来看,也是遵循SPI的服务规范的,即在resources目录下,创建一个指定名称的文件夹,将接口实现的全限定名放进去,那么客户端只需要依赖特定的SDK,然后通过 serviceLoader的方式即可加载到依赖的SDK的服务
客户端customer工程导入依赖
<dependencies>
<dependency>
<artifactId>service-common</artifactId>
<groupId>com.congge</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<artifactId>ali-pay</artifactId>
<groupId>com.congge</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<artifactId>wechat-pay</artifactId>
<groupId>com.congge</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
public class MainTest {
public static void main(String[] args) {
ServiceLoader<PayService> loader = ServiceLoader.load(PayService.class);
loader.forEach(payService ->{
System.out.println(payService);
payService.pay();
System.out.println("=======");
});
}
}
运行下这段客户端的测试程序
我们不妨来看看serviceLoader中的一段关键代码,即加载服务接口时,可以发现,该方法最终要去找接口的实现类所在jar包下的 “META-INF/services” 目录中的服务实现,如果找到了就能被加载和使用
SPI优点
SPI缺点
在小编的实际项目开发中,有这样一个需求,标准产品针对单点登录提供了多种实现,比如 基于cas方案,ldap方案,oauth2.0方案等,针对每种方案,提供了一套具体的实现,即封装成了各自的jar包
标准产品在出厂并在客户端安装的时候,会有一套默认的实现,即oauth2.0实现,但是客户方有时候有自己的一套,比如cas服务器,那么客户希望能够对接cas单点登录,这么以来,具体到项目在实际部署的时候,就需要现场做一些特定的参数配置,将标准实现切换为 cas的实现即可,那么问题来了,标准产品是如何根据参数配置做到的呢?
其实也很简单,就是使用了 serviceLoader机制,自动发现标准产品中能够加载到的所有单点登录实现,如果没有外部配置参数传入,则默认使用oauth2.0的实现,否则,将会采用外部参数传过来的那个实现。
可以说,dubbo框架是对spi使用的一个很好的例子,dubbo框架本身就是基于SPI规范做了更进一步的封装,从上面的优缺点分析中,我妈了解了原生的SPI在客户端选择服务的时候需要遍历所有的接口实现,比较浪费资源,而dubbo在此基础上有了更好的封装和实现,下面来了解下dubbo的SPI使用吧
Dubbo 的 SPI 规范是:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
Dubbo 的 SPI 举例
1、创建一个maven工程,并导入核心依赖
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
2、 定义 SPI 接口
比如这里有一个下单的接口,注意接口上需要加上@SPI 注解 ,注解里面的值可以填,也可以不用填,如果填,请注意和配置文件里面的key值名称保持一致,填写了的话,加载的时候,会默认找这个key对应的实现
@SPI("alipay")
public interface Order {
String way();
}
3、定义两个接口的实现类
public class AlipayOrder implements Order{
public String way() {
System.out.println("使用支付宝支付");
return "支付宝支付";
}
}
public class WechatOrder implements Order {
public String way() {
System.out.println("微信支付");
return "微信支付";
}
}
4、定义扩展类配置文件
alipay=com.congge.spi.AlipayOrder
wechat=com.congge.spi.WechatOrder
5、测试方法
@Test
public void test1(){
ExtensionLoader<Order> extensionLoader = ExtensionLoader.getExtensionLoader(Order.class);
Order alipay = extensionLoader.getExtension("alipay");
System.out.println(alipay.way());
Order wechat = extensionLoader.getExtension("wechat");
System.out.println(wechat.way());
}
如果不指定加载哪个,而且接口配置了默认值,这里只需要在getExtension中设置 “true”,就会自动加载默认的那个
在Dubbo源码中,很多地方会存在下面这样的三种代码,分别是自适应扩展点、指定名称的扩展点、激活扩展点,dubbo通过这些扩展的spi接口实现众多的插拔式功能
ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();
ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);
ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);
以dubbo源码中的Protocol 为例,对应dubbo源码中的rpc模块
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
上面那行代码就是dubbo里面大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态的根据配置去找到对应的实现类。如果你没有配置,那就走默认的实现类,即dubbo
我们知道,springboot框架相比spring,从配置文件上简化了不少,但简化的只是开发者看到的那些xml配置文件中的东西,其本质仍然未变,就算是少了xml配置文件,依旧在启动的时候,需要做配置的解析工作,如解析原来的数据库连接的xml配置文件中的内容加载到spring容器中
而springboot来说,很多看不到的配置文件,都是在容器启动过程中,自动将配置进行读取,解析和加载,而在这个过程中,我们不禁好奇,这些配置是存在哪里呢?这里就用到了SPI的思想,也就是涉及到springboot的自动装配过程
举例来说,springboot怎么知道启动时需要加载 DataSource这个数据库连接的bean对象呢?怎么知道要使用JdbcTemplate 还是Druid的连接呢?
在spingboot工程启动过程中,有很重要的一个工作,就是完成bean的自动装配过程,自动装配装配的是什么东西呢?简单来说就是:
其中第一步中的那些文件是什么呢?其实就是和dubbo或原生的spi规范中的那些 /META-INF 文件,只不过在springboot工程中,命名的格式和规范稍有不同
下面通过源码来看看springboot启动过程中是如何加载这些spi文件的吧
然后来到下面这里,重点关注setInitializers 这个方法,顾名思义,表示在启动过程中要做的一些初始化设置,那么要设置哪些东西呢?
在这个方法中,有一个方法getSpringFactoriesInstances,紧接着这个方法看进去
在该方法中需要重点关注这句代码,通过这句代码,将依赖包下的那些待装配的文件进行加载,说白了,就是加载classpath下的那些 spring.factory的文件里面的name
SpringFactoriesLoader.loadFactoryNames(type, classLoader)
那么问题是具体加载的是什么样的文件呢?不妨继续点进去看看,在SpringFactoriesLoader类的开头,有一个这样的路径,想必大家就猜到是什么了吧
也就是说,会去找以这样的名字结尾的文件,比如我们在依赖的jar包中,看到下面这一幕,在这个spring.factories中,会看到更多我们熟悉的配置
这样问题就很明白了,通过找到spring.factories的文件,然后解析出具体的类的完整的名称,然后再在:createSpringFactoriesInstances 这个方法中完成对这些 扩展的SPI接口实现类的初始化加载,即完成配的过程
沿着这个思路继续探究下去,相信感兴趣的同学对springboot中的这种类SPI的方式会有更深一层的理解,本篇到此结束,最后感谢观看!
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/zhangcongyi420/article/details/121883515
内容来源于网络,如有侵权,请联系作者删除!