5176 字
约 17 分钟
5
Java SPI 详解:从面向接口到插件化扩展机制

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 基本就吃透了。

Java SPI 详解:从面向接口到插件化扩展机制
http://clxhxhhr.top/posts/407/
作者
clxstart
发布于
2026-09-02
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。
文章目录
目录