什么是SPI
介绍
再聊下一个类加载器框架OSGI之前,我们首先学习一下前驱知识SPI
全称:Service Provider Interface
区别于API模式,本质是一种服务接口规范定义权的转移,从服务提供者转移到服务消费者。
怎么理解呢?
API指:服务提供方定义接口规范并按照接口规范完成服务具体实现,消费者需要遵守提供者的规则约束,否则无法消费
SPI指:由服务消费方定义接口规范,服务提供者需要按照消费者定义的规范完成具体实现。否则无法消费。而SPI则是一种callback的思想
所谓callback的思想呢,就是我想让别人跑我的代码,我又不能改别人的代码
SPI从理论上看,是一种接口定义和实现解耦的设计思路,以便于框架的简化和抽象;从实际看,是让服务提供者把接口规范定义权交岀去,至于交给谁是不一定的。这个定义权可以是服务消费者,也可以是任何一个第三方。一旦接口规范定义以后,只有消费者和服务提供者都遵循接口定义,才能匹配消费。
很多人有一些误解,说API就是传统老旧的开发方式和生产关系,SPI就代表了一种新的、更加优秀的架构模式。
其实不是的,两种技术,不明确场景的情况下,没有优劣之分。之所以会产生不同于原有的技术方案,是为了填补原有方案的空缺,而不是取而代之。
两者唯一的差别,在于服务提供者和服务消费者谁更加强势,仅此而已。
举个不恰当的例子:A国是C国工业制成品的消费国,C国只能提供相比A国更具性价比的产品,担心生产的产品会无法在A国销售。这时候,生产者必须遵守A国的生产标准。
谁有主动权,谁就有标准的制定权。在系统架构层面:谁是沉淀通用能力的平台方,谁就是主动权一方。
作用场景
可以定义好一个接口,并且提供多种实现,然后服务调用方可以按照需求调用特定的实现类
太抽象了不好理解没关系,这里用人人都用过的一个例子JDBC 中就用过 SPI 机制
public static Connection getConnection() {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
try {
conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
System.out.println(conn);
} catch (SQLException e) {
System.out.println("数据库连接失败");
}
} catch (ClassNotFoundException e) {
//System.out.println("驱动包不存在");
}
return conn;
}
其实不写注册驱动这一步也可以
public static Connection getConnection() {
Connection conn = null;
//try {
//Class.forName("com.mysql.jdbc.Driver");
try {
conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);
System.out.println(conn);
} catch (SQLException e) {
System.out.println("数据库连接失败");
}
//} catch (ClassNotFoundException e) {
//System.out.println("驱动包不存在");
//}
return conn;
}
为什么 JDBC 中不注册驱动还能连接数据库成功?就是因为SPI机制
文件 META-INF/services/java.sql.Driver
内容如下
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
读取这个内容和代码中Class.forName(“com.mysql.jdbc.Driver”);这一句是等价的
是不是有服务提供者需要按照消费者定义的规范完成具体实现这个味了,同理,oracle的jdbc驱动包这个文件对应的内容是
oracle.jdbc.OracleDriver
这样做有什么好处呢?就是调用方按照自己定义的规范调用服务提供方就行,不需要在意提供方实现细节。
那么在实际生产环境中SPI会有哪些应用场景呢?
例如:跟中台对接的ABC多个业务链路可以用自己行业定制的实现方式
比如假设我做电商网站的退货,标准退货流程中的验货节点
如果是快消行业 货品货值比较低,退货发个地址让客户自行寄回就行,验货节点默认空实现就行
如果是家装行业 货品货值高,而且搬运不便,就要实现工人师傅上门验货,在验货节点就要实现诸如派发工单上门的操作
如果是3c数码行业 货品货值高,而且有可能被换配件或者掉包,那么在验货的时候就需要核对机器sn码,正确的话才能寄回
在中台遇到诸如此类的同一节点不同操作,那么就可以使用SPI机制做不同业务逻辑了,这个思路的集大成者就是实现了SPI机制的OSGI框架的nbf实现。
三种SPI
Java SPI
Java SPI 是JDK内置的一种服务提供发现机制。 我们一般希望模块直接基于接口编程,调用服务不直接硬编码具体的实现,而是通过为某个接口寻找服务实现的机制,通过它就可以实现,不修改原来jar的情况下, 为 API 新增一种实现。这有点类似 IOC 的思想,将装配的控制权移到了程序之外。 对于 Java 原生 SPI,只需要满足下面几个条件:
1定义服务的通用接口,针对通用的服务接口,提供具体的实现类
2在 src/main/resources/META-INF/services 或者 jar包的 META-INF/services/ 目录中,新建一个文件,文件名为 接口的全名。 文件内容为该接口的具体实现类的全名
3将 spi 所在 jar 放在主程序的 classpath 中
4服务调用方用java.util.ServiceLoader,用服务接口为参数,去动态加载具体的实现类到JVM中,然后就可以正常使用服务了
上面这一大段代码示例如下
1.接口和实现类
接口
public interface DemoService {
void sayHello();
}
实现类
public class RedService implements DemoService{
@Override
public void sayHello() {
System.out.println("red");
}
}
public class BlueService implements DemoService{
@Override
public void sayHello() {
System.out.println("blue");
}
}
2.配置文件
META-INF/services文件夹下,路径名字一定分毫不差写对,配置文件名com.example.demo.spi.DemoService
文件内容
com.example.demo.spi.RedService
com.example.demo.spi.BlueService
3.jar包例如jdbc的需要导入classpath,我们这个示例程序自己写的代码就不用了
4.实际调用
public class ServiceMain {
public static void main(String[] args) {
ServiceLoader<DemoService> spiLoader = ServiceLoader.load(DemoService.class);
Iterator<DemoService> iteratorSpi = spiLoader.iterator();
while (iteratorSpi.hasNext()) {
DemoService demoService = iteratorSpi.next();
demoService.sayHello();
}
}
}
调用结果
red
blue
项目如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzFUbbsN-1669573629440)(https://raw.githubusercontent.com/13884566853/eck-article-imgs/main/img/%E6%9F%90%E6%96%87%E7%AB%A0image-20221128020510796.png)]
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
Java SPI的缺陷
实际上 Java 的 SPI 机制往往被扩展,因为 ServiceLoader 缺少一些有用的特性:
缺少实例的维护,ServiceLoader 每次 load 后,都会生成一份实例,也就是 prototype 无法获取指定的实例,ServiceLoader不像 Spring,只能一次获取所有的接口实例 不支持排序,随着新的实例加入,会出现排序不稳定的情况,作用域没有定义singleton和prototype的定义,不利于用户自由定制
DubboSPI
Dubbo的SPI主要改进了JDK标准的SPI实现:
JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
允许设置别名
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。
允许用户扩展实现Filter接口。
基本上讲到这里大家对于SPI可能有个大致的认识,但是要真正理解Dubbo的SPI,还是要仔细看一下源码才可以。
https://www.jianshu.com/p/344c00f8f550https://www.jianshu.com/p/344c00f8f550
别名
@SPI 注解的 value 属性,还可以默认一个“别名”的实现。比如在Dubbo 中,默认的是Dubbo 私有协议:dubbo protocol - dubbo:// 来看看Dubbo中协议的接口:
@SPI("dubbo")
public interface Protocol {
//
}
在 Protocol 接口上,增加了一个 @SPI 注解,而注解的 value 值为 Dubbo ,通过 SPI 获取实现时就会获取 Protocol SPI 配置中别名为dubbo的那个实现,com.alibaba.dubbo.rpc.Protocol文件如下:
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper mock=com.alibaba.dubbo.rpc.support.MockProtocol dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol com.alibaba.dubbo.rpc.protocol.http.HttpProtocol com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol registry=com.alibaba.dubbo.registry.integration.RegistryProtocol qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper
然后只需要通过getDefaultExtension,就可以获取到 @SPI 注解上value对应的那个扩展实现了
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension(); //protocol: DubboProtocol
还有一个 Adaptive 的机制,虽然非常灵活,但……用法并不是很“优雅”,这里就不介绍了
Dubbo 的 SPI 中还有一个“加载优先级”,优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载,如果遇到重复就跳过不会加载了。
所以如果想靠classpath加载顺序去覆盖内置的扩展,也是个不太理智的做法,原因同上 - 加载顺序不严谨
Spring SPI
Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:
//获取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
Spring Boot 中 spring.factories 的配置加上常用的mybatis-plus的包路径,Spring就会自动扫描并将mybatis的bean加载进ioc容器中
如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个META-INF/spring.factories文件,只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的 spring.factories 文件然后修改 比如我只想添加一个新的 LoggingSystemFactory 实现,那么我只需要新建一个META-INF/spring.factories文件,而不是完整的复制+修改:
org.springframework.boot.logging.LoggingSystemFactory=\com.example.log4j2demo.Log4J2LoggingSystem.Factory