引子
让我们来看一段使用 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 一家发扬光大,此处先按下不表。