SPI机制剖析——基于DriverManager+ServiceLoader的源码分析
我的上一篇博客类加载器与双亲委派中提到,SPI机制是一种上级类加载器调用下级类加载器的情形,因此会打破类加载的双亲委派模型。为了深入理解其中的细节,本博客详细剖析一下SPI机制,并以JDBC为例,基于源码来进行分析。
SPI
原理介绍
SPI(Service Provider Interface),是JDK内置的服务提供发现机制。即JDK内部定义规范的接口,不同厂商基于标准服务接口实现具体的实现类和方法。SPI一般被用来做框架扩展的开发。
下面这张图,很简明扼要地阐释了SPI的机理。

与SPI相对应的,是我们耳熟能详的API。API不需要上图中“标准服务接口”这一环节,而是调用方直接调用服务提供方。按照上一篇博客的分析,“标准服务接口”位于Java核心类库中,使用boot类加载器进行加载,而boot类加载器是无法获取“第三方实现类”的位置的。所以,相较于API而言,SPI需要打破双亲委派模型。
优缺点
好处
但是,我陷入思考,SPI这样的模式有什么好处吗,或者说API有什么缺点吗?
想象一下,如果程序直接调用第三方类库,当第三方类库发生改动时,应用程序代码很可能需要随之改动。但如果在JDK内部定义标准服务接口,要求第三方厂商实现这些接口,那无论实现类如何改动,只要标准接口不变,都不会影响到应用程序。所以我认为SPI机制的根本目的是为了“解耦”。这也就是面向对象中所谓的“接口编程”,把装配的控制权移到程序之外。
许多著名的第三方类库都采纳了SPI机制,JDBC就是其中之一。数据库厂商会基于标准接口来开发相应的连接库。如MySQL何PostgreSql的驱动都实现了标准接口:java.sql.Driver。对于应用程序而言,无需关心是MySQL还是PostgreSql,只需要与标准服务接口打交道即可。SPI正是基于这种模式完成了解耦合。
不足
当然,即便如此,SPI依旧是存在缺点和不足的,如下:
- 不能按需加载。需要遍历所有的实现,并且进行实例化,某些实现的实例化可能很耗时,这样会造成浪费;
- 获取实现类的方式不够灵活,只能通过Iterator获取,不能根据某个参数来获取实现类;
- ServiceLoader类的实例线程不安全。
JDBC的SPI机制
首先来看一段使用JDBC的简单代码:
@Test
public void testJDBC() throws SQLException, ClassNotFoundException {
String url = "jdbc:mysql://localhost:3307/mls";
String userName = "root";
String password = "123456";
// Class.forName("com.mysql.cj.jdbc.Driver");
Connection con = DriverManager.getConnection(url, userName, password);
Statement statement = con.createStatement();
String sql = "select * from mlsdb where id=1";
ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
System.out.println(rs.getString("province"));
}
}
注意到中间有一行注释的代码Class.forName("com.mysql.cj.jdbc.Driver");,其实这一行可写可不写。
我的倒数第二篇博客类加载时机与过程里提到,Class.forName方法会触发“初始化”,即触发类加载的进行。因此如果写上这行代码,此处则是使用APP类加载器加载mysql的jdbc驱动类。
然而,这一句Class.forName不用写,代码也能正常运行。因为加载DriverManager类时,会将MySQL的Driver对象注册进DriverManager中。具体流程后文会细说。其实这就是SPI思想的一个典型的实现。得益于SPI思想,应用程序中无需指定类似"com.mysql.cj.jdbc.Driver"这种全类名,尽可能地将第三方驱动从应用程序中解耦出来。
下面,通过源码来分析驱动加载以及服务发现的过程,主要涉及到DriverManager和ServiceLoader两个类
源码分析
DriverManager是用于管理Jdbc驱动的基础服务类,位于Java.sql包中,因此是由boot类加载器来进行加载。加载该类时,会执行如下代码块:
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
上述静态代码块会执行loadInitialDrivers()方法,该方法用于加载各个数据库驱动。代码如下:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//实例化ServiceLoader对象,并注入线程上下文类加载器和Driver.class
Iterator<Driver> 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);
}
}
}
ServiceLoader.load(Driver.class)此方法会把实例化一个ServiceLoader对象,并且向其注入线程上下文类加载器和Driver.class;loadedDrivers.iterator():获得ServiceLoader对象的迭代器;driversIterator.hasNext():查找Driver类;driversIterator.next():在实现的“next()”方法中进行类加载,使用上面的线程上下文类加载器。
ServiceLoader.load(Driver.class);的代码及相关调用方法如下:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();// 获得线程上下文类加载器
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> 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();
}
经过上述过程,用成员变量private final ClassLoader loader;引用传入的类加载器,用service接收Driver.class。同时,上述过程中实例化了一个LazyIterator对象,并用成员变量lookupIterator来引用。
执行ServiceLoader的“hasNext()”方法时最终会调用lookupIterator迭代器的“hasNext()”方法(此处暂且省略调用过程),如下:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
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);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
上述过程通过configs = loader.getResources(fullName)来查找实现Driver接口的类。
同样,ServiceLoader的迭代器的“next()”方法最终会调用lookupIterator迭代器的“next()”方法,如下:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);//使用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
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
可以看到,next()会最终调用到nextService()方法,并在此方法中通过c = Class.forName(cn, false, loader);执行类加载。此处的loader也是由ServiceLoader中的loader传入的,即为前文提到的线程上下文类加载器。
经历了上述ServiceLoader类中一系列操作之后(包括服务发现和类加载),位于mysql驱动包中的Driver类会被初始化。该类如下所示
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
上述Driver类加载时,会执行静态代码块,即执行DriverManager.registerDriver(new Driver());方法向DriverManager中注册一个Driver实例。
我们再回到DriverManager类中,看看registerDriver方法:
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
会将该MySQL驱动添加到成员变量registeredDrivers中,该成员变量存放已注册的jdbc驱动列表,如下:
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
如此一来,服务发现、类加载、驱动注册便到此结束。接下来,应用程序执行数据库连接操作时,会调用“getConnection”方法,遍历registeredDrivers,获取驱动,建立数据库连接。
总结
以上便是JDBC的SPI机制总结,最核心的地方在于,ServiceLoader中使用低级别的加载器发现Driver类,并进行类加载。这些工作是BootStrap类加载器所办不到的。由于DriverManager和ServiceLoader都位于Java核心类库中,使用BootStrap类加载器来加载,所以需要通过线程上下文类加载器向ServiceLoader对象中传入一个低级别的类加载器,如系统类加载器,从而来打破双亲委派机制。
SPI机制剖析——基于DriverManager+ServiceLoader的源码分析的更多相关文章
- Django——基于类的视图源码分析 二
源码分析 抽象类和常用视图(base.py) 这个文件包含视图的顶级抽象类(View),基于模板的工具类(TemplateResponseMixin),模板视图(TemplateView)和重定向视图 ...
- Android事件分发机制浅谈(二)--源码分析(ViewGroup篇)
上节我们大致了解了事件分发机制的内容,大概流程,这一节来分析下事件分发的源代码. 我们先来分析ViewGroup中dispatchTouchEvent()中的源码 public boolean dis ...
- Android事件分发机制浅谈(三)--源码分析(View篇)
写事件分发源码分析的时候很纠结,网上的许多博文都是先分析的View,后分析ViewGroup.因为我一开始理解的时候是按我的流程图往下走的,感觉方向很对,单是具体分析的时候总是磕磕绊绊的,老要跳到Vi ...
- Django——基于类的视图源码分析 一
基于类的视图(Class-based view)是Django 1.3引入的新的视图编写方式,用于取代以前基于函数(Function-based)方式. 借助于OO和Python中方便的多重继承特性, ...
- Django——基于类的视图源码分析 三
列表类通用视图(list.py) 此文件包含用于显示数据列表常用的类和工具类.不仅可以方便的用于显示基于模型(Model)的数据列表,也可以用于显示自定义数据列表. 此图中绿色部分属于base.py, ...
- spark 源码分析之十二 -- Spark内置RPC机制剖析之八Spark RPC总结
在spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv中,剖析了NettyRpcEnv的创建过程. Dispatcher.NettyStreamManager.T ...
- spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv
在前面源码剖析介绍中,spark 源码分析之二 -- SparkContext 的初始化过程 中的SparkEnv和 spark 源码分析之四 -- TaskScheduler的创建和启动过程 中的C ...
- Solr4.8.0源码分析(19)之缓存机制(二)
Solr4.8.0源码分析(19)之缓存机制(二) 前文<Solr4.8.0源码分析(18)之缓存机制(一)>介绍了Solr缓存的生命周期,重点介绍了Solr缓存的warn过程.本节将更深 ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
随机推荐
- Camtasia制作视频分割与视频拼接
视频的分割与拼接是在制作和编辑视频中经常用到的方法,运用Camtasia视频编辑器能够让视频制作更加的简单和便捷.Camtasia是一款录频软件和视频编辑器,可以进行屏幕录制.拖放视频等操作.小编采用 ...
- 怎么用思维导图软件iMindMap整理发文思路
如果你是一个普通的博客作者,那么你就应该明白在枯竭时寻找灵感就像是一场噩梦,即使你有一千个想法,但是你无法将它们关联起来也是无用的,所以,为什么不试试iMindMap思维导图呢,尝试创新,进行组建,你 ...
- python接口测试4-数据库获取参数
首先确定需要传递的参数和接口,使用接口测试工具验证一下,接口和参数没有问题. 编写python接口脚本 import requests import unittest import json impo ...
- Luogu P43916 图的遍历
我们把"u点能够到达的最大点"转化为反向图中能到达u点的所有点里的最大值,可知缩点后满足无后效性.val[i]的初值设为连通分量i中的最大点.反向存图,tarjan缩点,拓扑序dp ...
- Java基础教程——字符流
字符流 字节流服务文本文件时,可能出现中文乱码.因为一个中文字符可能占用多个字节. 针对于非英语系的国家和地区,提供了一套方便读写方式--字符流. java.io.Reader java.io.Wri ...
- kafka入门之broker--通信协议
kafka的通讯协议是基于tcp之上的二进制协议,所有类型的请求和响应都是结构化的,由不同的初始类型构成.kafka使用这组协议完成各个功能的实现. 单个kafka client通常需要同时连接多个b ...
- 在Mac下配置JDK
1.下载JDK,例JDK8U144 下载地址https://pan.baidu.com/s/1upjecBzTIMnyz0JmdkYprQ 2.安装 安装后配置JDK环境变量 cd ~ open .b ...
- Asp.NetCore之AutoMapper基础篇
应用场景 现在由于前后端技术的分离,后端程序员在使用ORM框架开发后台API接口的时候,往往会将数据库的"数据模型"直接提供给前端.而大多数时候,可能这些数据并不能够满足前端展示的 ...
- PyQt学习遇到的问题:重写notify发送的消息为什么首先给了一个QWindow对象?
在PyQt开发图形界面应用时,从QApplication派生的子类重写notify方法后(具体请参考<PyQt学习随笔:通过自定义类重写QApplication的notify方法捕获应用的所有消 ...
- 米酷CMS 7.0.4代码审计
工具:seay源代码审计系统 源代码:网上很好找,这里就懒得贴上了,找不到的话可以给我留言 后面一段时间会深入学习安全开发,代码审计,内网渗透和免杀,快快成长. 审这个系统是因为在先知上看到一篇审它老 ...