什么是 SPI,和 API 有啥区别?

#Java#面试八股

什么是 SPI,和 API 有啥区别?

结论:API(Application Programming Interface)是"我提供、你调用"——实现方定义并实现,调用方编译期依赖它;SPI(Service Provider Interface)是"我定接口、你实现"——框架只定义扩展点,第三方提供实现,通过 META-INF/services/ 下的配置文件(JDK 9+ 也可用 module-info 的 provides)声明,运行期由 ServiceLoader 发现并实例化。SPI 把依赖方向倒置了,是"可插拔扩展"的标准做法。

1. 依赖方向的区别

API: 你的代码 ──编译期依赖──→ 库(接口 + 实现都在库的 jar 里)

SPI: 框架 ──定义接口──→ 你实现接口 ──写 META-INF/services 注册──→
      框架运行期 ServiceLoader 扫描 classpath ──→ 实例化你的实现
      (框架编译期只依赖接口,不依赖任何实现)

典型例子:JDBC 只提供 java.sql.Driver 接口,MySQL/Oracle 各自实现;SLF4J 只提供日志门面,Logback/Log4j2 各自实现;你换驱动不用改一行业务代码,只换 jar。

2. 对照表

维度APISPI
依赖方向调用方依赖提供方提供方依赖调用方定义的接口
实现位置与接口同一个 jar由第三方 jar 提供
发现时机编译期直接引用运行期 ServiceLoader 扫描
目的复用功能可插拔扩展、解耦
谁控制实现提供方使用方(通过 classpath 选择)
例子HttpClient、Listjava.sql.Driver、CharsetProvider

3. 代码:自定义一个 SPI

// 1) 框架侧:定义扩展点
package com.example.codec;

public interface Codec {
    String name();
    byte[] encode(byte[] raw);
}

// 2) 提供方:实现接口
package com.example.codec.gzip;

public class GzipCodec implements Codec {
    @Override public String name() { return "gzip"; }
    @Override public byte[] encode(byte[] raw) { /* 省略实现 */ return raw; }
}
# 3) 提供方 jar 内的文件(路径与文件名都必须精确匹配接口全限定名)
src/main/resources/META-INF/services/com.example.codec.Codec
内容(UTF-8,一行一个实现类,可有 # 注释):
com.example.codec.gzip.GzipCodec
// 4) 框架侧:运行期加载
ServiceLoader<Codec> loader = ServiceLoader.load(Codec.class);
for (Codec c : loader) {
    if ("gzip".equals(c.name())) {
        return c;
    }
}
throw new IllegalStateException("no codec found");

ServiceLoader 的行为:扫描所有 classpath 下同名配置文件,逐个 Class.forName 并调用无参构造器实例化;它实现了 Iterable,遍历时才真正加载(延迟),但一旦遍历就会实例化全部实现。

4. SPI 的经典坑

坑说明解法
类加载器看不到实现框架用自身类加载器加载,第三方 jar 在别处用线程上下文类加载器(TCCL),JDBC 就是这么做的
无法按需/排序只能全量遍历,无法指定优先级用 @Order 或自研扩展加载器(Dubbo ExtensionLoader)
配置写错难排查文件名、包名拼错时静默无实现启动时校验并打印已加载的实现列表
全量实例化开销构造函数有成本时不划算框架侧缓存实例,或用 Provider 延迟 get()
与 Spring 冲突Spring 有自己的扩展机制Spring Boot 用 spring.factories / AutoConfiguration.imports,不是标准 SPI

5. 常见追问

追问回答要点
JDBC 为什么能只写 Class.forName?JDK 6+ 起 DriverManager 用 SPI 自动发现,Class.forName 只是兼容旧代码的显式加载
上下文类加载器怎么打破双亲委派?Thread.currentThread().getContextClassLoader() 由调用方设置,让父加载器加载的框架代码能看到子加载器里的实现
JDK 9 模块化后怎么写?module-info.java 里 provides com.example.Codec with com.example.GzipCodec;,消费方 uses com.example.Codec;
SPI 和 API 能同时存在吗?能,通常对外是 API、对内是 SPI,例如 Spring 的 BeanPostProcessor 既是 API 也是扩展点
为什么配置文件必须放 META-INF/services?这是 ServiceLoader 约定的扫描路径,文件名必须是接口的二进制名
SPI 能保证线程安全吗?不能,ServiceLoader 本身不是线程安全的,且实例通常是新对象

小结

API 是"调用",SPI 是"被发现"。判断一段代码是不是 SPI,看三点:接口由使用方定义、实现由第三方提供、通过配置在运行期发现。回答时补一句"JDBC 用 TCCL 解决类加载器可见性",基本就完整了。

笔记加载中…