Java|SPI

定义

SPI = Service Producer Interface

常常会把SPI和API进行对比,他俩之间的区别一言以蔽之就是:

  • API暴露功能给外部
  • SPI是定义规范给外部扩展

JDK SPI

Demo

下面我们快速看一个demo

假设我们正在开发一个电商平台。在用户下单后,需要调用支付服务进行付款。平台初期可能只支持“支付宝”,但未来需要方便地扩展,以支持“微信支付”、“信用卡支付”等,而不希望每次新增支付方式都去修改核心订单业务的代码

结构规划

为了清晰地展示解耦,我们把项目(逻辑上)分为四个模块:

  1. payment-api服务接口模块。定义支付服务的标准接口 PaymentService
  2. alipay-provider支付宝实现模块。提供 PaymentService 的支付宝实现。
  3. wechatpay-provider微信支付实现模块。提供 PaymentService 的微信支付实现。
  4. order-system服务消费模块。模拟订单系统,它只依赖 payment-api,并使用 ServiceLoader 来发现并使用所有可用的支付实现。

代码实现

  1. payment-api 模块 (定义服务标准)

这是所有模块都依赖的核心契约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package: com.example.payment.api

import java.math.BigDecimal;

/**
* 支付服务的标准接口 (Service Interface)
*/
public interface PaymentService {

/**
* 处理支付请求
* @param orderId 订单ID
* @param amount 支付金额
*/
void pay(String orderId, BigDecimal amount);

}
  1. alipay-provider 模块 (第一个服务实现)

实现了支付宝支付的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// package: com.example.payment.alipay

import com.example.payment.api.PaymentService;
import java.math.BigDecimal;

/**
* 支付宝支付实现 (Service Provider Implementation)
*/
public class AlipayService implements PaymentService {

// SPI要求实现类必须有一个公共的、无参的构造函数
public AlipayService() {}

@Override
public void pay(String orderId, BigDecimal amount) {
System.out.printf("【支付宝】接收到订单 [%s] 的支付请求,金额:¥ %s%n", orderId, amount);
// ... 此处省略调用支付宝网关的复杂逻辑
System.out.println("【支付宝】支付成功!");
}
}

关键配置 (SPI核心)

在该模块的 src/main/resources 目录下,创建 META-INF/services/ 文件夹,并创建一个文件,文件名是接口的全限定名

  • 文件名: com.example.payment.api.PaymentService
  • 文件内容: com.example.payment.alipay.AlipayService (实现类的全限定名)

这一步是实现 SPI “服务发现”的核心

  1. wechatpay-provider 模块 (第二个服务实现)

实现了微信支付的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// package: com.example.payment.wechat

import com.example.payment.api.PaymentService;
import java.math.BigDecimal;

/**
* 微信支付实现 (Service Provider Implementation)
*/
public class WeChatPayService implements PaymentService {

public WeChatPayService() {}

@Override
public void pay(String orderId, BigDecimal amount) {
System.out.printf("【微信支付】接收到订单 [%s] 的支付请求,金额:¥ %s%n", orderId, amount);
// ... 此处省略调用微信支付API的复杂逻辑
System.out.println("【微信支付】支付成功!");
}
}

还是需要SPI的配置

同样,在该模块的 src/main/resources/META-INF/services/ 目录下创建同名文件:

  • 文件名: com.example.payment.api.PaymentService
  • 文件内容: com.example.payment.wechat.WeChatPayService
  1. order-system 模块 (服务消费者)

这是我们的主应用,它只知道(依赖) PaymentService 接口,不知道任何具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// package: com.example.order

import com.example.payment.api.PaymentService;
import java.math.BigDecimal;
import java.util.ServiceLoader;
import java.util.UUID;

public class OrderApplication {

public static void main(String[] args) {
// 1. 模拟一个订单
String orderId = UUID.randomUUID().toString();
BigDecimal amount = new BigDecimal("199.99");

System.out.println("订单系统启动,正在加载所有可用的支付服务...");
System.out.println("----------------------------------------");

// 2. 使用 ServiceLoader 动态发现并加载所有 PaymentService 的实现
ServiceLoader<PaymentService> services = ServiceLoader.load(PaymentService.class);

// 3. 检查是否找到了任何支付服务
if (!services.iterator().hasNext()) {
System.out.println("错误:未找到任何可用的支付服务提供商!请检查类路径。");
return;
}

// 4. 遍历所有找到的服务并执行支付 (Java 8+ Stream API 风格)
// 在实际业务中,可能会根据用户选择调用其中一个
services.forEach(service -> {
System.out.println("发现支付服务: " + service.getClass().getSimpleName());
service.pay(orderId, amount);
System.out.println("----------------------------------------");
});

System.out.println("所有支付流程演示完毕。");
}
}

运行效果

OrderApplication 运行时,即使它的代码里没有一行 new AlipayService()new WeChatPayService()ServiceLoader 也会扫描 classpath,找到 META-INF/services/ 下的配置文件,并自动加载和实例化这两个实现类。

1
2
3
4
5
6
7
8
9
10
11
订单系统启动,正在加载所有可用的支付服务...
----------------------------------------
发现支付服务: AlipayService
【支付宝】接收到订单 [some-uuid-string] 的支付请求,金额:¥ 199.99
【支付宝】支付成功!
----------------------------------------
发现支付服务: WeChatPayService
【微信支付】接收到订单 [some-uuid-string] 的支付请求,金额:¥ 199.99
【微信支付】支付成功!
----------------------------------------
所有支付流程演示完毕。

SQL Driver

在JDK的源码中,一个很常见的SPI机制的应用就是JDBC Driver的实现。

JDK本身只定义了规范,也就是Connection相关的接口,具体如何获取数据以及数据字段类型这些不同数据库厂商的实现细节都放在了不同的驱动jar中。

不同数据库厂商实现的驱动jar中,都会有一个实现了java.sql.Driver的实现类。

以MySQL为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.mysql.cj.jdbc;

import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// Register ourselves with the DriverManager.
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance().
}
}

然后在/resources/META-INF/services中有一个名为java.sql.Driver的文件,内容是:

1
com.mysql.cj.jdbc.Driver

实际上不止MySQL的驱动是保持这样的结构,所有JDBC的驱动都需要保持这样的结构。

当我们调用DriverManager.getConnection(jdbcUrl, username, password)时,就是通过ServiceLoader.load(java.sql.Driver.class)来加载所有classpath下的JDBC驱动包。

我们没有明确指定使用哪个数据库厂商的JDBC驱动程序,因为DriverManager会自动为我们选择合适的驱动程序:JDK内部都扫描了一次,但是只是有一个驱动能满足我们输入的 jdbcUrl。

所以这也就是为什么我们只引入了驱动的jar包,但是能够直接通过JDBC的接口开箱即用。

SPI这种灵活的机制能够保证不修改代码(比如上面的例子中就是JDBC暴露的DriverManager.getConnection)的情况下,对功能进行扩展。


Java|SPI
http://example.com/2025/10/30/Java-SPI/
作者
Noctis64
发布于
2025年10月30日
许可协议