JDBC 驱动加载过程源码浅析

307 阅读3分钟

引子

让我们来看一段使用 jdbc 读取 MySQL 数据库中 database 为 users,数据表为 profile 的代码:

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/users", "root", "123456");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select name from profile");
while (resultSet.next()) {
    String title = resultSet.getString(0);
    System.out.println(title);
}

resultSet.close();
statement.close();
connection.close();

代码比较简单,总的过程分为以下三部分:

  • 创建连接
  • 创建 Statement
  • 执行 SQL
  • 获取结果集
  • 释放连接,关闭资源

当然,在使用这段代码之前,需要引入 MySQL 驱动:mysql-connector-java-xxxx.jar,具体引入方式有很多种,此处不进行赘述。

不知道各位有没有好奇:我们读取的是 MySQL 中的表,需要的也是 MySQL 的驱动,但是在真正的读取执行逻辑中,却没有发现任何对驱动代码进行显式的调用,这是怎么做到的呢?

本文尝试通过源码的方式,对此问题进行一个浅显的回复,如有不对的地方,还请方家批评指正。

说明一点,本文基于 JDK 19 版本进行分析。

驱动注册

首先看连接的获取过程。

获取连接时,需要传入的参数为 url、username、password。

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");

大家只要点击进入 getConnection 方法,一直往下走,就能看到下面的逻辑。

第 677 行这里是直接从 registeredDrivers 开始循环遍历,加载驱动,然后在 682 行去创建真正的数据库连接,而 registeredDrivers 的定义如下:

// List of registered JDBC drivers
private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

那么问题来了?

registeredDrivers 是怎么来的呢?我们可以进行合理猜想,肯定在某个地方,有一个注册逻辑,将所有的驱动类信息注册到此数据结构中。

我们将焦点上移至 671 行:ensureDriversInitialized 。 让我们进入该方法一探究竟。

这里涉及到一个关键类:ServiceLoader。该类的作用就是返回一个驱动器的迭代器,然后在后面进行遍历初始化。接着,进入 603 行:

到 1368 行,继续往里走。

newLookupIterator 方法返回一个匿名的迭代器,然后委托 LazyClassPathLookupIterator 和 ModuleServicesLookupIterator 执行迭代的逻辑。

我们看 LazyClassPathLookupIterator 的 hasNext 方法里面执行了什么?

继续往里走。

继续。

图穷匕见,这里面我们重点看地 1191 行。看看 fullName 是什么?

所以,逻辑是从 fullName 这个文件读取内容,走到 1213 行进行解析,在 1217 中进行类加载。

注意:上面的逻辑都是在 JDK 中,还没有涉及到驱动的任何代码。

这其实是利用了一个 Java 中常见的模式:SPI(Service Provider Interface,服务发现模式)。JDK 与其他服务提供者约定好,在服务 lib 的 META-INF/services/ 下,放置你需要加载的全限定类名即可,剩下的工作我们来做。

我们可以看下我们前面引入的 MySQL 驱动 jar 包:

在 META-INF/services/ 路径下,果然存在一个名为 java.sql.Driver 的文件,其内容为:com.mysql.cj.jdbc.Driver。这个类名就是 MySQL 的驱动。

JDK 会直接加载 com.mysql.cj.jdbc.Driver 类。因此,我们在此可以大胆猜想,一定是在 com.mysql.cj.jdbc.Driver 类的初始化或者其引发的逻辑中,进行了驱动的注册。

果不其然!

在该类的 static 块中,进行了驱动的注册。而此类本身确实实现了 java.sql.Driver 接口,我们看下此接口:

Driver 接口中一个重要的核心方法就是 connect。

public interface Driver {
    Connection connect(String url, java.util.Properties info) throws SQLException;
    .....
}

接口设计地非常简洁,没有任何冗余的信息。

总结

JDK 利用了 SPI 模式,对数据库驱动类进行加载初始化,引发其调用 JDK 中的 DriverManager 类的 register 方法进行注册。这里的 SPI 模式,不仅仅适用于数据库驱动的加载。所有其他不希望与 JDK 耦合的服务提供者,皆可以采用类似的模式进行。这种模式,后来被 Spring 一家发扬光大,此处先按下不表。