Java SPI 机制——项目模块化、插件化
Java SPI 机制
一、什么是 SPI?
Java SPI(Service Provider Interface)是 Java 官方为 实现解耦、实现可插拔组件 而提供的一套扩展机制。其核心思想:
接口由框架定义,实现由第三方提供,运行时动态加载指定实现。
SPI 主要用于:
- JDBC Driver 动态加载
- 日志框架 SLF4J 动态绑定
- Dubbo/Spring 等框架插件加载
- 加密算法、序列化算法的动态选择
SPI 在 JDK 中由 java.util.ServiceLoader 实现。
二、SPI 的核心原理
SPI 工作流程:
- 接口定义(服务接口)
- 在 META-INF/services 目录下创建配置文件
- 文件名:接口全限定类名
- 内容:实现类的全限定名(可多行)
- 使用 ServiceLoader 加载
- 读取 META-INF/services 文件
- 实例化实现类
- 按需选择实现执行
ServiceLoader 实现了按需加载、迭代查找和缓存机制。
三、SPI 机制完整示例
下面我们写一个从零开始的完整案例。
1. 定义 SPI 接口
public interface HelloService {
String sayHello(String name);
}
2. 创建两个实现类
public class EnglishHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello, " + name;
}
}
public class ChineseHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "你好," + name;
}
}
3. 在 META-INF/services 添加配置
目录结构:
resources
└── META-INF
└── services
└── com.example.spi.HelloService
文件内容:
com.example.spi.EnglishHelloService
com.example.spi.ChineseHelloService
4. 使用 ServiceLoader 加载 SPI
public class SpiDemo {
public static void main(String[] args) {
ServiceLoader<HelloService> services = ServiceLoader.load(HelloService.class);
for (HelloService service : services) {
System.out.println(service.sayHello("Java"));
}
}
}
运行输出:
Hello, Java
你好,Java
5. 按名称选择实现(扩展)
项目中通常需要根据外部配置选择实现,例如 application.properties 中:
hello.lang=cn
实现:
public class HelloServiceFactory {
private static final Map<String, HelloService> SERVICE_MAP = new HashMap<>();
static {
ServiceLoader<HelloService> services = ServiceLoader.load(HelloService.class);
for (HelloService service : services) {
SERVICE_MAP.put(service.getClass().getSimpleName(), service);
}
}
public static HelloService get(String key) {
return SERVICE_MAP.get(key);
}
}
使用:
HelloService service = HelloServiceFactory.get("ChineseHelloService");
System.out.println(service.sayHello("世界"));
四、Java SPI 的优缺点
优点
- 去掉代码硬编码依赖
- 易扩展、可插拔
- 框架化开发常用,约定优于配置
- 允许第三方自动接入
缺点
- 加载机制比较原始:全部加载、无法按需筛选
- 只依赖文件机制,不够灵活
- 无优先级管理
- 对类加载器、JDK 模块化不友好
为此,很多框架(如 Dubbo、Spring)扩展了自己的 SPI 体系。
五、如何把现有项目改造成 SPI 机制(最佳实践)
以下为真实项目改造步骤(可直接应用在微服务、工具包、平台 SDK 中)。
📌 目标
把“写死的实现类”改造成“可插拔的扩展点”。
例如:
OrderValidator validator = new NormalOrderValidator(); // 强耦合!!
改造成 SPI:
OrderValidator validator = ServiceLoader.load(OrderValidator.class).iterator().next();
Step 1:抽象接口(定义扩展点)
把具体逻辑改成接口:
public interface OrderValidator {
boolean validate(Order order);
}
Step 2:拆分模块(推荐放到 API 模块)
创建两个模块:
order-api ---> 存放接口和 SPI 配置
order-impl ---> 一个或多个实现
其中 order-api 中包含:
- OrderValidator 接口
- META-INF/services 配置
Step 3:在实现模块新增 SPI 实现
例如:
public class NormalOrderValidator implements OrderValidator {
@Override
public boolean validate(Order order) {
return order.getAmount() > 0;
}
}
并在:
order-api/resources/META-INF/services/com.demo.OrderValidator
写入:
com.demo.impl.NormalOrderValidator
Step 4:使用 ServiceLoader 动态加载
封装一个工厂:
public class OrderValidatorFactory {
private static volatile OrderValidator instance;
public static OrderValidator get() {
if (instance == null) {
synchronized (OrderValidatorFactory.class) {
if (instance == null) {
ServiceLoader<OrderValidator> loader = ServiceLoader.load(OrderValidator.class);
instance = loader.iterator().next();
}
}
}
return instance;
}
}
业务代码:
OrderValidator validator = OrderValidatorFactory.get();
validator.validate(order);
Step 5:支持多个插件并按需选择
SPI 支持多实现,但需要我们自己区分。
最佳做法:给实现类加 @SPIName 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPIName {
String value();
}
实现类:
@SPIName("normal")
public class NormalOrderValidator implements OrderValidator {...}
工厂类升级:
public class OrderValidatorFactory {
private static final Map<String, OrderValidator> CACHE = new HashMap<>();
static {
ServiceLoader<OrderValidator> loader = ServiceLoader.load(OrderValidator.class);
for (OrderValidator validator : loader) {
SPIName name = validator.getClass().getAnnotation(SPIName.class);
CACHE.put(name.value(), validator);
}
}
public static OrderValidator get(String type) {
return CACHE.get(type);
}
}
这样项目就完全模块化、插件化了。
六、改造项目为 SPI 的最佳实战建议
1. 扩展点接口一定要放在独立模块(API 模块)
避免实现模块互相依赖。
2. 不要在实现模块创建 META-INF/services
应该放在接口所在的模块(API 模块)。
3. 建议为每个实现增加唯一 ID 注解(如 Dubbo 的 @SPI)
否则 SPI 无法支持多实现选择。
4. 尽量封装自己的 ExtensionLoader 工具类
JDK 的 ServiceLoader 太原始,需要补充:
- 缓存
- 日志
- 异常提示
- 默认实现
- 选用策略
5. 插件机制尽量支持热插拔(JAR 放进去即可生效)
这非常适合:
- 数据质量规则扩展
- 支付方式扩展
- 认证方式扩展
- 通知渠道扩展(短信/邮件/钉钉)
七、总结
Java SPI 是最轻量、最标准、最易维护的插件化方案,可以让你的项目:
- 实现完全解耦
- 支持多个实现动态接入
- 运行时根据配置选择实现
- 像 JDBC 驱动一样自动发现插件
八 、SPI 与 Spring @Conditional 条件装配对比与注意事项
一、SPI 与 @Conditional 的本质区别
- SPI(Service Provider Interface) 是 JDK 原生提供的动态扩展机制,通过扫描
META-INF/services来加载实现类,适合构建插件体系。 - @Conditional(Spring 条件装配) 是 Spring 容器级别的 Bean 装配控制,用于根据环境、配置、依赖条件来决定是否注册某个 Bean。
功能对比
| 维度 | Java SPI | Spring @Conditional |
|---|---|---|
| 效果 | 发现第三方扩展、插件化机制 | 根据条件控制 Bean 装配 |
| 场景 | 框架扩展点、插件发现(如 JDBC、Dubbo) | Spring Boot 自动配置、环境差异 Bean |
| 加载方式 | 扫描并实例化所有实现类 | 满足条件的 Bean 才会生成 |
| 可插拔程度 | 高,真正可热插拔(加 JAR 即可) | 中,对应用内部的组件选择 |
二、SPI 的注意事项(重点)
SPI 默认会实例化所有实现类,这是使用时必须注意的最大坑。
1. 全量加载问题
只要通过 ServiceLoader 遍历扩展,所有插件都会被实例化,包括:
- 调用构造函数
- 执行 static 静态代码块
- 执行任何副作用逻辑(如连接服务器、加载配置)
2. 可能导致的问题
- 启动慢
- 不使用的插件仍然执行初始化
- 插件初始化相互影响(如 AD、OAuth 都被初始化)
- 插件报错导致整个系统启动失败
3. 建议的安全使用方式(增强版 SPI)
- 不要在构造函数、static 块执行副作用逻辑
- 扫描 SPI 时只登记插件,不实例化插件
- 通过插件工厂按需实例化:
for (AuthService s : loader) {
registry.put(s.getAuthType(), s.getClass());
}
- 延迟加载(Lazy Initialization)
- 为插件定义显式的 init() 生命周期,避免构造时执行初始化
三、在插件化系统中的推荐组合
在企业级工程中:
- 用 SPI 做插件发现(扩展点),支持第三方丢 JAR 即可接入
- 用 @Conditional 配置插件的启用策略,例如:
@ConditionalOnProperty(name = "auth.type", havingValue = "ad")
这样既可以自动发现插件,也可通过配置和环境控制具体使用哪一个,实现“插件化 + Spring Boot 条件装配”的最佳结合方案。
更多推荐


所有评论(0)