专栏名称: 石杉的架构笔记
专注原创、用心雕琢!十余年BAT一线大厂架构经验倾囊相授
目录
相关文章推荐
参考消息  ·  歼-35A亮相,美媒评估战力 ·  17 小时前  
吉安发布  ·  火了!吉安人在越南开江西饭店上热搜! ·  昨天  
Foodaily每日食品  ·  瑞幸辗压下,库迪活成了“便利店” ·  4 天前  
参考消息  ·  最新!以总理向军方下令 ·  3 天前  
51好读  ›  专栏  ›  石杉的架构笔记

从源码角度,看 Java 是如何实现自己的 SPI 机制的?

石杉的架构笔记  · 公众号  ·  · 2020-04-03 08:30

正文

公众号后台回复“ 面试 ”,获取精品学习资料

扫描下方海报了解 专栏详情

本文来自于公众号源码笔记的投稿


《Java工程师面试突击(第3季)》重磅升级,由原来的 70讲增至160讲 ,内容扩充一倍多,升级部分内容请参见文末

注:该源码分析对应JDK版本为1.8

1 引言

这是【源码笔记】的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码。

2 什么是SPI机制

那么,什么是SPI机制呢?
SPI是Service Provider Interface 的简称,即 服务提供者接口 的意思。
根据字面意思我们可能还有点困惑,SPI说白了就是一种扩展机制,我们在相应配置文件中定义好某个接口的实现类,然后再根据这个接口去这个配置文件中加载这个实例类并实例化,其实SPI就是这么一个东西。
说到SPI机制,我们最常见的就是Java的SPI机制,此外,还有Dubbo和SpringBoot自定义的SPI机制。
有了SPI机制,那么就为一些框架的灵活扩展提供了可能,而不必将框架的一些实现类写死在代码里面。
那么,某些框架是如何利用SPI机制来做到灵活扩展的呢?下面举几个栗子来阐述下:
  1. JDBC驱动加载案例 :利用Java的SPI机制,我们可以根据不同的数据库厂商来引入不同的JDBC驱动包;
  2. SpringBoot的SPI机制 :我们可以在 spring.factories 中加上我们自定义的自动配置类,事件监听器或初始化器等;
  3. Dubbo的SPI机制 :Dubbo更是把SPI机制应用的 淋漓尽致 ,Dubbo基本上自身的每个功能点都提供了扩展点,比如提供了集群扩展,路由扩展和负载均衡扩展等差不多接近30个扩展点。如果Dubbo的某个内置实现不符合我们的需求,那么我们只要利用其SPI机制将我们的实现替换掉Dubbo的实现即可。
上面的三个栗子先让我们直观感受下某些框架利用SPI机制是如何做到灵活扩展的。

3 如何使用Java的SPI?

我们先来看看如何使用Java自带的SPI。先定义一个 Developer 接口
// Developer.java

package com.ymbj.spi;

public interface Developer {
void sayHi();
}

再定义两个 Developer 接口的两个实现类:

// JavaDeveloper.java

package com.ymbj.spi;

public class JavaDeveloper implements Developer {

@Override
public void sayHi() {
System.out.println("Hi, I am a Java Developer.");
}
}
// PythonDeveloper.java

package com.ymbj.spi;

public class PythonDeveloper implements Developer {

@Override
public void sayHi() {
System.out.println("Hi, I am a Python Developer.");
}
}
然后再在项目 resources 目录下新建一个 META-INF/services 文件夹,然后再新建一个以 Developer 接口的全限定名命名的文件,文件内容为:
// com.ymbj.spi.Developer文件

com.ymbj.spi.JavaDeveloper
com.ymbj.spi.PythonDeveloper

最后我们再新建一个测试类 JdkSPITest

// JdkSPITest.java

public class JdkSPITest {

@Test
public void testSayHi() throws Exception {
ServiceLoader serviceLoader = ServiceLoader.load(Developer.class);
serviceLoader.forEach(Developer::sayHi);
}
}
运行上面那个测试类,运行成功结果如下截图所示:
由上面简单的Demo我们知道了如何使用Java的SPI机制来实现扩展点加载,下面推荐一篇文章:JAVA拾遗--关于SPI机制,通过这篇文章,相信大家对Java的SPI会有一个比较深刻的理解,特别是JDBC加载驱动这方面。

4 Java的SPI机制的源码分析

通过前面扩展 Developer 接口的简单Demo,我们看到Java的SPI机制实现跟 ServiceLoader 这个类有关,那么我们先来看下 ServiceLoader 的类结构代码:
// ServiceLoader实现了【Iterable】接口
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
// 构造方法
private ServiceLoader(Class svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

// ...暂时省略相关代码

// ServiceLoader的内部类LazyIterator,实现了【Iterator】接口
// Private inner class implementing fully-lazy provider lookup
private class LazyIterator
implements Iterator<S>
{
Class service;
ClassLoader loader;
Enumeration configs = null;
Iterator pending = null;
String nextName = null;

private LazyIterator(Class service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
// 覆写Iterator接口的hasNext方法
public boolean hasNext() {
// ...暂时省略相关代码
}
// 覆写Iterator接口的next方法
public S next() {
// ...暂时省略相关代码
}
// 覆写Iterator接口的remove方法
public void remove() {
// ...暂时省略相关代码
}

}

// 覆写Iterable接口的iterator方法,返回一个迭代器
public Iterator iterator() {
// ...暂时省略相关代码
}

// ...暂时省略相关代码

}
可以看到, ServiceLoader 实现了 Iterable 接口,覆写其 iterator 方法能产生一个迭代器;同时 ServiceLoader 有一个内部类 LazyIterator ,而 LazyIterator 又实现了 Iterator 接口,说明 LazyIterator 是一个迭代器。

4.1 ServiceLoader.load方法,为加载服务提供者实现类做前期准备

那么我们现在开始探究Java的SPI机制的源码, 先来看 JdkSPITest 的第一句代码 ServiceLoader serviceLoader = ServiceLoader.load(Developer.class); 中的 ServiceLoader.load(Developer.class) 的源码:
// ServiceLoader.java

public static ServiceLoader load(Class service) {
//获取当前线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 将service接口类和线程上下文类加载器作为参数传入,继续调用load方法
return ServiceLoader.load(service, cl);
}

我们再来看下 ServiceLoader.load(service, cl); 方法:

// ServiceLoader.java

public static ServiceLoader load(Class service,
ClassLoader loader)

{
// 将service接口类和线程上下文类加载器作为构造参数,新建了一个ServiceLoader对象
return new ServiceLoader<>(service, loader);
}

继续看 new ServiceLoader<>(service, loader); 是如何构建的?

// ServiceLoader.java

private ServiceLoader(Class svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
可以看到在构建 ServiceLoader 对象时除了给其成员属性赋值外,还调用了 reload 方法:
// ServiceLoader.java

public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
可以看到在 reload 方法中又新建了一个 LazyIterator 对象,然后赋值给 lookupIterator
// ServiceLoader$LazyIterator.java

private LazyIterator(Class service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
可以看到在构建 LazyIterator 对象时,也只是给其成员变量 service loader 属性赋值呀,我们一路源码跟下来,也没有看到去 META-INF/services 文件夹加载 Developer 接口的实现类!这就奇怪了,我们都被 ServiceLoader load 方法名骗了。
还记得分析前面的代码时新建了一个 LazyIterator 对象吗? Lazy 顾名思义是 的意思, Iterator 就是迭代的意思。我们此时猜测那么 LazyIterator 对象的作用应该就是在迭代的时候再去加载 Developer 接口的实现类了。

4.2 ServiceLoader.iterator方法,实现服务提供者实现类的懒加载

我们现在再来看 JdkSPITest 的第二句代码 serviceLoader.forEach(Developer::sayHi); ,执行这句代码后最终会调用 serviceLoader iterator 方法:
// serviceLoader.java

public Iterator iterator() {
return new Iterator() {

Iterator> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
if (knownProviders.hasNext())
return true;
// 调用lookupIterator即LazyIterator的hasNext方法
// 可以看到是委托给LazyIterator的hasNext方法来实现
return lookupIterator.hasNext();
}

public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 调用lookupIterator即LazyIterator的next方法
// 可以看到是委托给LazyIterator的next方法来实现
return lookupIterator.next();
}

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

};
}
可以看到调用 serviceLoader iterator 方法会返回一个匿名的迭代器对象,而这个匿名迭代器对象其实相当于一个门面类,其覆写的 hasNext next 方法又分别委托 LazyIterator hasNext next 方法来实现了。
我们继续调试,发现接下来会进入 LazyIterator hasNext 方法:
// serviceLoader$LazyIterator.java

public boolean hasNext () {
if (acc == null) {
// 调用hasNextService方法
return hasNextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

继续跟进 hasNextService 方法:

// serviceLoader$LazyIterator.java

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = "META-INF/services/"
// service.getName()即接口的全限定名
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性service赋值吗
String fullName = PREFIX + service.getName();
// 加载META-INF/services/目录下的接口文件中的服务提供者类
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 还记得前面的代码构建LazyIterator对象时已经给其成员属性loader赋值吗
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 返回META-INF/services/目录下的接口文件中的服务提供者类并赋值给pending属性
pending = parse(service, configs.nextElement());
}
// 然后取出一个全限定名赋值给LazyIterator的成员变量nextName
nextName = pending.next();
return true;
}
可以看到在执行 LazyIterator hasNextService 方法时最终将去 META-INF/services/ 目录下加载接口文件的内容即加载服务提供者实现类的全限定名,然后取出一个服务提供者实现类的全限定名赋值给 LazyIterator 的成员变量 nextName
到了这里,我们就明白了 LazyIterator 的作用真的是懒加载,在用到的时候才会真正去加载服务提供者实现类。
思考 :为何这里要用懒加载呢?懒加载的思想是怎样的呢?懒加载有啥好处呢?你还能举出其他懒加载的案例吗?
同样,执行完 LazyIterator hasNext 方法后,会继续执行 LazyIterator next 方法:
// serviceLoader$LazyIterator.java




    


public S next() {
if (acc == null) {
// 调用nextService方法
return nextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

我们继续跟进 nextService 方法:

// serviceLoader$LazyIterator.java

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// 还记得在hasNextService方法中为nextName赋值过服务提供者实现类的全限定名吗
String cn = nextName;
nextName = null;
Class> c = null;
try {
// 【1】去classpath中根据传入的类加载器和服务提供者实现类的全限定名去加载服务提供者实现类
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 {
// 【2】实例化刚才加载的服务提供者实现类,并进行转换
S p = service.cast(c.newInstance());
// 【3】最终将实例化后的服务提供者实现类放进providers集合
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
可以看到 LazyIterator nextService 方法最终将实例化之前加载的服务提供者实现类,并放进 providers 集合中,随后再调用服务提供者实现类的方法(比如这里指 JavaDeveloper sayHi 方法)。
注意,这里是加载一个服务提供者实现类后,若 main 函数中有调用该服务提供者实现类的方法的话,紧接着会调用其方法;然后继续实例化下一个服务提供者类。
因此,我们看到了 ServiceLoader.iterator 方法真正承担了加载并实例化 META-INF/services/ 目录下的接口文件里定义的服务提供者实现类。
设计模式 :可以看到,Java的SPI机制实现代码中应用了迭代器模式,迭代器模式屏蔽了各种存储对象的内部结构差异,提供一个统一的视图来遍历各个存储对象(存储对象可以为集合,数组等)。 java.util.Iterator 也是迭代器模式的实现:同时Java的各个集合类一般实现了 Iterable 接口,实现了其 iterator 方法从而获得 Iterator 接口的实现类对象(一般为集合内部类),然后再利用 Iterator 对象的实现类的 hasNext next 方法来遍历集合元素。

5 JDBC驱动加载源码解读

前面分析了Java的SPI机制的源码实现,现在我们再来看下Java的SPI机制的实际案例的应用。
我们都知道,JDBC驱动加载是Java的SPI机制的典型应用案例。JDBC主要提供了一套接口规范,而这套规范的api在java的核心库( rt.jar )中实现,而不同的数据库厂商只要编写符合这套JDBC接口规范的驱动代码,那么就可以用Java语言来连接数据库了。
java的核心库( rt.jar )中跟JDBC驱动加载的最核心的接口和类分别是 java.sql.Driver 接口和 java.sql.DriverManager 类,其中 java.sql.Driver 是各个数据库厂商的驱动类要实现的接口,而 DriverManager 是用来管理数据库的驱动类的,值得注意的是 DriverManager 这个类有一个 registeredDrivers 集合属性,用来存储Mysql的驱动类。
// DriverManager.java

// List of registered JDBC drivers
private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();
这里以加载Mysql驱动为例来分析JDBC驱动加载的源码。
我们的项目引入 mysql-connector-java 依赖(这里的版本是 5.1.47 )后,那么Mysql的驱动实现类文件如下图所示:

可以看到Mysql的驱动包中有两个 Driver 驱动类,分别是 com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver ,默认情况下一般我们只用到前者。
5.1 利用Java的SPI加载Mysql的驱动类
那么接下来我们就来探究下JDBC驱动加载的代码是如何实现的。
先来看一下一个简单的JDBC的测试代码:
// JdbcTest.java

public class JdbcTest {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet rs = null;

try {
// 注意:在JDBC 4.0规范中,这里可以不用再像以前那样编写显式加载数据库的代码了
// Class.forName("com.mysql.jdbc.Driver");
// 获取数据库连接,注意【这里将会加载mysql的驱动包】
/***************【主线,切入点】****************/
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc", "root", "123456" );
// 创建Statement语句
statement = connection.createStatement();
// 执行查询语句
rs = statement.executeQuery("select * from user");
// 遍历查询结果集
while(rs.next()){
String name = rs.getString("name");
System.out.println(name);
}
} catch(Exception e) {
e.printStackTrace();
} finally {
// ...省略释放资源的代码
}
}
}
JdbcTest main 函数调用 DriverManager getConnection 方法时,此时必然会先执行 DriverManager 类的静态代码块的代码,然后再执行 getConnection 方法,那么先来看下 DriverManager 的静态代码块:
// DriverManager.java

static {
// 加载驱动实现类
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

继续跟进 loadInitialDrivers 的代码:

// DriverManager.java

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
// 来到这里,是不是感觉似曾相识,对,没错,我们在前面的JdkSPITest代码中执行过下面的两句代码
// 这句代码前面已经分析过,这里不会真正加载服务提供者实现类
// 而是实例化一个ServiceLoader对象且实例化一个LazyIterator对象用于懒加载
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
// 调用ServiceLoader的iterator方法,在迭代的同时,也会去加载并实例化META-INF/services/java.sql.Driver文件
// 的com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver两个驱动类
/****************【主线,重点关注】**********************/
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
在上面的代码中,我们可以看到Mysql的驱动类加载主要是利用Java的SPI机制实现的,即利用 ServiceLoader 来实现加载并实例化Mysql的驱动类。
5.2 注册Mysql的驱动类
那么,上面的代码只是Mysql驱动类的加载和实例化, 那么,驱动类又是如何被注册进 DriverManager registeredDrivers 集合的呢?
这时,我们注意到 com.mysql.jdbc.Driver 类里面也有个静态代码块,即实例化该类时肯定会触发该静态代码块代码的执行,那么我们直接看下这个静态代码块做了什么事情:
// com.mysql.jdbc.Driver.java

// Register ourselves with the DriverManager
static {
try {
// 将自己注册进DriverManager类的registeredDrivers集合
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
可以看到,原来就是Mysql驱动类 com.mysql.jdbc.Driver 在实例化的时候,利用执行其静态代码块的时机时将自己注册进 DriverManager registeredDrivers 集合中。






请到「今天看啥」查看全文