Java SPI(Service Provider Interface)

目的

面向对象设计鼓励模块间基于接口而非具体实现编程,以降低模块间的耦合,遵循依赖倒置原则,并支持开闭原则(对扩展开放,对修改封闭)。然而,直接依赖具体实现会导致在替换实现时需要修改代码,违背了开闭原则。

Service Provider Interface(SPI)将服务接口具体的服务实现分离开来,将服务调用方服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方

image-20250812162647688

图下方SPI实例中接口与调用方紧密结合,而实现方可以是不同的提供方。

具体的实例:数据库加载驱动、日志接口等

使用

接口

定义功能接口,其中包含接口中提供的功能方法。

1
2
3
4
5
package top.chc.api;

public interface MyService {
String doService();
}

调用方使用

调用方引入上述接口,并通过ServiceLoader.load()寻找路径下所有实现该接口的实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
package top.chc.call;

import java.util.ServiceLoader;
import top.chc.api.MyService;

public class Main {
public static void main(String[] args) {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for(MyService myService : loader){
System.out.println(myService.doService());
}
}
}

以上便是调用方的全部代码了,后续不需要再修改上述所有代码,即调用方代码无需再修改。

实现方代码

实现方引入上述接口,并创建该接口的实现类。

1
2
3
4
5
6
7
8
9
10
11
package com.chc;

import top.chc.api.MyService;

public class MyServiceImpl implements MyService {
@Override
public String doService(){
return "service";
}
}

调用方配置

配置调用方工程的 resources/META-INF/services 文件夹,创建文件src/main/resources/META-INF/services/top.chc.api.MyService并写入实现类的权限定名。

  • 路径:resources/META-INF/services/
  • 文件名:接口的全限定名
  • 文件内容:实现类的全限定名(一个或多个,每行一个)
1
2
# 文件路径:src/main/resources/META-INF/services/top.chc.api.MyService
com.chc.MyServiceImpl

工作原理:

ServiceLoader 工作原理:

  1. 找到 META-INF/services/<接口全名> 文件
  2. 读取里面的每一行实现类名
  3. Class.forName() 加载这些类
  4. 通过无参构造方法 newInstance() 实例化

其中 接口类实现类 都必须放在调用方的 classpath 中。

代码实现

创建三个工程:

  • api项目:提供接口
  • achieve项目:提供实现类,其中包含api项目
  • use项目:调用类,其中包含api项目和achieve项目

实现代码:https://github.com/caohongchuan/java_spi_example