一、概述
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
暂无评论