概述
一、SPI介绍
SPI具体介绍,可以参考以前的文章:https://blog.csdn.net/Hellowenpan/article/details/101112365?spm=1001.2014.3001.5501
二、为什么要使用SPI机制加载driver
- 主要原因是为了实现解耦和可插拔
- JDK提供数据库驱动的规范(即Driver接口)但不提供任何的实现,位于
java.sql
包下 - 各个不同的数据库厂商按照JDK提供的规范去进行不同的实现(比如MySQL驱动实现,Oracle数据库驱动实现)
- JVM启动的时候由启动类加载器加载
java.sql
包下的Driver接口类。 - 但是各个厂商所实现的数据库驱动在JVM启动的时候,并不知道该从哪儿去加载具体的驱动实现类。那么JDK进制定了一套规范:
- 你们各大数据库厂商要实现自己的数据库驱动,需要实现我JDK提供的
Driver
接口 - 并且将你们的实现类打成jar包,并且在你们jar包中的
META-INF/services
目录下创建一个以java.sql.Driver
命名的文件,文件中写上你们的驱动具体实现类名即可。 - 然后将你们的jar包放到程序的classpath下,我JVM启动的时候自有我的办法去把你们的驱动实现加载进来(这里所谓的办法就是 利用
SPI机制
加载classpath下的驱动实现)。
- 你们各大数据库厂商要实现自己的数据库驱动,需要实现我JDK提供的
- JDK提供数据库驱动的规范(即Driver接口)但不提供任何的实现,位于
三、使用SPI机制加载MySQL驱动源码分析
说明:关于什么是SPI以及SPI如何使用,这里不过多介绍,这里主要是分析JDK中是如何使用SPI机制
来实现可插拔的数据库驱动加载的。关于SPI的一些介绍,可参考:https://blog.csdn.net/Hellowenpan/article/details/101112365?spm=1001.2014.3001.5501
①、大体流程分析
- JVM启动的时候,首先由
启动类加载器
去加载JAVA_HOME/jre/lib
目录下的类 - 当加载到
DriverManager
类的时候会触发静态代码块的执行,在静态代码块中会执行loadInitialDrivers();
代码 - 在
loadInitialDrivers();
方法中会从启动类加载器
切换到应用类加载器
,然后通过应用类加载器
去加载classpath下的mysql驱动到内存中。应用类加载器
首先会读取并解析classpath下的MySQL驱动jar包中的META-INF/services/
目录下的配置文件,然后解析配置文件中的驱动类名- 根据上一步解析的驱动类名,使用
Class.forName()
将类加载进来,然后使用反射来为该Class创建一个实例对象
②、源码分析
1、DriverManager被启动类加载器加载
public class DriverManager {
static {
// 启动类加载器加载DriverManager类,触发静态方法执行
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
}
2、loadInitialDrivers触发加载
- 下面方法会创建
ServiceLoader
对象,使用ServiceLoader
对象来切换到上下文类加载器 - 重点需要关注
ServiceLoader
类和他的迭代器,核心逻辑就在这两个地方
private static void loadInitialDrivers() {
String drivers;
// ==========================================核心实现如下==========================================
// 加载java.sql.Driver驱动的实现
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 1、创建一个 ServiceLoader对象,【这里就将上下文类加载器设置到ServiceLoader对象的变量上了】,可自行查看
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 2、创建一个迭代器对象(这里只是创建,暂时不会涉及到迭代器的任何方法调用)
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 3、这里调用driversIterator.hasNext()的时候,触发将 META-INF/services 下的
// 配置文件中的数据读取进来,方便下面的next方法使用
while(driversIterator.hasNext()) {
// 4、【关键】:触发上面创建的迭代器对象的方法调用。这里才是具体加载的实现逻辑,非常不好找
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// ==========================================核心实现如上==========================================
}
3、META-INF/services下文件读取逻辑
我们知道如果要加载MySQL的驱动,那么需要读取MySQL的jar包下的META-INF/services配置文件
中的数据。那么整个流程中是在哪儿去读取的META-INF/services配置文件
呢??? 其实非常隐蔽,在迭代器的hasNext()
方法中触发的配置文件读取操作。源码如下
// 文件读取前缀
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 1、拼凑要读取的文件的全名
String fullName = PREFIX + service.getName();
// 2、根据 fullName 去到META-INF/services/目录下寻找配置文件
// 如果类加载器为空,则使用系统类加载器,如果不为空则使用指定的类加载器
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 由应用类加载器从classpath下的META-INF/services/目录下读取
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 3、使用parse方法解析配置文件中的每一行数据
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
4、使用反射创建对象逻辑
从上面一步分析中我们已经知道,META-INF/services配置文件是在hasNext()
方法被调用时,懒惰的被读取的。此时被读取进来的仅仅是一个驱动实现类类名,并不是实际的对象。那么是在什么地方将MySQL驱动类实例化的呢???其实就是在迭代器的next()
方法中,源码如下
public S next() {
// 可以看到next方法内部又调用了nextService方法,如下
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
// 通过classloader + 类名称 来加载驱动类
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
// 使用反射newInstance实例化一个对象
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);
}
}
5、其他
创建迭代器代码逻辑如下:
// 注意,这里lookupIterator是懒加载迭代器 LazyIterator
private LazyIterator lookupIterator;
public Iterator<S> iterator() {
// 创建一个迭代器(这里直接创建的是匿名对象)。后面调用迭代器遍历的时候就会吊起这里的方法
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
public boolean hasNext() {
// 先查缓存,如果缓存中存在,则直接返回true。很显然我们这里缓存中不存在
if (knownProviders.hasNext())
return true;
// 缓存中不存在,则调用lookupIterator.hasNext()。这里的lookupIterator的类型是【 LazyIterator 】
return lookupIterator.hasNext();
}
public S next() {
// 先查缓存,如果缓存中存在,则直接返回缓存中的值
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 缓存中不存在,则调用lookupIterator.next()去查找
return lookupIterator.next();
}
};
}
???? 注意:DriverManager本身是被启动类加载器加载的,只是在加载DriverManager类的时候会触发调用static方法,在static方法中使用的是SPI机制来切换到上下文类加载器,然后使用上下文类加载器来加载classpath下的MySQL驱动。
最后
以上就是乐观大象为你收集整理的使用SPI机制加载MySQL驱动源码分析的全部内容,希望文章能够帮你解决使用SPI机制加载MySQL驱动源码分析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复