深入了解JDK SPI的源码分析及实践使用方式,看完对你应该有所帮助

Posted 程序猿DaBo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解JDK SPI的源码分析及实践使用方式,看完对你应该有所帮助相关的知识,希望对你有一定的参考价值。

目录

  • SPI简介

  • JDK SPI 机制

  • JDK SPI 源码分析

  • JDK SPI 在 JDBC 中的应用

  • 总结


前言

Dubbo 为了更好地达到 OCP 原则(对修改封闭,对扩展开放的原则),采用了“微内核+插件”的架构。那微内核架构是什么呢?微内核架构是一种典型的架构模式,也被叫做插件化架构(Plug-in Architecture),这是一种面向功能进行拆分的可扩展性架构。在微内核架构中,内核管理插件生命周期的方式一般是采用 IoC、Factory、OSGi 等,Dubbo 最终决定采用 SPI 机制来加载插件,Dubbo SPI 参考 JDK 原生的 SPI 机制,进行了性能优化以及功能增强。所以如果你想要了解 Dubbo SPI ,需要先了解下 JDK SPI 的工作原理。


SPI简介

SPI( Service Provider Interface)是一种服务发现机制。SPI本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类,这样运行时可以动态的为接口替换实现类。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

此机制给很多框架扩展提供了选择,例如在JDBC、Dubbo中都使用到了SPI机制。


JDK SPI 机制

当服务的提供者提供一种接口的实现,接着就需要在 Classpath 下的 META-INF/services/ 目录里创建好一个以服务接口命名的文件,此文件记录了这个 jar 包提供的服务接口的具体实现类。当某个应用引入了这个 jar 包并且需要使用这个服务时,通过查找这个 jar 包的 META-INF/services/ 中的配置文件JDK SPI 机制就能获得具体的实现类名,进行实现类的加载和实例化,然后使用该实现类完成业务功能。

先通过简单的示例演示一下 JDK SPI 的基本使用方式:

  • 1、首先创建一个database-driver工程,并创建DataBaseDriver接口:

public interface DataBaseDriver {

String connect(String host);
}


  • 2、然后创建mysql-driver工程

2.1 引入database-driver工程依赖,并实现DataBaseDriver接口:

public class MysqlDriver implements DataBaseDriver {

@Override
public String connect(String host) {
return "begin build Mysql connect:"+host;
}
}

2.2 在mysql-driver工程的
resources/META-INF/services 目录下添加一个名为 com.yibo.spi.DataBaseDriver的文件,此为 JDK SPI 需要读取的配置文件:

com.yibo.spi.MysqlDriver


  • 3、接下来创建oracle-driver工程

3.1 引入database-driver工程依赖,并实现DataBaseDriver接口:

public class OracleDriver implements DataBaseDriver{

@Override
public String connect(String host) {
return "begin build Oracle connect:"+host;
}
}

3.2 在oracle-driver工程的
resources/META-INF/services 目录下添加一个名为 com.yibo.spi.DataBaseDriver的文件,此为 JDK SPI 需要读取的配置文件:

com.yibo.spi.OracleDriver


  • 4、再新建client-demo工程,引入oracle-driver、mysql-driverdatabase-driver依赖

<dependencies>
<dependency>
<artifactId>oracle-driver</artifactId>
<groupId>com.yibo</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<artifactId>mysql-driver</artifactId>
<groupId>com.yibo</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<artifactId>database-driver</artifactId>
<groupId>com.yibo</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>


  • 5、最后创建测试类,加载DataBaseDriver接口,调用connect方法

public class App {

public static void main( String[] args ) {
ServiceLoader<DataBaseDriver> serviceLoader = ServiceLoader.load(DataBaseDriver.class);
System.out.println( "Java SPI" );
for (DataBaseDriver driver : serviceLoader) {
System.out.println(driver.connect("localhost"));
}
}
}

#输出
Java SPI
begin build Mysql connect:localhost
begin build Oracle connect:localhost


JDK SPI 源码分析

通过上面的示例可以看到 ServiceLoader.load() 方法是 JDK SPI 的入口方法,我们接下来就对它的具体实现进行深入分析。

在 ServiceLoader.load() 方法中,我们先尝试获取当前使用的 ClassLoader(获取当前线程绑定的 ClassLoader,查找失败后使用 SystemClassLoader),然后再调用 reload() 方法。

在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),这个缓存用来记录 ServiceLoader 创建的实现对象,其中Value 是实现类的对象, Key 是实现类的完整类名。然后创建 LazyIterator 迭代器读取 SPI 配置文件并实例化实现类对象。

ServiceLoader.reload() 方法的具体实现:

private LazyIterator lookupIterator;

// 缓存,用来缓存 ServiceLoader创建的实现对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
providers.clear();// 清空缓存
lookupIterator = new LazyIterator(service, loader);// 迭代器
}

在如上示例中,main() 方法中使用的迭代器底层就是调用了
ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法:next() 方法和hasNext() 方法 。这里的 LazyIterator 中的hasNext() 方法最终调用的是 hasNextService() 方法,next() 方法最终调用的是其 nextService() 方法。

我们看下
LazyIterator.hasNextService() 方法,此方法重点负责查找 META-INF/services 目录下的 SPI 配置文件,并进行遍历:

public final class ServiceLoader<S> implements Iterable<S>{

private static final String PREFIX = "META-INF/services/";

private class LazyIterator implements Iterator<S>{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
// PREFIX前缀与服务接口的名称拼接起来,就是META-INF目录下定义的SPI配
// 置文件(即示例中的META-INF/services/com.yibo.spi.MysqlDriver)
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);
}
}
// 按行SPI遍历配置文件的内容
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析配置文件
pending = parse(service, configs.nextElement());
}
// 更新 nextName字段
nextName = pending.next();
return true;
}
}
}

SPI 配置文件的解析在 hasNextService() 方法中完成之后,我们再来看 LazyIterator.nextService() 方法,这个方法负责将hasNextService() 方法读取到的实现类实例化,其中会将实例化的对象放到 providers 集合中缓存起来,具体核心实现:

public final class ServiceLoader<S> implements Iterable<S>{

private static final String PREFIX = "META-INF/services/";

private class LazyIterator implements Iterator<S>{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加载 nextName字段指定的类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 检测类型
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 创建实现类的对象
S p = service.cast(c.newInstance());
// 将实现类名称以及相应实例对象添加到缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
}
}

如上就是在 main() 方法中使用的迭代器的底层实现。最后看一下 main() 方法中使用ServiceLoader.iterator() 方法拿到的迭代器是如何实现的(foreach在遍历集合时也是依赖于iterator),这个迭代器是依赖 LazyIterator 实现的一个匿名内部类,核心实现:

public final class ServiceLoader<S> implements Iterable<S>{

private static final String PREFIX = "META-INF/services/";

public Iterator<S> iterator() {
return new Iterator<S>() {
// knownProviders用来迭代providers缓存
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 先走查询缓存,缓存查询失败,再通过LazyIterator加载
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}

public S next() {
// 先走查询缓存,缓存查询失败,再通过 LazyIterator加载
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}

private class LazyIterator implements Iterator<S>{
。。。。。。
}
}


JDK SPI 在 JDBC 中的应用

在了解下 JDK SPI 实现的原理之后,我们再来看下 JDBC如何在实践中使用 JDK SPI 机制加载不同数据库厂商的实现类。

JDK 中仅仅定义了一个 java.sql.Driver 接口,具体的实现是由不同数据库厂商来提供的。这里我们以 MySQL 提供的 JDBC 实现包为例来分析。

在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下,有一个 java.sql.Driver 文件中只有一行内容,如下:

com.mysql.cj.jdbc.Driver 

使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时,会用到下面的语句创建数据库连接:

String url = "jdbc:xxx://xxx:xxx/xxx"; 
Connection conn = DriverManager.getConnection(url, username, pwd);

DriverManager是 JDK 提供的数据库驱动管理器,其中的代码片段如下:

static { 
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

在调用 getConnection() 方法的时,DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行;在 loadInitialDrivers() 方法中通过 JDK SPI 扫描 Classpath 下 java.sql.Driver 接口实现类并实例化,核心实现如下:

private static void loadInitialDrivers() { 
String drivers = System.getProperty("jdbc.drivers")
// 使用 JDK SPI机制加载所有 java.sql.Driver实现类
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
String[] driversList = drivers.split(":");
for (String aDriver : driversList) { // 初始化Driver实现类
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
}
}

在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中,同样有一段 static 静态代码块,这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到
DriverManager.registeredDrivers 集合中(CopyOnWriteArrayList 类型),如下:

static { 
java.sql.DriverManager.registerDriver(new Driver());
}

在 getConnection() 方法中,DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection,核心实现如下:

private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { 
// 省略 try/catch代码块以及权限处理逻辑
for(DriverInfo aDriver : registeredDrivers) {
Connection con = aDriver.driver.connect(url, info);
return con;
}
}


总结

这次我们主要介绍了 JDK 提供的 SPI 机制的基本使用,然后深入分析了 JDK SPI 的核心原理和底层实现,对其源码进行了深入剖析,最后我们以 MySQL 提供的 JDBC 实现为例,分析了 JDK SPI 在实践中的使用方式。


以上是关于深入了解JDK SPI的源码分析及实践使用方式,看完对你应该有所帮助的主要内容,如果未能解决你的问题,请参考以下文章

JDK源码系列 ------ 深入理解SPI机制

Dubbo SPI源码分析

源码角度了解Skywalking之SPI在SKywalking中应用

dubbo源码分析01:SPI机制

Dubbo SPI 源码深入分析

Dubbo 2.7.3源码分析——JDK SPI篇