为什么是 SPI

做框架开发时,经常需要让外部团队在不改源码的情况下插入自定义逻辑。Java 的 Service Provider Interface(SPI)正是解决这一痛点的经典方案:框架只暴露接口,具体实现由使用者提供,双方通过约定的加载机制解耦。

下图展示了 SPI 的角色关系:

框架接口  -->  服务提供者实现  -->  应用在运行时按需调用
   |                |                     |
API 定义       resources/META-INF/services  ServiceLoader 按需加载

框架负责定义接口与约定,提供者将实现类打包好,ServiceLoader 在运行时装配,两端互不感知彼此细节。

SPI 与 API 的区别

  • API:框架写好实现,并通过方法开放给调用方使用。
  • SPI:框架只制定契约,由外部提供实现,框架在运行时回调实现。

因此 SPI 更适合“我要在某个节点上让别人接管”的场景,比如数据库驱动、日志实现、路由策略等。

Java 里的落地步骤

  1. 定义接口:在框架模块中写出需要开放的契约。
  2. 制定注册文件:在 resources/META-INF/services 目录下,新建以接口全限定名命名的文件,内容是实现类的全限定名。
  3. 加载实现:使用 ServiceLoader.load(Interface.class) 遍历可用实现。
  4. 按需调用:框架可以根据配置挑选实现,或全部加载做聚合处理。

代码示例:

ServiceLoader<MyExtension> loader = ServiceLoader.load(MyExtension.class);
for (MyExtension extension : loader) {
    extension.handle(request);
}

这种方式天然支持多实现并存,也可以搭配权重、标签等策略做更细粒度的路由。

经验分享

  • 约定胜于配置:接口命名尽量描述清楚输入输出,让实现方知道该承担的责任。
  • 尽早校验实现:在框架启动阶段做初始化测试,确保缺失依赖时能及时报错。
  • 限制加载范围:按需使用自定义的 ClassLoader,避免整个应用 ClassPath 都被扫描,启动速度更可控。
  • 配合缓存:ServiceLoader 支持懒加载,常驻的服务实现可以二次封装成缓存,减少重复实例化。

适用与注意事项

SPI 并非银弹。它适用于以下场景:

  • 框架层希望允许合作方扩展特性,例如自定义数据库驱动或序列化方式。
  • 系统需要在部署后动态增删实现,而不改变主程序。
  • 需要保持核心模块的稳定性,将变化部分沉淀为插件。

但在以下情况下要谨慎:

  • 业务逻辑强耦合,调用方对实现类的生命周期有过多假设。
  • 对性能有极端要求,却没有对 ServiceLoader 做缓存和隔离处理。
  • 缺乏统一监控,出现问题时难以定位是哪一个实现导致。

小结

SPI 是一套成熟的可插拔扩展方案,能让框架与实现各司其职。明确契约、约束加载方式、补齐监控与测试,就能在保持主干稳定的同时,让外部团队自由扩展能力。