1. 基本概念
SPI (Service Provider Interface), 是 Java 提供的一套用来被第三方实现或者扩展的 API, 它可以用来启用框架扩展和替换组件
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制
2. 使用场景
- 数据库驱动
- 日志 SLF4J
- Spring 使用 SPI
- Dubbo 使用 SPI, 但对原生 SPI 做了封装, 允许用户扩展实现 Filter 接口
3. 使用要求
要使用 SPI, 需要遵循如下约定
- SPI 的实现类中必须有一个无参的构造方法
- 当服务提供者提供了接口的实现, 在 jar 包的 META-INF 目录下创建一个以“接口全限定名”为命名的文件, 内容为实现类的全限定名
- JDK 中路径时 META-INF/services/
- Spring 中配置是 META-INF/spring.factories
- 接口实现类所在的 jar 包放在主程序的 classpath 中
- 通过 Loader 类将 META-INF 目录下的配置文件加载, 解析文件中的全限定名类名, 将类加载到 JVM
- JDK 中是使用的是 ServiceLoader 加载 META-INF/services/ 下的文件
- Spring 中使用的是 SpringFactoriesLoader 加载 META-INF/spring.factories
4. JDK SPI 实现
对数据库驱动源码分析
MySQL 驱动是 om.mysql.cj.jdbc 包下 Driver
实现了接口 JDK 的 java.sql.Driver
, 并在 META-INF/services
目录下创建文件名为 java.sql.Driver
的文件, 在文件里填写实现类的全路径信息 com.mysql.cj.jdbc.Driver
// 实现 JDK 的 java.sql.Driver 类
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// SPI 的实现类中必须有一个无参的构造方法
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
查看 java.util.ServiceLoader 源码
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
// 代表被加载的类或者接口
private final Class<S> service;
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
ServiceLoader 可以跨越 jar 包获取 META-INF/services
目录下的配置文件
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
获取实现类的全类名后, 通过反射方法 Class.forName()
加载类对象
5. Spring SPI 实现
Spring 的 SPI 实现是由 org.springframework.core.io.support 包下的 SpringFactoriesLoader 实现的, 配置文件路径是 META-INF/spring.factories 与 JDK 有差异
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
...
// 扫描 jar 包下 META-INF/spring.factories 文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
...
}
}
在 Spring 的 spring-data-redis 中对 JDK 和 Spring 都做了 SPI 实现