JDBC是如何打破双亲委派模式的

为什么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;
        }
    });
}
复制代码

我们分析注释中每一步的作用:

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实现类,从而打破了双亲委派模型

参考

  1. java - Why the book says JDBC damage the Parental delegation model? - Stack Overflow
  2. Why do JDBC and Tomcat destroy the parental delegation model? - Katastros
  3. JDK/Dubbo/Spring 三种 SPI 机制,谁更好? - SegmentFault 思否
  4. Tech 101 ‐ AccessController.doPrivileged的作用