网络上优秀的SPI机制解析很多,会在文末推荐
这篇文章主要是介绍一下“报表/数据导出格式扩展”场景下的SPI机制应用。 强烈建议下载源码后阅读
代码地址:https://gitee.com/lcdzzz/spi-export-demo
介绍
SPI(Service Provider Interface)机制是Java提供的一种服务发现与动态加载的标准化方式。其核心思想是**“面向接口编程 + 策略模式 + 配置文件”**
简单来说,SPI定义了一个服务接口,而具体的实现则由第三方提供。程序通过读取配置文件来找到并加载这些实现,无需在代码中硬编码具体实现类。
工作原理
- 定义接口:服务方定义一个标准的接口。
- 提供实现:第三方(服务提供者)提供该接口的具体实现,并在其
META-INF/services/目录下创建一个以接口全限定名命名的文件。文件内容是该接口具体实现类的全限定名(每行一个)。
- 加载服务:服务方通过
java.util.ServiceLoader工具类来扫描、加载并实例化配置文件中声明的所有实现类。
SPI 与 API 的核心区别
| 对比维度 |
API (Application Programming Interface) |
SPI (Service Provider Interface) |
| 定义方 |
实现方定义接口和实现,提供给调用方使用。 |
调用方定义接口,由实现方来提供具体实现。 |
| 控制权 |
接口和实现的控制权在实现方。 |
接口的控制权在调用方,实现的控制权在第三方。 |
| 目的 |
提供明确的功能,让开发者调用。 |
提供扩展点,让框架或平台能被定制和扩展。 |
| 关系方向 |
实现方 -> 调用方 |
调用方 <- 实现方 |
SPI是一种强大的解耦和扩展机制,它让程序的架构更加灵活,实现了“面向接口编程”和“开闭原则”,是许多优秀框架可扩展性的基石。
应用场景
报表/数据导出格式扩展
系统需要将数据导出为Excel、PDF、Word、CSV等多种格式。
- SPI应用:定义
ExportService接口。每种导出格式作为一个SPI实现。用户在前端选择“导出为PDF”时,后端根据格式参数,通过SPI找到对应的实现类执行导出。
- 优势:方便地增加新的导出格式(如导出为图片),不会影响已有的导出功能。
实现
项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| spi-export-demo/ ├── pom.xml ├── export-api/ │ └── src/main/java/com/example/export/ │ ├── ExportData.java │ └── spi/ExportService.java ├── export-excel/ ├── export-pdf/ ├── export-csv/ └── web-app/ └── src/main/java/com/example/export/ ├── ExportApplication.java ├── controller/ExportController.java └── factory/ExportServiceFactory.java
|
根据上面提到的SPI机制的实现原理,本项目的实现可以分为以下几步
SPI机制实现步骤
步骤1:定义接口
在 export-api 模块中,我们定义了标准的 ExportService 接口:
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
|
public interface ExportService {
String getFormat();
String getDescription();
void export(ExportData data, OutputStream outputStream) throws Exception;
String getContentType();
String getFileExtension(); }
|
步骤 2:提供实现
在各个导出模块中,我们提供了 ExportService 接口的具体实现:

Excel导出实现(其余同理)
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
| public class ExcelExportService implements ExportService { @Override public String getFormat() { return "excel"; } @Override public String getDescription() { return "Microsoft Excel (.xlsx)"; } @Override public String getContentType() { return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; } @Override public String getFileExtension() { return "xlsx"; } @Override public void export(ExportData data, OutputStream outputStream) throws Exception {...} private CellStyle createHeaderStyle(Workbook workbook) {...} }
|
创建 SPI 配置文件
在每个导出模块的 META-INF/services/ 目录下,我们创建了以接口全限定名命名的文件:
Excel 模块配置文件 (其余同理):
- 文件路径:
export-excel/src/main/resources/META-INF/services/com.example.export.spi.ExportService
- 文件内容:
com.example.export.excel.ExcelExportService
这些配置文件告诉 Java 运行时哪些类实现了 ExportService 接口。
步骤 3:加载服务
在 web-app 模块中,我们通过 ExportServiceFactory 类使用 ServiceLoader 来加载所有实现:
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @Component public class ExportServiceFactory { private final Map<String, ExportService> exportServiceMap = new HashMap<>();
@PostConstruct public void init() { ServiceLoader<ExportService> loader = ServiceLoader.load(ExportService.class); for (ExportService service : loader) { String format = service.getFormat().toLowerCase(); if (exportServiceMap.containsKey(format)) { System.err.println("警告:导出格式 " + format + " 有多个实现,使用 " + service.getClass().getName()); } exportServiceMap.put(format, service); } System.out.println("加载的导出服务:" + exportServiceMap.keySet()); }
public ExportService getExportService(String format) { ExportService service = exportServiceMap.get(format.toLowerCase()); if (service == null) { throw new IllegalArgumentException("不支持的导出格式: " + format + ",可用格式: " + getSupportedFormats()); } return service; }
public List<String> getSupportedFormats() { return new ArrayList<>(exportServiceMap.keySet()); }
public Map<String, String> getFormatDescriptions() { Map<String, String> descriptions = new LinkedHashMap<>(); exportServiceMap.forEach((format, service) -> { descriptions.put(format, service.getDescription()); }); return descriptions; } }
|
当应用启动时, @PostConstruct 注解标记的 init() 方法会被执行:
创建 ServiceLoader<ExportService> 实例
调用 ServiceLoader.load(接口.class)
- 定位配置文件:扫描类路径下所有
META-INF/services/com.example.export.spi.ExportService文件
- 读取实现类名: 读取文件中每一行的全限定类名(如
com.example.export.excel.ExcelExportService)
- 延迟加载: 创建
ServiceLoader实例,但不立即实例化实现类(懒加载)
- 提供服务: 返回一个可迭代的
ServiceLoader对象,用于按需加载实现类
底层机制流程图如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 调用 ServiceLoader.load(接口.class) ↓ 扫描类路径下的 META-INF/services/接口全限定名 文件 ↓ 读取文件中所有实现类的全限定名 ↓ 为每个实现类创建 LazyClassPathLookupIterator ↓ 当调用 iterator().next() 或 for-each 循环时 ↓ 通过反射 Class.forName() 加载类 ↓ 调用无参构造器 newInstance() 创建实例 ↓ 缓存实例,避免重复创建 ↓ 返回实例给调用者
|
遍历加载所有实现类
将实现类缓存到 exportServiceMap 中,以格式名称为键
运行时效果
当应用启动时,控制台会输出:
1
| 加载的导出服务:[excel, pdf, csv]
|
这表明 SPI 机制成功加载了所有导出服务实现。
如何使用SPI服务
在 ExportController 中,我们通过 ExportServiceFactory 获取具体的导出服务:
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
| @RestController @RequestMapping("/api/export") public class ExportController { @Autowired private ExportServiceFactory exportServiceFactory; @PostMapping("/execute") public void exportData( @RequestParam String format, @RequestBody ExportRequest request, HttpServletResponse response) throws IOException { try { ExportService exportService = exportServiceFactory.getExportService(format); } catch (Exception e) { } } }
|
假如要扩展新的导出格式
要添加新的导出格式(如 Word),只需:
- 创建新的 Maven 模块 export-word
- 实现 ExportService 接口
- 在 src/main/resources/META-INF/services/ 目录下创建配置文件
- 在 web-app/pom.xml 中添加新模块的依赖
无需修改任何现有代码,新的导出格式会被自动加载。
参考资料: Java SPI 机制详解 | JavaGuide
Java常用机制–SPI机制详解一、什么是SPI机制 SPI(Service Provider Interface), - 掘金