API 和 SPI 有什么区别?一篇讲透两者的设计思想

API 和 SPI 两个词都以 Interface 结尾,很容易被当成同一种东西。它们确实都是接口,但描述的是两个方向的关系:API 是我们去调用别人实现好的能力,SPI 是别人定好接口、我们来提供实现,等着在运行时被找出来。

两行代码就能看出区别。调用 API:

java 复制代码
List<String> list = new ArrayList<>();
list.add("a");

写一个 SPI 实现:

java 复制代码
public class EnglishGreeter implements Greeter {
    public String greet() { return "hello"; }
}

前者我们主动依赖 List、ArrayList;后者我们的类反过来依赖一个别人定好的 Greeter,至于谁会来调它,我们并不知道。


API

API(Application Programming Interface)是一组已经实现好的类和方法,写业务代码时直接调。String.format、Map.get、ObjectMapper.readValue 都是 API。

它的特点是调用方在编译期就知道要调什么。类名、方法名、参数类型全都看得见,IDE 能补全,写错了编译不过。库这边对使用者有明确的承诺:这个方法叫什么、收什么、返回什么、抛什么异常。

依赖方向是单边的:我们的代码 → 库的代码。

SPI

SPI(Service Provider Interface)把方向掉过来了。接口由使用方(通常是 JDK 或者框架)定义,实现由第三方 jar 提供,使用方在运行时才去找有哪些实现。

最典型的是 java.sql.Driver。这个接口在 JDK 里,DriverManager 也是 JDK 写的,但它们都不知道 MySQL、PostgreSQL 的驱动长什么样。驱动 jar 负责实现 Driver,部署到 classpath 上,DriverManager 启动的时候自己去发现。

编译框架的时候,它只认识 Driver 这个接口;具体跑的是哪个驱动,要等 JVM 起来、classpath 定下来之后才知道。这就是常说的依赖倒置:高层模块不依赖低层实现,两边都依赖接口。

SPI 对扩展方的约定不是「你能调用什么」,而是「你要实现什么、怎么声明自己是实现」。


ServiceLoader

JDK 自带的 SPI 机制只有一个类:java.util.ServiceLoader。我们写个小例子跑一遍。

接口:

java 复制代码
package com.example.spi;

public interface Greeter {
    String greet();
}

两个实现:

java 复制代码
package com.example.impl;

public class EnglishGreeter implements Greeter {
    public String greet() { return "hello"; }
}
java 复制代码
package com.example.impl;

public class ChineseGreeter implements Greeter {
    public String greet() { return "你好"; }
}

调用方完全不知道这两个类的存在:

java 复制代码
ServiceLoader<Greeter> loader = ServiceLoader.load(Greeter.class);
int n = 0;
for (Greeter g : loader) {
    System.out.println(g.getClass().getName() + " -> " + g.greet());
    n++;
}
System.out.println("loaded = " + n);

光有类还不够,得告诉 ServiceLoader 去哪找实现。在 classpath 上放一个文件:

text 复制代码
META-INF/services/com.example.spi.Greeter

文件名就是接口的全限定名,内容一行一个实现类:

text 复制代码
com.example.impl.EnglishGreeter
com.example.impl.ChineseGreeter

跑起来:

text 复制代码
com.example.impl.EnglishGreeter -> hello
com.example.impl.ChineseGreeter -> 你好
loaded = 2

把那个文件删掉,同样的一段 main 输出是 loaded = 0,一行实现都不加载。接口和实现之间唯一的联系就是那个文本文件。

ServiceLoader 内部做的事并不神秘:沿着 classpath 把所有同名文件找出来,读每一行的类名,Class.forName 拿到 Class,再 getDeclaredConstructor().newInstance() 造对象。所以实现类必须是 public 的,必须有无参构造器。这整条链路就是反射的应用。

Java 9 之后模块化项目不用这个文件了,改成在 module-info.java 里声明:

java 复制代码
provides com.example.spi.Greeter with com.example.impl.EnglishGreeter;

现实里的 SPI

JDBC 是最标准的例子。JDBC 4.0 之后,驱动 jar 里带一个 META-INF/services/java.sql.Driver,内容是 com.mysql.cj.jdbc.Driver 这样的类名,DriverManager 用 ServiceLoader 把它们全找出来。

早年写 Class.forName("com.mysql.jdbc.Driver") 是为了触发驱动类静态块里的注册逻辑。有了 SPI 之后这一步省掉了,现在不写 forName 也能连上数据库。反射那篇里讲 forName 会触发类初始化,说的就是那个年代的事。

SLF4J 也是。我们的代码只依赖 org.slf4j.Logger,具体是 logback 还是 log4j2,由 LoggerFactory 在启动时通过 SPI 找出来。换实现只换 jar,代码一行不改。


两者的关系

同一个库里往往两个都有,以 JDBC 为例:

角色 例子 谁定义 谁实现 谁调用
API Connection、Statement JDK 驱动厂商 我们的业务代码
SPI Driver JDK 驱动厂商 DriverManager

Connection 是我们主动调的,所以是 API;Driver 是 DriverManager 在运行时找出来调的,所以是 SPI。两者都定义在 java.sql 里、都由驱动厂商实现,区别只在于调用是谁发起的、发生在什么阶段。

API SPI
依赖方向 调用方 → 实现方 实现方 → 接口 ← 调用方
绑定时机 编译期 运行期
谁写实现 库/框架 第三方
典型 List、Connection java.sql.Driver、SLF4J provider

SPI 的限制

ServiceLoader 只解决「找到所有实现」,再往下的问题它不管:

  • 不能按名字挑。 load() 拿到的是一串实现,要么全用,要么自己在外面再选。Dubbo 的 ExtensionLoader 就是在 ServiceLoader 思路上加了一层:给每个实现起个名字,用 @SPI("dubbo") 标默认实现,按名字取。
  • 没有优先级。 加载顺序取决于 classpath 上文件里写的顺序,ServiceLoader 不提供排序。
  • 只能无参构造。 实现类必须 public + 无参构造器,想注入依赖得自己套一层。
  • 实例作用域是 loader 级的。 每次 ServiceLoader.load() 都返回新的 loader,各自缓存自己的实例;同一个 loader 迭代两次拿到的是同一批对象,跨 loader 就不是了。

所以 ServiceLoader 本身只是一个很轻的发现机制,框架要做按需选择、带优先级的扩展点,都会在它上面再包一层。

相关推荐
imDwAaY1 小时前
Java String 核心原理:一篇讲透字符串常见面试题
后端
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 26 章 · 指标、观测与链路追踪:Micrometer 与 OpenTelemetry
java·后端·开源
for_ever_love__1 小时前
Agentic RAG 实战:让 Agent 自己决定怎么检索、检索几次
人工智能·后端·agent·控制·自适应·rag·检索策略
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 19 章 · Spring MVC 自动配置:DispatcherServlet、消息转换
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 28 章 AOT 与 GraalVM Native与序列化模型
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 27 章 · 可执行归档与 Loader:`JarLauncher` 与嵌套 jar
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 25 章 · Actuator 与健康:端点机制、`HealthIndicator`、管理端口子上下文
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 16 章 · 排序、元数据与报告:`AutoConfigurationSorter`、配置元数据、条件报告
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 14 章 · 自动配置引擎:`AutoConfigurationImportSelector` 全流程
java·后端·开源