SPI 机制与 ServiceLoader
SPI(Service Provider Interface,服务提供者接口)是 Java 的插件化扩展机制,允许第三方在不修改框架代码的情况下注入自己的实现。JDBC 驱动加载、SLF4J 日志绑定、Dubbo 扩展体系都基于 SPI 思想。
SPI 的核心思想
框架定义接口(API) ← 由框架方提供
│
扩展方实现接口 ← 由第三方提供
│
SPI 配置文件将接口 → 实现类 ← 写在 META-INF/services/
│
框架用 ServiceLoader 动态加载实现 ← 框架方在运行时发现并调用
与 API 的区别:
- API:接口和实现都由框架方提供,用户直接调用
- SPI:接口由框架方定义,实现由第三方提供,框架发现并调用
Java SPI 的使用步骤
以实现一个日志 SPI 为例:
第一步:定义接口
// 框架包:myframework.jar
package com.example.framework;
public interface Logger {
void log(String message);
}
第二步:实现类(第三方)
// 扩展包:mylogger.jar
package com.example.logger;
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[Console] " + message);
}
}
第三步:配置文件
在 mylogger.jar 的资源目录创建文件:
META-INF/services/com.example.framework.Logger
文件内容(可以有多行,每行一个实现类的全限定名):
com.example.logger.ConsoleLogger
第四步:框架方动态加载
// 框架方使用 ServiceLoader 加载所有实现
ServiceLoader<Logger> loaders = ServiceLoader.load(Logger.class);
for (Logger logger : loaders) {
logger.log("Hello SPI!"); // 调用具体实现
}
经典案例:JDBC 驱动加载
JDK 4 之前(Class.forName 方式):
// 用户必须手动加载驱动
Class.forName("com.mysql.cj.jdbc.Driver"); // 硬编码驱动类名
Connection conn = DriverManager.getConnection(url, user, pwd);
JDK 6 之后(SPI 方式):
// 不需要任何 Class.forName!
Connection conn = DriverManager.getConnection(url, user, pwd);
原因:MySQL 驱动包(mysql-connector-java.jar)中有:
META-INF/services/java.sql.Driver
→ com.mysql.cj.jdbc.Driver
DriverManager 在静态初始化时用 ServiceLoader.load(Driver.class) 自动发现并注册所有 JDBC 驱动。
ServiceLoader 的工作原理
// OpenJDK 源码简化版
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(service, cl);
}
// 懒加载:遍历时才真正加载
public Iterator<S> iterator() {
return new Iterator<S>() {
public S next() {
// 1. 读取 META-INF/services/{接口名} 文件
// 2. 逐行读取实现类名称
// 3. Class.forName(className) 加载类
// 4. clazz.newInstance() 创建实例
// 5. 缓存已加载的实例
}
};
}
}
关键细节:
- 使用线程上下文类加载器(ContextClassLoader),打破双亲委派模型
- 懒加载:只有在遍历时才加载,不用一下子加载所有实现
- 无状态:每次调用
ServiceLoader.load()都创建新的 ServiceLoader
打破双亲委派的原因
问题:ServiceLoader 在 java.util 包里,由 Bootstrap ClassLoader 加载。但 JDBC 驱动实现类在 classpath 里,由 Application ClassLoader 加载。
双亲委派下,Bootstrap ClassLoader 无法加载 Application ClassLoader 能加载的类。
解决方案:线程上下文类加载器
// JDK DriverManager 中的关键代码
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 默认就是 Application ClassLoader
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, cl);
线程上下文类加载器允许父类加载器委托给子类加载器,"逆向"委派,是 SPI 机制的核心技巧。
Dubbo SPI vs JDK SPI
JDK SPI 的痛点:
- 加载所有实现:无法按需加载
- 不支持依赖注入:实现类之间无法互相注入
- 线程安全问题:多线程加载时可能有问题
Dubbo 扩展 SPI 解决了这些问题:
// Dubbo 扩展点定义
@SPI("netty") // 默认实现
public interface Transporter {
@Adaptive("server") // 自适应扩展点
Server bind(URL url, ChannelHandler handler);
}
// 按名字获取实现(按需加载)
Transporter transporter = ExtensionLoader
.getExtensionLoader(Transporter.class)
.getExtension("netty"); // 只加载 netty 实现
// 配置文件位置(Dubbo)
// META-INF/dubbo/com.alibaba.dubbo.remoting.Transporter
// 文件格式:key=value
// netty=com.alibaba.dubbo.remoting.transport.netty.NettyTransporter
// mina=com.alibaba.dubbo.remoting.transport.mina.MinaTransporter
Dubbo SPI 特点:
- 按名字加载:
getExtension("netty")只加载 netty 实现 - 自适应扩展:根据 URL 参数动态选择实现(
@Adaptive) - 激活扩展:根据条件自动激活(
@Activate) - IoC 注入:扩展点之间可以相互依赖
Spring 的 SPI(spring.factories)
Spring Boot 的自动装配也是 SPI 思想:
META-INF/spring.factories(Spring Boot 2.x)
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(3.x)
// 内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration,\
com.example.AnotherAutoConfiguration
Spring Boot 启动时扫描所有 jar 包的 spring.factories,加载标注了 @AutoConfiguration 的配置类,这就是"约定大于配置"自动装配的底层机制。