一、概述
SPI(Service Provider Interface)主要是被框架开发人员使用的一种技术。
JDK中的SPI是面向接口编程的,服务规则提供者会在JRE的核心API里提供服务访问接口。开发者实现对应的接口,并配置。
(1)实现步骤:
- 规范制定者定义接口
- 开发者实现接口,并在
META-INF/services文件夹下建立文件
例如,使用 Java 语言访问数据库时会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。
例如,如果:
- 规范制定者在
rt.jar包里定义了数据库的驱动接口java.sql.Driver- 那么,
MySQL实现的开发商则会在MySQL的驱动包的META-INF/services文件夹下建立名称为java.sql.Driver的文件,文件内容就是MySQL对java.sql.Driver接口的实现类。如图:
文件内容:
com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver
(2)JDK SPI 自定义实现
当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。
当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。
public interface Log {
void log(String info);
}
public class Logback implements Log {
@Override
public void log(String info) {
System.out.println("Logback:" + info);
}
}
public class Log4j implements Log {
@Override
public void log(String info) {
System.out.println("Log4j:" + info);
}
}
在项目的 resources/META-INF/services 目录下添加一个名为 com.donaldy.spi.Log 的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:
com.donaldy.spi.Log4j
com.donaldy.spi.Logback
样例图:
最后创建 main() 方法,其中会加载上述配置文件,创建全部 Log 接口实现的实例,并执行其 log() 方法,如下所示:
public class Main {
public static void main(String[] args) {
ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
for (Log log : serviceLoader) {
log.log("JDK SPI");
}
}
}
// 输出如下:
// Log4j:JDK SPI
// Logback:JDK SPI
二、SPI 的实现原理
重要知识点:
-
Java核心API(比如rt.jar包)是使用Bootstrap ClassLoader类加载器加载的。 -
而用户提供的
Jar包是由AppClassLoader加载的。
如果一个类由类加载器加载,那么这个类依赖的类也是相同的类加载器加载的。
用来搜索开发商提供的 SPI 扩展实现类的 API 类(ServiceLoader)是使用 Bootstrap ClassLoader 加载的,那么 ServiceLoader 里面依赖的类应该也是由 Bootstrap ClassLoader 加载的。
而用户提供的 Jar 包是由 AppClassLoader 加载的,所以这就需要一种违反双亲委派模型的方法,线程上下文类加载器 ContextClassLoader 就是用来解决这个问题的。
测试代码,查看具体是如何实现的:
import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;
public class TestSpi {
public static void main(String[] args) {
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
for (Driver driver : loader) {
System.out.println("driver: " + driver.getClass() + ", loader: "
+ driver.getClass().getClassLoader());
System.out.println("current thread contextLoader: "
+ Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader loader: " + ServiceLoader.class.getClassLoader());
}
}
}
引入 MySQL 驱动的 jar 包,执行结果如下:
driver: class com.mysql.jdbc.Driver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2
current thread contextLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader loader: null
driver: class com.mysql.fabric.jdbc.FabricMySQLDriver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2
current thread contextLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader loader: null
从执行结果可以知道 ServiceLoader 的加载器为 Bootstarp , 因为这里输出了 null,并且该类在 rt.jar 里面。
ServiceLoader 的 load() 源码:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程上下文加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这里获取了当前线程上下文加载器,指 AppClassLoader