为什么JDBC需要打破双亲委派机制
JDBC的DriverManager与SPI机制
类加载的机制以及双亲委派机制的介绍可以参考 JVM类加载机制
在JDBC 4.0之后,我们不再需要调用Class.forName()方法去加载驱动类。只需要将对应的驱动类jar包放到工程的class path下,驱动类会自动被加载。
这种自动加载的技术被称为SPI(Service Privider Interface)[3],SPI可以简单理解为:为了解耦,从配置里获取某个接口的具体实现类。各个数据库也都更新支持了这个特性。包括MySQL-JDBC等,每个JDBC的jar包里都有一个META-INF/services
目录,里面有一个 java.sql.Driver
文件,里面指定了这个driver的实现类的全限定名。
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
有了这个技术,我们在使用JDBC时,只需要在代码里如下这么写,就可以获取到对应的JDBC连接,完全无需指定是MySQL还是Oracle的DBMS:
Connection con = DriverManager.getConnection(url , username , password);
复制代码
DriverManager的类加载问题
类加载的范围受到限制,某些情况下父class loader无法加载某些类文件,这时候就需要委托到下层级的class loader去加载类文件。 [1]
JDBC的driver接口定义在JDK中,但是它的实现类是放在classpath下的(比如MySQL)。
- DriverManager类会加载每个Driver接口的实现类并管理它们,但是DriverManager类自身是
jre/lib/rt.jar
里的类,是由bootstrap classloader加载的 - 根据类加载机制,某个类需要引用其它类的时候,虚拟机将会用这个类的classloader去加载被引用的类
- boostrap classloader显然是无法加载到MySQL driver的(ClassNotFoundException)
- 因此只能在DriverManager里强行指定下层classloader来加载Driver实现类,而这就会打破双亲委派模型
JDBC打破双亲委派的实现方式
DriverManager加载Driver的过程
JDK8及之前版本
通过查看DriverManager类的代码可以看到,当我们使用DriverManager的时候就会触发static代码块,进而会加载 META-INF/services/java.sql.Driver
指定的类。[2]
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() { // 1. AccessController,Java安全模型
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 2. 核心,ServiceLoader就是JDK提供的SPI的实现方式
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next(); // 3. 遍历的过程会触发每个Driver实现类的加载
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
复制代码
我们分析注释中每一步的作用:
- 1、AccessController,Java安全模型 AccessController.doPrivileged的作用,AccessController.doPrivileged - 蹲厕所的熊
- 2、核心, ServiceLoader 就是JDK提供的SPI的实现方式, 更多ServiceLoader介绍
- 3、遍历的过程会触发每个Driver实现类的加载
ServiceLoader.load方法会用context class loader来根据配置加载对应的类
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
复制代码
JDK9及之后的版本
笔者的机器上装的是JDK16,从注释还有源码中可以看出,最新的DriverManager加载驱动实现类的过程不是上述static代码块的方式,而是在getConnection的时候 懒加载 的方式去执行上述遍历过程。
不过除了从 饿汉模式 变为 懒汉模式 以外,加载实现类的过程和原理没有太大改动。
什么是context class loader
public Launcher() {
...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}
复制代码
如代码所示,当 sun.misc.Launcher 初始化的时候,AppClassLoader就会被获取并且设置到Thread类的成员变量里,因此 Thread.currentThread().getContextClassLoader()
获取的默认就是系统类加载器,当然开发者可以自行更改。
总结
- 由于类加载机制存在的 可见性 问题,bootstrap classloader无法加载用户的jar包
- 但是用于装载JDBC驱动实现类的 DriverManager 类是JDK核心类,而被装载的类是用户类,导致无法加载的尴尬问题
- 所以需要用Context Class Loader来加载Driver实现类,从而打破了双亲委派模型
近期评论