引言
最近在重新学习Java基础,学习到SPI相关知识时发现大部分文章并没能描述清楚其核心意义及思想,于是结合自己的感悟梳理了一篇相对全面、易懂的SPI文章,仅供大家学习参考
一、概念
SPI全称:Service Provider Interface,即服务提供接口。是Java官方提供的一种服务发现机制,它允许在程序运行时动态地加载实现特定接口的实现类,极大的增强了程序的灵活性、降低了耦合性。
一套完整的逻辑闭环的SPI简单来说就是服务方提供某一个服务的接口, 提供给服务开发者或者服务生产商来进行实现。场景举例:JDBC与MySql厂商、Oracle厂商,JDBC就是服务方,提供了java.sql.Driver接口,MySql厂商与Oracle厂商来分别实现此接口,以提供自家的数据库驱动程序;在此过程中,JDBC的开发者不关注java.sql.Driver接口的具体实现,他们仅需与java.sql.Driver接口交互即可。而我们在引用JDBC包的时候,就可以直接使用到Driver的实现类
二、原理
思考:在上述过程中,JDBC的Driver实现类是由别人提供的,也就是说此实现类并不能存在于JDBC的src目录下,那么JDBC的项目是如何加载到这个实现的呢?
具体实现逻辑如下图:
由图可知,除开JDBC提供接口、数据库厂商实现接口这两个环节之外,还有一个非常重要的动作将这两个环节融合到一起,即上图中标黄的元素:ServiceLoader。至此,我们认识到了SPI中的所有角色,分别是:
1.Service:一个公开的接口,或定义了一个抽象的功能模块
2.ServiceProvider:Service接口的实现类
3.ServiceLoader:SPI机制中的核心组件,负责在程序运行过程中发现并加载ServiceProvider
三、实例
模拟需求:有一家公司A,它需要接入互联网。它定义了一个连接网络的API,由中国移动和中国联通提供网络服务。此场景下共涉及三个角色,如下图所示:
在此需求中spi-core项目中的ClassName接口即为SPI的Service
spi-moblie、spi-unicom项目分别实现spi-core中的ClassName接口
同时ServiceProvider项目的resource目录下需要添加一个配置文件,文件格式如图:简单来说就是需要让ServiceLoader清楚的知道需要加载哪个接口,此接口又有哪些实现类(当实现类为多个时,用回车分割多个全路径名即可)
至此,一套简易的SPI就搭建完成了,当我们在程序中依赖了spi-unicom时,调用如下代码即可访问到spi-unicom对spi-core的实现
package com.example.web.test;
import com.example.spicore.core.ClassName;
import java.util.ServiceLoader;
/**
* @author cui
* @version 1.0.0
* @ClassName RunName.java
* @Description
* @createTime 2023年09月18日 17:35:00
*/
public class RunName {
public static void main(String[] args) {
ServiceLoader<ClassName> classNames = ServiceLoader.load(ClassName.class);
for (ClassName className : classNames) {
className.soutName();
}
}
}
输出结果如下:
注:SPI的三大要素:
1.规范的配置文件:
·文件路径:必须在Jar包的META-INF/service目录下
·文件名称:必须为service接口的全限定名
·文件路径:必须为ServiceProvider类的全限定名,如果有多个实现类,那么每一个 实现类在文件中单独占一行,用回车符分割即可
2.ServiceProvider必须具备无参的构造方法:
ServiceProvider的实例化是通过反射技术实现的,所以必须要有无参构造器
3.保证能加载到配置文件和ServiceProvider类:
常用的方式是将ServiceProvider的Jar包放到classpath路径下;还可以将jar包安装到JRE的扩展目录中(不建议修改JRE扩展目录中的文件);或者自定义一个class Loader
总结
JavaSPI提供了一种组件发现和注册的方式,可以用于实现各种插件,或者零或替换框架所使用的组件,它面向接口编程,优雅的实现模块之间的解耦,其根本设计思想为:“面向接口”+“配置文件”+“反射技术”
SPI的本质是一种程序的扩展思路,程序将接口的具体实现交由服务开发者或者服务生产商来进行实现,从而保证程序与程序之间高度灵活的同时,耦合度并不高。