Java SPI 实现插件化扩展只需要三步:定义一个接口,在实现模块的 META-INF/services/ 目录下放一个以「接口全限定名」命名的文本文件,再用 java.util.ServiceLoader.load(接口.class) 加载。整个过程不需要修改调用方一行代码,新增插件就是新增一个 jar 包。
Java SPI 的实现步骤是什么?
结论:SPI(Service Provider Interface,服务提供者接口)是 JDK 内置的服务发现机制,从 Java 6 起由 java.util.ServiceLoader 正式提供。
第一步,定义接口,放在调用方能依赖的模块里:
public interface PayChannel {
String name();
void pay(long amount);
}
第二步,在实现模块的 src/main/resources/META-INF/services/ 下新建文件,文件名必须是接口的全限定名 com.example.PayChannel,内容为实现类全限定名,一行一个:
com.example.alipay.AlipayChannel
com.example.wechat.WechatChannel
第三步,调用方加载:
ServiceLoader<PayChannel> loader = ServiceLoader.load(PayChannel.class);
for (PayChannel channel : loader) {
System.out.println(channel.name());
}
注意:实现类必须是 public 且带无参构造器,否则加载时抛 ServiceConfigurationError。
ServiceLoader 是懒加载还是提前实例化?
结论:ServiceLoader.load() 本身不实例化任何对象,只在第一次迭代时逐个 Class.forName + newInstance。
load() 返回的是一个实现了 Iterable 的懒迭代器,每次 next() 才创建实例,loader.reload() 可清空缓存重新扫描。所以「加载所有插件很慢」这个说法要分场景:只取第一个实现时,后面的类不会被初始化。Java 9 之后还可以用 loader.stream() 拿到 Provider,配合 findFirst() 短路。
原生 SPI 有哪些坑,怎么绕?
结论:JDK 原生 SPI 有 5 个硬伤——只能全量遍历、无名称索引、无优先级、无依赖注入、不支持条件激活。
- 无法按名字取实现。
ServiceLoader 没有 getExtension("alipay") 这种 API,只能遍历后自己判断。
- 顺序不可控。迭代顺序取决于 classpath 顺序和配置文件内的行顺序,没有
@Order 之类的机制。
- 无法选择性加载。想按配置只启用其中一个,也得先全部实例化再筛选。
- 没有依赖注入。实现类由反射构造,拿不到 Spring 容器里的 Bean。
- 接口与实现必须对同一个类加载器可见。默认用的是线程上下文类加载器
Thread.currentThread().getContextClassLoader(),在 Tomcat、OSGi 这类多类加载器环境下容易踩坑,建议显式传入 ServiceLoader.load(PayChannel.class, myClassLoader)。
绕开办法有两种:一是自己写一个 ExtensionLoader,把配置格式从「一行一个类名」改成 name=className(如 alipay=com.example.AlipayChannel),加载时解析成 Map<String, Class>,就能实现按名取用、单例缓存;二是直接用成熟方案,Dubbo 的 ExtensionLoader 就是这么做的,它支持 @SPI("dubbo") 指定默认实现、@Adaptive 自适应扩展、@Activate 自动激活,配置目录为 META-INF/dubbo/、META-INF/dubbo/internal/ 和 META-INF/services/ 三处。
配置文件格式还有哪些硬性要求?
结论:路径必须精确到 META-INF/services/接口全限定名,内容必须 UTF-8 编码,# 开头为注释行。
文件名不能带 .properties 后缀,也不能只写简单类名。Java 9 引入模块化后,还可以在 module-info.java 里用 provides com.example.PayChannel with com.example.AlipayChannel; 声明,效果等价于配置文件,两者可共存。
哪些知名项目在用 SPI?
结论:JDBC 从 4.0 规范起就靠 SPI 自动注册驱动,DriverManager 会扫描 META-INF/services/java.sql.Driver,所以 Class.forName 加载驱动早已不是必需。
此外,SLF4J 用 SPI 绑定具体日志实现(logback、log4j2 各提供一个 provider),Spring Boot 的自动配置在 2.7 之前依赖 META-INF/spring.factories,3.0 起改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,Dubbo、ShardingSphere 也都基于自研的 ExtensionLoader。可以说,Java 生态里所有「一个接口、多套实现、运行时选一个」的场景,背后基本都是 SPI。
总结:接口 + META-INF/services/ 配置文件 + ServiceLoader.load() 是 Java SPI 的最小可用组合,三步即可实现插件化;原生实现的短板集中在按名取用、顺序控制和类加载器三处,生产环境建议改用 name=className 格式的自研 ExtensionLoader 或 Dubbo 的扩展机制。