目录
一、什么是 SPI
SPI( Service Provider Interface ),是 JDK 内置的一种服务提供发现机制( JDK 本身就带着的一个功能)。SPI 允许服务提供者在运行时动态地将实现类注入到系统中,从而实现组件的可插拔性和扩展性。
通俗来说,就是我们定义了一套标准,别人去实现,然后在程序执行的时候,根据这个约定我们来发现其实现的具体的内容,来进行执行。
举个栗子:
要实现数据库的增删改查操作,但数据库由不同的公司或厂商去做,不同的公司、不同厂商 实现 CRUD 的方式是不同的。这样假如有一天要切换数据库,发现来自不同公司或厂商的数据库有不同的写法,对开发者来说是很麻烦的,不具备通用性了,因此就有了一种机制 ,提供一套接口,开发者直接使用接口里的方法实现对应功能,让不同厂商去实现这个接口,从而不再被动的去兼容数据库厂商,而是提供了一个规范 / 插件 / 发现机制 / 接口,让底层 / 下游的数据库厂商对接既定的接口,从而提供具体的实现,这就是 SPI 的一个作用。
--- 我是分割线---
【补充】所以 SPI 和动态代理完全不一样的,动态代理和反射比较类似,动态代理是反射的一种实现,动态代理只是“玩”自己里面的,“格局小”,而 SPI “玩”的是一种插件、是不同系统之间的对接,“格局更大”。
对于 SPI 机制而言,通常有三个角色:
- 服务接口( Service Interface ):定义一组接口或抽象类,表示一种服务或功能
- 服务提供者接口( Service Provider Interface ):是服务接口的具体实现,提供了服务的具体功能(相当于上述栗子的厂商)
- 服务加载器( Service Loader ):程序执行时,负责加载并实例化服务提供者,并将其注册到系统中,以供服务接口使用(有接口、有实现了,中间得有一个“运输者”,即服务加载器)
二、SPI 的使用场景
数据库驱动
不同的数据库厂商提供了自己的数据库驱动实现,这些实现都实现了同一个 JDBC 接口。JVM 在运行时可以动态加载适合的数据库驱动,使得开发者在不修改代码的情况下切换不同的数据库。
日志框架
日志框架也实现了该机制,例如 SLF4J (没有具体实现,就是一个接口)。开发者可以选择不同的日志实现(如 Logback、Log4j 等),并将其注入到日志框架中,从而灵活地切换日志实现。
SPI 框架
其他很多框架也使用了该技术,例如 Dubbo(高性能 RPC 框架,Remote Procedure Call 远程过程调用协议)中,它们允许开发者通过 SPI 方式定义和管理应用程序的组件和扩展。
三、SPI 实现
JDK SPI 实现
首先,我们定义一个服务接口 MessageService ,表示一种消息服务:
public interface MessageService {
void sendMessage(String message);
}
然后,我们定义两个实现类,分别表示不同的消息服务提供者:
public class EmailMessageService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
public class SmsMessageService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
【注意】以上提供的这两个具体的实现,是 jar 包,在类里面是不会直接去使用 jar 包的,我们使用的是上层的类,所以此时我们需要依靠 SPI 将实现与接口“拼”到一块,怎么拼?在 Java 里是固定的写法,往下看哈~
接下来,在 src/main/resources/META-INF/services 目录下,创建一个名为 spi.MessageService (包名 + 类名)的文件(注意:文件的全路径名与服务接口的全限定名一致),内容如下:
spi.EmailMessageService
spi.SmsMessageService
这个文件里列出了所有实现了 MessageService 接口的类的全限定名,每行一个。
最后,我们编写一个测试类,使用 ServiceLoader 来加载并实例化实现类,并调用其方法:
import java.util.ServiceLoader;
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<MessageService> loader = ServiceLoader.load(MessageService.class);
for (MessageService service : loader) {
service.sendMessage("Hello, SPI!");
}
}
【注意】在这部分代码里,完全没有使用子类,使用的始终都是最上层的类,最上层的类是定义接口的,是不关心底层类的具体实现的,因为不同厂商是有不同实现的,所以在调用和使用的时候用的都是上层类,那此时在调用方法的时候能执行的原因就是通过加载器找到了服务提供者和服务接口,将这两联系起来,然后进行调用即可。