- Java常用机制 - SPI机制详解
- 简单介绍SPI工作流程
- SPI实现代码示例
- 步骤 1:定义服务接口
- 步骤 2:提供具体实现(由不同厂商提供)
- 步骤 3:创建配置文件
- 步骤 4:使用 ServiceLoader 发现并调用服务
- 输出可能为:
- 需要SPI的情况
- 个人总结
- 1. 你的角色:框架/平台开发者
- 2. 用户的角色:扩展开发者
- 3. 运行时效果
- ServiceLoader的扫描范围
- 问题:
- 解答:
- 1. ServiceLoader的扫描范围
- 2. ServiceLoader的工作机制
- ServiceLoader.load()的执行步骤:
- 步骤1:确定要扫描的配置文件名称
- 步骤2:扫描整个ClassPath中所有JAR包和目录
- 步骤3:找到用户JAR包中的配置文件
- 步骤4:加载并实例化实现类
- 关键的技术原理
- 原理1:统一的ClassPath
- 原理2:ClassLoader的全局视图
- 原理3:资源查找不关心来源
- 总结
- 框架方、扩展方
Java常用机制 - SPI机制详解
Java常用机制 - SPI机制详解 | Java 全栈知识体系
(6 封私信) Java SPI思想梳理 - 知乎
简单介绍SPI工作流程
-
框架方:定义规则(接口)。
-
框架方:加载接口的实现(ServiceLoader.load()),并调度使用。
-
扩展方:实现接口,写好业务逻辑,并配置SPI配置文件()。
- SPI配置文件:JAR 包的
META-INF/services/
目录下,创建一个以服务接口全限定名命名的文件(例如com.example.DatabaseDriver
) - SPI配置文件内容:逐行写下实现类的全限定名(例如
com.mysql.jdbc.MySQLDriver
)。
- SPI配置文件:JAR 包的
SPI实现代码示例
假设我们有一个数据库驱动的 SPI。
步骤 1:定义服务接口
// 文件:com.example.DatabaseDriver.java
package com.example;public interface DatabaseDriver {String connect(String url);String query(String sql);
}
步骤 2:提供具体实现(由不同厂商提供)
MySQL 实现
// 文件:com.mysql.cj.jdbc.MysqlDriver.java (在 mysql-connector-java.jar 中)
package com.mysql.cj.jdbc;import com.example.DatabaseDriver;public class MysqlDriver implements DatabaseDriver {@Overridepublic String connect(String url) {return "连接到 MySQL: " + url;}@Overridepublic String query(String sql) {return "执行 MySQL 查询: " + sql;}
}
PostgreSQL 实现
// 文件:org.postgresql.Driver.java (在 postgresql.jar 中)
package org.postgresql;import com.example.DatabaseDriver;public class Driver implements DatabaseDriver {@Overridepublic String connect(String url) {return "连接到 PostgreSQL: " + url;}@Overridepublic String query(String sql) {return "执行 PostgreSQL 查询: " + sql;}
}
步骤 3:创建配置文件
在 mysql-connector-java.jar
中,创建文件:
META-INF/services/com.example.DatabaseDriver
文件内容:
com.mysql.cj.jdbc.MysqlDriver
在 postgresql.jar
中,创建文件:
META-INF/services/com.example.DatabaseDriver
文件内容:
org.postgresql.Driver
步骤 4:使用 ServiceLoader 发现并调用服务
// 文件:MainApplication.java
import com.example.DatabaseDriver;
import java.util.ServiceLoader;public class MainApplication {public static void main(String[] args) {// 加载所有 DatabaseDriver 的实现ServiceLoader<DatabaseDriver> drivers = ServiceLoader.load(DatabaseDriver.class);// 遍历并调用所有找到的实现for (DatabaseDriver driver : drivers) {System.out.println(driver.connect("jdbc:mysql://localhost:3306/test"));System.out.println(driver.query("SELECT * FROM users"));System.out.println("---");}}
}
输出可能为:
连接到 MySQL: jdbc:mysql://localhost:3306/test
执行 MySQL 查询: SELECT * FROM users
---
连接到 PostgreSQL: jdbc:mysql://localhost:3306/test
执行 PostgreSQL 查询: SELECT * FROM users
---
┌─────────────────┐ 实现 ┌───────────────────┐
│ Java JDK │ ◄──────────────── │ mysql-connector-java │
│ │ │ JAR │
│ ├─────────────┤ │ │ ├─────────────────┤ │
│ │DriverManager│ │ │ │Driver(MySQL实现)│ │
│ │ 类 │ │ │ │ │ │
│ ├─────────────┤ │ │ ├─────────────────┤ │
│ │ 调用 │ │ │ │ META-INF/services│ │
│ │ServiceLoader│ │ │ │ /java.sql.Driver │ │
│ └─────────────┘ │ │ └─────────────────┘ │
│ ├─────────────┤ │ └───────────────────┘
│ │Driver接口 │ │
│ │(java.sql) │ │
│ └─────────────┘ │
└─────────────────┘▲│ 发现和加载
┌─────────────────┐
│ 你的应用程序 │
│ │
│ DriverManager. │
│ getConnection() │
└─────────────────┘
需要SPI的情况
在正常的业务开发中,确实很少需要SPI。它是一个面向框架开发者而非业务开发者的工具。
对于90%的Java开发者来说:
- 如果你主要做业务系统开发:用Spring的依赖注入就够了
- 如果你主要做中间件/框架开发:才需要认真考虑SPI,只有当你的代码要被很多其他团队复用,并且你希望他们能无侵入地扩展功能时,才需要考虑SPI。
需要SPI的情况(满足以下所有条件):
- ✅ 我是框架/平台开发者,不是业务开发者
- ✅ 我希望其他人能在不修改我代码的情况下扩展功能
- ✅ 扩展的实现应该独立部署(不同的JAR包)
- ✅ 我需要运行时自动发现所有可用实现
- ✅ 接口相对稳定,不会频繁变更
个人总结
相当于我开发了一个程序,jar包里定义好结构,即创建好的接口interface,然后jar包里直接使用ServiceLoader.load(接口.class)来获取对象,此时是没有实现类的,获取的应该是空。然后别人拿到我们发布的jar包后,他们去实现interface,配置对应的类到META-INF/services/,这样ServiceLoader.load(接口.class)就能获取到他们写好的对象。
SPI就是:
"我定义规矩,你来实现,我来调用,互不干扰"
让我用更清晰的流程来总结你说的这个完美场景:
1. 你的角色:框架/平台开发者
你发布的JAR包结构:
my-framework.jar
├── com/
│ └── yourcompany/
│ └── framework/
│ ├── YourFramework.class # 主框架类
│ └── spi/ # 定义的SPI接口
│ └── DataProcessor.class # 接口定义
└── META-INF/└── MANIFEST.MF
你的框架代码:
// 1. 定义SPI接口(在你的框架JAR中)
package com.yourcompany.framework.spi;public interface DataProcessor {String process(String data);boolean supports(String type);
}// 2. 框架中使用ServiceLoader加载实现(在你的框架JAR中)
package com.yourcompany.framework;import com.yourcompany.framework.spi.DataProcessor;
import java.util.ServiceLoader;public class YourFramework {public void processData(String type, String data) {// 关键点:这里加载时,你的JAR包里没有实现类!ServiceLoader<DataProcessor> processors = ServiceLoader.load(DataProcessor.class);boolean found = false;for (DataProcessor processor : processors) {if (processor.supports(type)) {String result = processor.process(data);System.out.println("处理结果: " + result);found = true;break;}}if (!found) {System.out.println("没有找到支持类型 '" + type + "' 的处理器");}}
}
此时,如果用户只用你的框架JAR包:
YourFramework framework = new YourFramework();
framework.processData("json", "test data");
// 输出:没有找到支持类型 'json' 的处理器
// 因为ServiceLoader.load()返回空的迭代器
2. 用户的角色:扩展开发者
用户实现的JAR包结构:
user-json-plugin.jar
├── com/
│ └── usercompany/
│ └── plugins/
│ └── JsonDataProcessor.class # 实现类
└── META-INF/└── services/└── com.yourcompany.framework.spi.DataProcessor # SPI配置文件
用户的实现代码:
// 用户实现你的接口
package com.usercompany.plugins;import com.yourcompany.framework.spi.DataProcessor;public class JsonDataProcessor implements DataProcessor {@Overridepublic String process(String data) {return "{\"processed\": \"" + data + "\", \"format\": \"json\"}";}@Overridepublic boolean supports(String type) {return "json".equalsIgnoreCase(type);}
}
用户的SPI配置文件:
在 META-INF/services/com.yourcompany.framework.spi.DataProcessor
文件中:
com.usercompany.plugins.JsonDataProcessor
3. 运行时效果
classpath包含:
classpath/
├── my-framework.jar # 你的框架(只有接口)
└── user-json-plugin.jar # 用户的实现
运行结果:
YourFramework framework = new YourFramework();
framework.processData("json", "hello world");
// 输出:处理结果: {"processed": "hello world", "format": "json"}
ServiceLoader的扫描范围
问题:
-
放在扩展开发者的SPI配置文件,为什么我发布的JAR包用ServiceLoader.load,能读取?
-
mysql-connector-java包中实现java.sql.Driver 接口且配置在它的META-INF/services/目录下,为什么jdk包里面使用ServiceLoader.load(Driver.class) 能获取 mysql-connector-java包实现的东西?
解答:
关键点: ServiceLoader.load()
扫描的是整个应用的ClassPath,而不仅仅是调用者所在的JAR包。
1. ServiceLoader的扫描范围
运行时ClassPath结构:
classpath/
├── my-framework.jar # 你的框架JAR(包含ServiceLoader调用)
│ ├── com/yourcompany/framework/YourFramework.class
│ └── com/yourcompany/framework/spi/DataProcessor.class
│
├── user-json-plugin.jar # 用户的插件JAR
│ ├── com/usercompany/plugins/JsonDataProcessor.class
│ └── META-INF/services/com.yourcompany.framework.spi.DataProcessor
│
└── user-xml-plugin.jar # 另一个用户的插件JAR ├── com/othercompany/plugins/XmlDataProcessor.class└── META-INF/services/com.yourcompany.framework.spi.DataProcessor
2. ServiceLoader的工作机制
ServiceLoader.load()的执行步骤:
// 当你的框架代码执行这行时:
ServiceLoader<DataProcessor> loader = ServiceLoader.load(DataProcessor.class);// ServiceLoader内部实际上做了这些事情:
步骤1:确定要扫描的配置文件名称
String configFile = "META-INF/services/" + DataProcessor.class.getName();
// 得到:META-INF/services/com.yourcompany.framework.spi.DataProcessor
步骤2:扫描整个ClassPath中所有JAR包和目录
// 遍历classpath中的每一个位置:
// 1. my-framework.jar
// 2. user-json-plugin.jar
// 3. user-xml-plugin.jar
// 4. 其他所有在classpath中的JAR和目录// 在每个位置中查找:META-INF/services/com.yourcompany.framework.spi.DataProcessor
步骤3:找到用户JAR包中的配置文件
// 在 user-json-plugin.jar 中找到:
// META-INF/services/com.yourcompany.framework.spi.DataProcessor
// 文件内容:com.usercompany.plugins.JsonDataProcessor// 在 user-xml-plugin.jar 中找到:
// META-INF/services/com.yourcompany.framework.spi.DataProcessor
// 文件内容:com.othercompany.plugins.XmlDataProcessor
步骤4:加载并实例化实现类
// 使用当前线程的ClassLoader来加载类
ClassLoader cl = Thread.currentThread().getContextClassLoader();// 加载:com.usercompany.plugins.JsonDataProcessor
Class<?> jsonClass = cl.loadClass("com.usercompany.plugins.JsonDataProcessor");
DataProcessor jsonProcessor = (DataProcessor) jsonClass.newInstance();// 加载:com.othercompany.plugins.XmlDataProcessor
Class<?> xmlClass = cl.loadClass("com.othercompany.plugins.XmlDataProcessor");
DataProcessor xmlProcessor = (DataProcessor) xmlClass.newInstance();
关键的技术原理
原理1:统一的ClassPath
Java应用运行时,所有JAR包都被合并到一个统一的ClassPath中:
// 比如启动命令:
java -cp "my-framework.jar:user-json-plugin.jar:user-xml-plugin.jar" MainClass// 这三个JAR包在运行时是平等的,ServiceLoader可以看到所有JAR的内容
原理2:ClassLoader的全局视图
// ServiceLoader使用ClassLoader来查找资源
ClassLoader cl = Thread.currentThread().getContextClassLoader();// 这个ClassLoader能看到classpath中所有JAR包的META-INF/services/目录
Enumeration<java.net.URL> resources = cl.getResources(configFile);
// 这会返回所有JAR包中匹配的配置文件URL
原理3:资源查找不关心来源
ServiceLoader只关心:"在当前的ClassPath中,有哪些地方存在这个配置文件?" 它不关心这个配置文件是在哪个JAR包里。
总结
为什么你的框架能读取用户的配置文件?
因为:
- ClassPath是统一的 - 所有JAR包在运行时是平等的
- ServiceLoader扫描整个ClassPath - 不局限于调用者所在的JAR包
- ClassLoader提供全局视图 - 能看到所有JAR包中的资源文件
- 基于约定的发现机制 - 只要按照约定放置配置文件,就能被自动发现
这就是SPI的"魔法"所在——解耦的发现机制。你的框架不需要知道扩展在哪里,扩展也不需要知道框架在哪里,只要它们都在ClassPath中,就能自动连接起来。
框架方、扩展方
理论上是在我发布的JAR包用ServiceLoader.load?其实扩展开发者也可以使用ServiceLoader.load?
// 从技术上讲,确实都可以: // 在你的框架JAR中(推荐) ServiceLoader.load(DataProcessor.class); // ✅ 框架发现扩展// 在用户的扩展JAR中(技术上可行,但通常不推荐) ServiceLoader.load(DataProcessor.class); // ⚠️ 扩展发现其他扩展?
答:理论上双方都可以使用ServiceLoader.load,但这通常不是好的设计
职责总结表
职责 | 框架方 | 扩展方 |
---|---|---|
定义接口 | ✅ 负责 | ❌ 不负责 |
实现接口 | ❌ 通常不实现 | ✅ 负责 |
调用 ServiceLoader.load() | ✅ 负责发现扩展 | ❌ 不应该调用 |
创建SPI配置文件 | ❌ 不创建 | ✅ 必须创建 |
提供具体业务逻辑 | ❌ 不提供 | ✅ 负责提供 |
管理扩展生命周期 | ✅ 负责 | ❌ 不负责 |
框架方:
- 定义规则(接口)
- 发现扩展(ServiceLoader.load())
- 调度使用(根据条件调用合适的扩展)
扩展方:
- 遵守规则(实现接口)
- 声明存在(SPI配置文件)
- 专注实现(写好业务逻辑)