定义 SPI = Service Producer Interface
常常会把SPI和API进行对比,他俩之间的区别一言以蔽之就是:
API暴露功能给外部用
SPI是定义规范给外部扩展
JDK SPI Demo 下面我们快速看一个demo
假设我们正在开发一个电商平台。在用户下单后,需要调用支付服务进行付款。平台初期可能只支持“支付宝”,但未来需要方便地扩展,以支持“微信支付”、“信用卡支付”等,而不希望每次新增支付方式都去修改核心订单业务的代码 。
结构规划 为了清晰地展示解耦,我们把项目(逻辑上)分为四个模块:
payment-api:服务接口模块 。定义支付服务的标准接口 PaymentService。
alipay-provider:支付宝实现模块 。提供 PaymentService 的支付宝实现。
wechatpay-provider:微信支付实现模块 。提供 PaymentService 的微信支付实现。
order-system:服务消费模块 。模拟订单系统,它只依赖 payment-api,并使用 ServiceLoader 来发现并使用所有可用的支付实现。
代码实现
payment-api 模块 (定义服务标准)
这是所有模块都依赖的核心契约。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.math.BigDecimal;public interface PaymentService { void pay (String orderId, BigDecimal amount) ; }
alipay-provider 模块 (第一个服务实现)
实现了支付宝支付的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.example.payment.api.PaymentService;import java.math.BigDecimal;public class AlipayService implements PaymentService { 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 “服务发现”的核心
wechatpay-provider 模块 (第二个服务实现)
实现了微信支付的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.example.payment.api.PaymentService;import java.math.BigDecimal;public class WeChatPayService implements PaymentService { public WeChatPayService () {} @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.wechat.WeChatPayService
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 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) { String orderId = UUID.randomUUID().toString(); BigDecimal amount = new BigDecimal ("199.99" ); System.out.println("订单系统启动,正在加载所有可用的支付服务..." ); System.out.println("----------------------------------------" ); ServiceLoader<PaymentService> services = ServiceLoader.load(PaymentService.class); if (!services.iterator().hasNext()) { System.out.println("错误:未找到任何可用的支付服务提供商!请检查类路径。" ); return ; } 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 { static { try { java.sql.DriverManager.registerDriver(new Driver ()); } catch (SQLException E) { throw new RuntimeException ("Can't register driver!" ); } } public Driver () throws SQLException { } }
然后在/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)的情况下,对功能进行扩展。