首先先来了解一下数据库驱动的加载过程:
数据库驱动加载的过程
我们先来看看Java中SPI定义的一个核心类:DriverManager
,该类位于rt.jar
包中,是Java中用于管理不同数据库厂商实现的驱动,同时这些各厂商实现的Driver
驱动类,都继承自Java的核心类java.sql.Driver
,如MySQL的com.mysql.cj.jdbc.Driver
的驱动类
DriverManager类位于rt.jar包中,由启动类加载器加载。
我们导入的Mysql的jar包或依赖中的mysql驱动对应的类,由应用程序类加载器来加载
DriverManager属于rt.jar是启动类加载器加载的。而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制。(这点存疑,一会儿再讨论)
那么问题来了,DriverManager怎么知道jar包中要加载的驱动在哪儿?
1、在类的静态代码块中有这么一个方法LoadInitialDrivers
:
2、这里使用了SPI机制,去加载所有jar包中实现了Driver接口的实现类。(SPI机制不明白的可以看我的这篇文章:SPI机制)
3、SPI机制就是在这个位置下存放了一个文件,文件名是接口名,文件里包含了实现类的类名。这样SPI机制就可以找到实现类了。如果加载数据库的话,文件名就必须是java.sql.Driver
,文件内写上驱动类的全类名 com.mysql.cj.jdbc.Driver
4、SPI中利用了线程上下文类加载器(应用程序类加载器) 去加载类并创建对象。
总结:
线程上下文类加载器是什么时候放进去的?
在前面我们分析Java中的双亲委派实现时,曾提到了Ext、App
类加载器都是Launcher
类的内部类,Ext、App
类加载器的初始化操作都是在Launcher
的构造函数中完成的,同时,在该构造函数中,Ext、App
初始化完成后,会执行下面这句代码: Thread.currentThread().setContextClassLoader(loader);
// sun.misc.Launcher类
public class Launcher {
// sun.misc.Launcher类 → 构造器
public Launcher(){
Launcher.ExtClassLoader var1;
try {
// 会先初始化Ext类加载器并创建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 再创建AppClassLoader并把Ext作为父加载器传递给App
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError("Could not create application class loader");
}
// 将APP类加载器设置为线程上下文类加载器
Thread.currentThread().setContextClassLoader(loader);
// 省略......
}
通过如上这句代码,在Launcher
的构造函数中,会将已经创建好的AppClassLoader
系统类加载器设置为默认的线程上下文类加载器。
分析到了这个地方后,可能有部会有些绕,我们稍微梳理一下总体流程:
Java程序启动 → JVM初始化C++编写的
Bootstrap
启动类加载器 →Bootstrap
加载Java核心类(核心类中包含Launcher
类) →Bootstrap
加载Launcher
类,其中触发Launcher
构造函数 →Bootstrap
执行Launcher
构造函数的逻辑 →Bootstrap
初始化并创建Ext、App
类加载器 →Launcher
类的构造函数中将Ext
设置为App
的父类加载器 → 同时再将App
设置为默认的线程上下文类加载器 →Bootstrap
继续加载其他Java核心类(如:SPI接口) → SPI接口中调用了第三方实现类的方法 →Bootstrap
尝试去加载第三方实现类,发现不在自己的加载范围内,无法加载 → 依赖于SPI的动态服务发现机制,这些实现类会被交由线程上下文类加载器进行加载(在前面讲过,线程上下文加载器在Launcher
构造函数被设置为了App
类加载器) → 通过App
系统类加载器加载第三方实现类,发现这些实现类在App
的加载范围内,可以被加载,SPI接口的实现类加载完成…
加载流程如上,很明显的就可以感觉出来,线程上下文类加载器介入后,轻而易举的打破了原有的双亲委派模型,同时,也正是因为线程上下文类加载器的出现,从而使得Java的类加载器机制更加灵活,方便。
好了,上面分析了数据库驱动的加载过程,JDBC的案例可以说是介绍打破双亲委派机制的最好案例了。
刚才我也说到JDBC案例打破了双亲委派机制,几乎网上能搜到的文章也都说JDBC打破了双亲委派机制。但是,它整的打破了双亲委派机制吗?
问题:JDBC的案例中真的打破了双亲委派机制吗?
支持打破了双亲委派机制的理由:
先由启动类加载器去加载DriverManger,又使用了应用类加载器去加载了驱动类。但是这个过程并没有按照从上到下依次加载的过程,而是通过线程上下文类加载器获取到应用类加载器进行加载的。这是不符合双亲委派机制的加载流程的。正常的加载流程是 从下至上委派,从上至下加载。这个过程并没有从上至下加载,所以打破了双亲委派机制。
支持没有打破双亲委派机制的理由:
JDBC只是在DriverManger加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制
我们先来看看Java中SPI定义的一个核心类:
DriverManager
,该类位于rt.jar
包中,是Java中用于管理不同数据库厂商实现的驱动,同时这些各厂商实现的Driver
驱动类,都继承自Java的核心类java.sql.Driver
,如MySQL的com.mysql.cj.jdbc.Driver
的驱动类。先看看DriverManager
的源码,如下:
// rt.jar包 → DriverManager类
public class DriverManager {
// .......
// 静态代码块
static {
// 加载并初始化驱动
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
// DriverManager类 → loadInitialDrivers()方法
private static void loadInitialDrivers() {
// 先读取系统属性 jdbc.drivers
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类查找驱动类的文件位置并加载
ServiceLoader<Driver> loadedDrivers =
ServiceLoader.load(Driver.class);
//省略......
}
});
//省略......
}
观察如上源码,在
DriverManager
类的静态代码块中调用了loadInitialDrivers()
方法,该方法中,会通过ServiceLoader
查找服务接口的实现类。也就是说执行到了DriverManager
的静态代码块时才触发了对数据库驱动类的加载,也就是在加载DriverManager
的初始化阶段进行的。
整体分析
-
打破双亲委派机制的分析
- 加载流程对比:在双亲委派机制中,类加载是遵循 “从下至上委派,从上至下加载” 的原则。对于 JDBC 中 DriverManager 和驱动类的加载,如果启动类加载器加载 DriverManager,而应用类加载器加载驱动类且不是按照正常双亲委派的流程,这确实存在与双亲委派机制冲突的地方。
- 线程上下文类加载器的影响:当通过线程上下文类加载器获取应用类加载器来加载驱动类时,这种绕过了常规双亲委派流程的方式是关键证据。因为双亲委派机制旨在保证类加载的层级顺序,这种跨层级的加载行为可能导致同一个类在不同的类加载器中存在多个副本,破坏了双亲委派机制所期望的类加载的一致性和层级性。
-
未打破双亲委派机制的分析
- 加载触发角度:从这个角度看,如果认为 JDBC 只是在 DriverManager 加载完成后,通过初始化阶段触发驱动类的加载,那么在每个类加载的具体过程中可能仍然遵循双亲委派机制。即驱动类在被触发加载时,可能还是会从下至上先检查父类加载器是否已经加载过该类,再决定由哪个类加载器进行加载。
- 整体机制的遵循:支持者可能认为虽然加载触发的方式看似特殊,但整体的类加载规则,比如类加载器之间的层级关系和加载顺序的内在逻辑在每个独立的类加载动作中没有被破坏,只是整个加载过程的触发机制比较复杂,容易被误解为打破了双亲委派机制
-
持有打破双亲委派机制观点的更加注重类的加载过程,必须遵循 “从下至上委派,从上至下加载” 的原则;
-
持有没有打破双亲委派机制观点的更加注重类加载器之间的层级关系和加载顺序的内在逻辑。
关于JDBC是否打破双亲委派机制的问题,也是很值得讨论的~~
标签:委派,DriverManager,JDBC,Launcher,---,双亲,JVM,机制,加载 From: https://blog.csdn.net/weixin_73205368/article/details/143824414