Java SPI 详解:从面向接口到插件化扩展机制
一、SPI 是什么
SPI 全称是:
Service Provider Interface
翻译过来就是:
服务提供者接口。
它是 Java 提供的一种服务发现机制。
简单来说:
SPI 允许我们先定义一个统一接口,然后由不同的实现方提供具体实现,最终由程序在运行时动态发现并加载这些实现,而不是在代码中把具体实现写死。
例如,我们定义一个支付接口:
public interface PayService {
void pay(int amount);
}
然后分别提供支付宝和微信支付实现:
public class AlipayService implements PayService {
@Override
public void pay(int amount) {
System.out.println("支付宝支付:" + amount);
}
}
public class WechatPayService implements PayService {
@Override
public void pay(int amount) {
System.out.println("微信支付:" + amount);
}
}
普通写法可能是:
PayService payService = new AlipayService();
这种方式虽然使用了接口,但具体实现依然被写死。
如果未来需要增加:
UnionPayService
代码往往还需要修改。
而 SPI 的核心思想是:
系统只依赖 PayService
具体有哪些实现
由外部提供
程序运行时
动态发现这些实现
所以 SPI 可以理解为:
接口
+
实现
+
注册
+
运行时发现
二、SPI 和面向接口编程是什么关系
很多人第一次看到 SPI 都会问:
这不就是面向接口编程吗?
确实非常像,但并不完全一样。
面向接口编程解决的问题是:
调用方依赖抽象,而不是依赖具体实现。
例如:
PayService payService = new AlipayService();
这里:
PayService
是接口。
所以已经属于面向接口编程。
但是:
new AlipayService()
仍然明确依赖了具体实现。
也就是说:
调用者
↓
知道 PayService
↓
也知道 AlipayService
而 SPI 更进一步:
调用者
↓
只知道 PayService
↓
不知道有哪些实现
↓
运行时动态发现
因此:
面向接口编程强调的是“依赖抽象”。
而:
SPI 强调的是“实现可以被动态发现”。
三、SPI 和工厂模式有什么区别
SPI 和工厂模式也非常容易混淆。
例如我们使用一个简单工厂:
public class PayFactory {
public static PayService getPayService(String type) {
if ("alipay".equals(type)) {
return new AlipayService();
}
if ("wechat".equals(type)) {
return new WechatPayService();
}
throw new IllegalArgumentException("未知支付类型");
}
}
调用方:
PayService payService =
PayFactory.getPayService("alipay");
这种情况下,调用方确实不知道具体实现。
但是:
PayFactory
知道。
也就是说:
Main
↓
PayFactory
↓
AlipayService
WechatPayService
UnionPayService
如果未来增加:
UnionPayService
我们通常还要修改:
PayFactory
而 SPI 不一样。
SPI 是:
Main
↓
PayService
↓
ServiceLoader
↓
配置文件
↓
发现实现
核心代码不需要提前知道:
AlipayService
WechatPayService
UnionPayService
因此可以这么总结:
| 技术 | 核心解决的问题 |
|---|---|
| 面向接口 | 依赖抽象还是具体类 |
| 工厂模式 | 谁负责创建对象 |
| SPI | 如何发现有哪些实现 |
| IOC | 谁负责管理对象生命周期 |
| 插件机制 | 如何让外部模块接入系统 |
一句比较容易记的话:
工厂是“代码告诉系统有哪些实现”。
而 SPI 是:
“运行环境告诉系统有哪些实现”。
四、SPI 最适合解决什么问题
SPI 最适合的场景通常具有几个明显特点。
第一:
系统存在一个稳定的抽象接口。
例如:
Serializer
Driver
Plugin
PayService
StorageService
SmsProvider
第二:
具体实现可能有很多种。
例如:
Serializer
├── JsonSerializer
├── ProtobufSerializer
├── KryoSerializer
└── HessianSerializer
第三:
未来还可能不断新增实现。
最关键的是第四点:
核心系统不希望每增加一个实现,就修改一次自己的代码。
所以 SPI 的典型场景就是:
扩展点
+
插件化
+
第三方实现
+
动态发现
五、一个生活中的 SPI 类比
SPI 可以类比成 USB。
电脑厂商定义:
USB 标准
但是电脑厂商并不知道未来会出现什么设备。
可能是:
鼠标
键盘
U盘
摄像头
移动硬盘
游戏手柄
只要设备符合 USB 标准,就能够接入系统。
对应到 Java:
USB 标准
≈
Java Interface
USB 设备
≈
Interface Implementation
设备插入电脑
≈
Provider 加入 classpath
操作系统识别设备
≈
ServiceLoader
这就是 SPI 最核心的思想:
我先定义扩展标准,至于未来谁来实现,不需要提前知道。
六、Java SPI 的具体实现
Java 原生 SPI 的核心类是:
java.util.ServiceLoader
整个实现可以拆成四步。
1. 定义接口
例如:
package com.demo.spi;
public interface PayService {
void pay(int amount);
}
这个接口就是 SPI 的:
Service
也就是扩展规范。
2. 编写实现类
支付宝:
package com.demo.spi.impl;
import com.demo.spi.PayService;
public class AlipayService implements PayService {
@Override
public void pay(int amount) {
System.out.println("支付宝支付:" + amount + " 元");
}
}
微信:
package com.demo.spi.impl;
import com.demo.spi.PayService;
public class WechatPayService implements PayService {
@Override
public void pay(int amount) {
System.out.println("微信支付:" + amount + " 元");
}
}
这两个类属于:
Provider
3. 注册 Provider
在:
src/main/resources
下面创建:
META-INF/services
最终目录:
src
└── main
├── java
│ └── com
│ └── demo
│ └── spi
│ ├── PayService.java
│ └── impl
│ ├── AlipayService.java
│ └── WechatPayService.java
│
└── resources
└── META-INF
└── services
└── com.demo.spi.PayService
这里非常重要。
文件名必须是:
接口的全限定类名
所以:
com.demo.spi.PayService
文件内容则是所有实现类的全限定类名:
com.demo.spi.impl.AlipayService
com.demo.spi.impl.WechatPayService
七、使用 ServiceLoader 加载实现
使用方式:
ServiceLoader<PayService> loader =
ServiceLoader.load(PayService.class);
for (PayService payService : loader) {
payService.pay(100);
}
运行结果:
支付宝支付:100 元
微信支付:100 元
整个过程可以理解为:
ServiceLoader.load(PayService.class)
↓
找到
META-INF/services/com.demo.spi.PayService
↓
读取配置
↓
AlipayService
WechatPayService
↓
加载 Class
↓
实例化对象
↓
返回给调用者
所以 Java SPI 的底层思想可以粗略理解成:
配置文件
+
ClassLoader
+
反射
+
面向接口
八、为什么说 SPI 是插件化机制
假设我们的支付系统已经上线。
核心系统只有:
PayService
现在有一家新的支付公司:
UnionPay
它希望接入我们的系统。
它只需要实现:
public class UnionPayService implements PayService {
@Override
public void pay(int amount) {
System.out.println("银联支付:" + amount);
}
}
然后在自己的 jar 中提供:
META-INF/services/com.demo.spi.PayService
内容:
com.unionpay.UnionPayService
最终:
union-pay-plugin.jar
被放到应用 classpath。
核心系统原来的代码:
ServiceLoader<PayService> loader =
ServiceLoader.load(PayService.class);
一行都不需要修改。
但是现在却可以发现:
AlipayService
WechatPayService
UnionPayService
这就是:
插件化扩展。
所以 SPI 最重要的价值并不是:
少写几个 if else
而是:
允许核心系统在不知道未来实现的情况下,让第三方提供新的扩展。
九、SPI 适合哪些具体业务
1. 数据库驱动
这是 Java 世界最经典的例子之一。
JDK 定义:
java.sql.Driver
MySQL 提供:
com.mysql.cj.jdbc.Driver
PostgreSQL 也提供自己的:
PostgreSQL Driver
Oracle 也一样。
于是:
JDK
↓
只定义 Driver 接口
数据库厂商
↓
自己提供 Driver 实现
JDK 不需要知道全世界有多少数据库。
现代 JDBC 驱动通常可以通过服务发现机制完成驱动注册。
十、序列化框架
假设我们开发 RPC 框架。
定义:
public interface Serializer {
byte[] serialize(Object obj);
Object deserialize(byte[] data);
}
别人可以实现:
JsonSerializer
ProtobufSerializer
KryoSerializer
HessianSerializer
RPC 框架核心代码只依赖:
Serializer
然后动态发现有哪些序列化实现。
这就是非常典型的 SPI 场景。
十一、RPC 框架
例如一个 RPC 框架内部可能存在很多扩展点:
Serialization
LoadBalance
Protocol
Registry
Cluster
Filter
Transport
假设负载均衡:
public interface LoadBalance {
Server select(List<Server> servers);
}
可以提供:
RandomLoadBalance
RoundRobinLoadBalance
LeastActiveLoadBalance
ConsistentHashLoadBalance
框架只定义:
LoadBalance
不同算法作为 Provider 接入。
Dubbo 就大量使用类似 SPI 的扩展机制。
不过 Dubbo SPI 并不是完全照搬 Java 原生 SPI,而是在它基础上做了大量增强。
十二、短信平台
比如公司内部需要统一短信服务。
定义:
public interface SmsProvider {
void send(String phone, String content);
}
可以有:
AliyunSmsProvider
TencentSmsProvider
HuaweiSmsProvider
TwilioSmsProvider
未来新增一家云服务商。
不修改核心代码,只增加新的 Provider。
非常适合插件化。
十三、对象存储
定义:
public interface StorageService {
void upload(String fileName, byte[] data);
}
实现可能有:
LocalStorage
AliyunOSS
TencentCOS
AmazonS3
MinIO
核心业务只调用:
StorageService
具体存到哪里,由具体 Provider 决定。
这种架构尤其适合 SaaS、多云平台、基础设施中间件。
十四、支付渠道
定义:
public interface PaymentProvider {
PayResult pay(PayRequest request);
}
实现:
AlipayProvider
WechatPayProvider
UnionPayProvider
PaypalProvider
StripeProvider
如果公司属于支付平台、聚合支付系统,这类业务特别适合扩展点设计。
不过如果只是一个普通电商项目,支付渠道全部由自己团队维护,很多时候 Spring Bean + 策略模式可能更加简单。
SPI 并不是任何“多实现接口”都应该使用。
十五、文件解析器
例如公司有一个数据导入平台。
接口:
public interface FileParser {
Object parse(byte[] data);
}
实现:
CsvParser
ExcelParser
JsonParser
XmlParser
PdfParser
未来第三方团队想支持:
Parquet
Avro
只需要新增实现,而不需要修改核心解析平台。
十六、规则引擎 / 风控系统
例如:
public interface RiskRule {
boolean check(Order order);
}
不同业务团队可以提供:
AmountRiskRule
IpRiskRule
DeviceRiskRule
FrequencyRiskRule
RegionRiskRule
如果规则由不同团队独立开发并动态接入,那么 SPI 或类似 Extension 机制就很合适。
十七、什么时候不建议使用 SPI
SPI 并不是万能的。
如果只是普通业务:
UserService
OrderService
ProductService
然后项目内部自己有几个实现类,通常不需要使用 SPI。
例如:
public interface DiscountService {
}
项目内部:
VipDiscountService
NormalDiscountService
如果这两个类都由同一个 Spring Boot 项目管理,那么直接:
Spring IOC
+
策略模式
通常已经足够。
例如:
@Component("vip")
public class VipDiscountService implements DiscountService {
}
然后:
@Autowired
private Map<String, DiscountService> discountServices;
完全没必要再搞:
META-INF/services
ServiceLoader
因为这并不是一个真正的:
第三方扩展
插件扩展
运行时发现
场景。
所以判断 SPI 是否合适,可以问自己:
这些实现是不是由核心系统之外的人提供?
如果答案是:
是
SPI 的价值通常比较高。
如果答案是:
不是,都是自己业务代码
那很可能:
Spring
策略模式
工厂模式
就够了。
十八、Java SPI 的优点
1. 解耦
核心模块只依赖:
interface
而不依赖具体 Provider。
2. 可扩展
增加实现时,可以不修改核心系统代码。
3. 插件化
第三方可以通过 jar 的方式提供扩展。
4. 符合开闭原则
开闭原则:
对扩展开放,对修改关闭。
新增 Provider:
新增类
新增 jar
新增配置
而不是修改已有核心代码。
十九、Java SPI 的缺点
Java 原生 SPI 也不是完美的。
1. 会加载所有实现
例如:
ServiceLoader<PayService> loader =
ServiceLoader.load(PayService.class);
遍历时:
for (PayService service : loader) {
}
可能把所有 Provider 都实例化出来。
但是很多情况下我们只想要:
某一个特定实现
原生 SPI 在这方面能力比较弱。
2. 不方便根据名字获取实现
例如我们希望:
getExtension("alipay")
直接得到:
AlipayService
Java 原生 SPI 并没有直接提供这种强大的名称映射机制。
所以一些框架会自己扩展 SPI。
例如 Dubbo SPI 就支持类似:
name -> implementation
的能力。
3. Provider 构造失败会影响加载
如果某个 Provider:
初始化失败
构造方法异常
依赖缺失
可能影响服务加载。
4. 依赖 ClassLoader
SPI 本身依赖 Java 类加载机制。
在复杂容器环境,例如:
Tomcat
OSGi
应用服务器
插件框架
如果 ClassLoader 层级比较复杂,可能出现:
Provider 明明存在
ServiceLoader 却找不到
这种问题。
二十、SPI 底层原理
Java SPI 的核心:
ServiceLoader.load(Xxx.class)
本质上会通过 ClassLoader 查找:
META-INF/services/
下面对应的配置文件。
例如:
ServiceLoader.load(PayService.class);
就需要查找:
META-INF/services/com.demo.spi.PayService
读取:
com.demo.spi.impl.AlipayService
com.demo.spi.impl.WechatPayService
然后通过类加载机制加载这些实现。
可以粗略理解为:
ClassLoader classLoader = ...;
Enumeration<URL> resources =
classLoader.getResources(
"META-INF/services/" +
PayService.class.getName()
);
然后解析文件中的实现类名称。
最终:
Class<?> clazz = Class.forName(className);
Object instance =
clazz.getDeclaredConstructor().newInstance();
真正源码细节会更复杂,但是理解 SPI 到这个层次已经足够抓住核心。
二十一、SPI、Spring IOC、工厂、策略模式如何选择
这是实际开发中特别重要的一点。
可以简单理解成:
如果实现都在自己项目内部:
Spring IOC
+
策略模式
如果对象创建逻辑复杂:
工厂模式
如果第三方需要扩展:
SPI
如果需要真正插件化:
SPI / Extension Framework
例如:
订单状态处理
优惠券策略
营销规则
这些基本属于自己业务。
通常:
Spring + Strategy
更合适。
而:
数据库 Driver
RPC 序列化
插件
第三方存储
第三方日志实现
这种扩展型场景:
SPI
更合适。
二十二、SPI 常见面试题
1. 什么是 Java SPI?
可以回答:
Java SPI 是一种服务发现机制。服务提供方实现约定接口,并通过
META-INF/services注册实现类,调用方通过ServiceLoader在运行时发现和加载这些实现,从而达到接口与具体实现解耦以及插件化扩展的目的。
2. SPI 和 API 有什么区别?
API 更倾向于:
调用方调用提供方
例如:
list.add();
我们调用 Java API。
SPI 更倾向于:
框架定义规范
第三方实现规范
框架反过来调用第三方实现
可以简单记成:
API:
我调用别人。
SPI:
别人按照我的规范实现,
然后我调用别人。
3. SPI 和工厂模式有什么区别?
工厂模式重点解决:
对象创建
SPI 重点解决:
实现发现
传统工厂通常提前知道:
A
B
C
而 SPI 可以在运行时发现:
未来新增的 D、E、F
4. SPI 和 Spring IOC 有什么区别?
Spring IOC:
容器管理 Bean
依赖注入
生命周期管理
作用域管理
AOP
SPI:
发现 Provider
加载 Provider
SPI 更轻量。
Spring 更偏:
对象管理框架
SPI 更偏:
扩展发现机制
5. Java SPI 的配置文件放在哪里?
答案:
META-INF/services/
文件名:
接口的全限定类名
文件内容:
实现类的全限定类名
6. Java SPI 的核心类是什么?
答案:
java.util.ServiceLoader
7. Java SPI 底层原理是什么?
主要涉及:
ServiceLoader
ClassLoader
META-INF/services
反射
整体流程:
ServiceLoader.load()
↓
ClassLoader 查找配置文件
↓
读取实现类名称
↓
ClassLoader 加载 Class
↓
创建 Provider
↓
返回调用者
8. Java SPI 为什么可以实现解耦?
因为调用方只依赖:
Service Interface
而不需要直接依赖:
Provider Implementation
Provider 可以独立打包成不同 jar。
新增实现时:
核心代码无需修改
9. Java SPI 有什么缺点?
可以回答:
Java 原生 SPI 功能比较基础,它在遍历时可能加载多个实现,也不方便按名称获取指定实现,同时依赖 ClassLoader,在复杂类加载环境下可能遇到 Provider 无法发现的问题。
10. JDBC 为什么会涉及 SPI?
JDK 定义:
java.sql.Driver
不同数据库厂商提供:
MySQL Driver
Oracle Driver
PostgreSQL Driver
数据库厂商可以通过服务注册机制暴露 Driver Provider。
所以 JDBC Driver 是非常经典的 SPI 使用案例。
二十三、面试进阶题:为什么 JDBC 以前要 Class.forName,现在不需要?
以前经常写:
Class.forName("com.mysql.jdbc.Driver");
目的是主动加载数据库 Driver。
Driver 类加载以后,会向:
DriverManager
注册。
现代 JDBC 驱动支持自动发现机制。
驱动 jar 可以注册:
META-INF/services/java.sql.Driver
因此:
DriverManager
可以发现对应 Driver。
所以很多现代 JDBC 程序已经不需要手写:
Class.forName(...)
二十四、面试进阶题:SPI 和策略模式是什么关系?
策略模式解决:
同一个行为存在多个算法,可以在运行时选择。
例如:
DiscountStrategy
├── VipDiscount
├── NormalDiscount
└── NewUserDiscount
SPI 解决:
这些实现是怎么被发现的。
所以完全可以:
SPI
+
Strategy
一起使用。
例如:
SPI
负责发现所有支付策略
Strategy
负责选择其中一个支付策略
两者并不冲突。
二十五、面试进阶题:SPI 是不是一定需要反射?
从使用者角度:
ServiceLoader.load()
并不需要自己写反射。
但从实现机制上:
根据字符串类名
加载 Class
创建 Provider
本质上依赖 Java 类加载和反射相关机制。
所以可以说:
SPI 的底层实现与 ClassLoader 和反射机制密切相关。
二十六、Java SPI 和 Dubbo SPI 的区别
Java SPI 比较简单:
ServiceLoader
+
META-INF/services
Dubbo SPI 在此基础上增强了很多能力,例如:
按名称获取扩展
IOC
AOP Wrapper
Adaptive Extension
扩展点自动激活
缓存
Java SPI 更像:
“这里有哪些实现?”
Dubbo SPI 更进一步解决:
“我要哪个实现?”
“什么时候加载?”
“怎么自动选择?”
“怎么包装扩展?”
因此 Dubbo SPI 更适合复杂框架内部扩展机制。
二十七、最终总结
如果只用一句话解释 Java SPI:
Java SPI 是一种基于接口、配置文件、ClassLoader 和 ServiceLoader 实现的运行时服务发现机制。
它解决的核心问题并不是:
如何写 interface
而是:
如何让未来未知的实现
在不修改核心代码的情况下
接入系统
所以:
面向接口
解决依赖抽象
工厂模式
解决对象创建
策略模式
解决行为选择
IOC
解决对象管理
SPI
解决实现发现和插件扩展
真正适合 SPI 的场景,一般都有明显的:
框架
驱动
插件
扩展点
第三方 Provider
可插拔组件
特征。
最经典的理解路线就是:
interface
↓
implements
↓
META-INF/services
↓
ServiceLoader
↓
ClassLoader
↓
Provider
↓
插件化
只要把这条链路真正理解,Java SPI 基本就吃透了。