0%

数据库驱动如何对接到 JDBC

JDBC 作为数据库驱动的 SPI 接口,数据库驱动只需遵循与实现该接口,就可以通过 JDBC 使用该数据库。本文会对 MySQL 驱动进行分析,来探究 MySQL 驱动是如何与 JDBC 进行对接的。

使用 JDBC 获取数据库链接

先来看到 JDBC 获取数据库链接的例子,如下所示我们先引入 MySQL 驱动依赖,然后通过 DriverManagement#getConnection 方法传入 url、用户名和密码来获取数据库链接,拿到 Connection 之后就可以对数据库进行一些操作。

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.2</version>
</dependency>
1
Connection conn = DriverManager.getConnection("数据库url", "username", "password");

相信以上操作大部分读者都已经熟练掌握了,可能会有读者产生疑问,获取数据库链接前不是要通过 Class#fromName 来加载驱动嘛?其实对于该操作是可有可无的,Java 提供了一个 SPI (Service Provider Interface、服务提供接口)机制,而 MySQL 驱动就是使用该机制(META-INF/services 目录声明驱动)来让 DriverManagement 自动加载 MySQL 驱动。

Driver 接口

Driver 接口作为 JDBC 一个核心接口,基于 JDBC 的数据库驱动都要去实现该接口。Driver 接口的行为也很明确,可以通过该接口来链接数据库,还可以获取数据库的版本号。我们想象一个场景,如果当前项目引入了多个数据库的驱动,这些驱动会通过 SPI 机制来进行注册,那么通过 DriverManagement#getConnection 获取数据库链接时,该如何通过 url 来从这些驱动中找到可以处理的驱动呢?针对这一点需求,Driver 接口该如何进行设计?Driver 接口提供一个 acceptsURL 方法用于判断该 url 是否由这个驱动进行处理,从这一点可以看出,对于接口的设计需要职责分明(该接口需要做什么事情,就赋予该接口什么行为),这也是面对对象编程带来的好处之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;

boolean acceptsURL(String url) throws SQLException;

DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;

int getMajorVersion();

int getMinorVersion();

boolean jdbcCompliant();

public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

MySQL驱动如何通过SPI进行注册

MySQL 中 com.mysql.cj.jdbc.Driver 类继承了 NonRegisteringDriver 类并实现了 java.sql.Driver 接口,在该类中的 static 代码块中通过 DriverManager#registerDriver(java.sql.Driver) 方法来将驱动注册到 DriverManager 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// com.mysql.cj.jdbc.Driver
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

SPI 机制就是通过一个服务发现机制,来对具体的接口的实现类查找并进行初始化,该特性满足了我们驱动注册的需求,在初始化时会调用该 static 代码块从而来实现驱动的注册。

下列对 MySQL 的 SPI 机制进行分析,MySQL 驱动包下的 META-INF/services 目录下创建了一个 java.sql.Driver 文件,文件中内容是该文件名的实现类的全限定名称。也就是说如果使用 SPI 机制,那么需要在 META-INF/services 目录下创建一个 SPI 接口全限定名的文件,然后在文件中输入该接口实现类的全限定名称,如有该接口多个实现类那么换行输入即可。

1
2
// java.sql.Driver 文件内容
com.mysql.cj.jdbc.Driver

DriverManagement#getConnection 方法解读

DriverManagement 翻译成中文是“驱动管理”,也就是用于管理驱动。加载驱动的来源有两种,一种是通过系统的 jdbc.drivers属性,另外一种通过 SPI 机制。我们思考一下,DriverManagement 会在何时加载驱动呢?首先可以通过类的初始化机制来进行实现,也就是在 static 静态代码块中进行加载,这种方式称为及时初始化。另外一种方式在获取链接时来通过某个操作加载,也就是按需加载,这种方式称为懒加载。两种方式各有各的优点,及时初始化是空间换时间,而懒加载是时间换空间,我们需要根据业务的需求来选择合适的初始化方式。对于 Web 容器它也采用了懒加载机制,只有在第一次访问时才对 DispatcherServlet 组件来进行初始化。如果提前进行初始化,倘若用户没有访问 web 服务,那么就会造成一个资源的浪费。如果用户肯定会访问 web 服务,我们也可以选择及时初始化,提前来将 DispatcherServlet 组件进行初始化,从而消除第一次访问初始化组件的时间。及时初始化或懒加载并不是万能的银弹,并不是说某种方式一定要优于对方,这两种方式只是为了解决特定场景的需求。

通过 DriverManager#getConnection 方法获取数据库链接时,传入数据库url、用户名与密码。在该方法中会将用户名与密码作为属性添加到 properties 中,添加之前会对用户名与密码来进行校验,这也是为了提高程序的健壮性。倘若此时不进行校验,将值null的属性进行添加,那么在后续的操作还是要进行校验的,这会导致方法的职责不清晰。之后调用该 getConnection 重载方法,我们看到该重载方法的参数,它最后一个参数接收一个类的字节码对象,而 Reflection.getCallerClass() 是用于获取调用该方法的字节码对象,也就是说我们需要拿到调用 getConnection(String url, String user, String password) 方法字节码对象然后传给该 getConnection 重载方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// java.sql.DriverManager#getConnection(java.lang.String, java.lang.String, java.lang.String)
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();

if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}

return (getConnection(url, info, Reflection.getCallerClass()));
}

进入 getConnection 重载方法,首先获取该字节码的 ClassLoader,该 ClassLoader 是为了校验数据库驱动。之后调用 ensureDriversInitialized 方法来加载驱动,正如我们前面所料,DriverManagement 也是使用懒加载机制进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
callerCL = Thread.currentThread().getContextClassLoader();
}

if (url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

ensureDriversInitialized();

// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for (DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}


}

然后进入 ensureDriversInitialized 方法,该方法通过 driversInitialized 标志位来知道数据库驱动是否被加载了。前面我们知道 Drivermanagement 加载驱动的方式有两种,那么针对这两种方式,该方法肯定有具体的实现。看到 12 行,会获取系统的 jdbc.drivers 属性赋值给 drivers 变量,之后并不是先对该来源的驱动进行初始化,而是先对 SPI 机制进行初始化。28 行通过 ServiceLoader.load(Driver.class) 来加载所有的 java.sql.Driver SPI 接口的实现类,因为我们能拿到该实现类的对象,那么该对象字节码肯定被加载到 JVM 中,该字节码中的 static 代码块会被调用,从而将驱动注册到 DriverManagement 中。在 56 行会对系统属性来源的驱动进行加载,首先进行校验的操作,如果该值为 null 或为空,那么就没有必要进行加载了。之后根据 :分隔符来进行分割,拿到一个 String 数组,数组中的元素是 java.sql.Driver 接口实现类的全限定名,最后通过 Class#forName(String) 方法来加载相应的数据库驱动。等到全部的驱动都加载完毕之后,最后将 driversInitialized 标志位设置为 true,防止下次调用 ensureDriversInitialized 方法时再次加载驱动,从而导致资源的浪费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
private static void ensureDriversInitialized() {
if (driversInitialized) {
return;
}

synchronized (lockForInitDrivers) {
if (driversInitialized) {
return;
}
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty(JDBC_DRIVERS_PROPERTY);
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});

println("DriverManager.initialize: jdbc.drivers = " + drivers);

if (drivers != null && !drivers.equals("")) {
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

driversInitialized = true;
println("JDBC DriverManager initialized");
}
}

执行完 ensureDriversInitialized 方法,我们再次进入 getConnection 重载方法,看到该方法的 26 行,代码如下所示。首先会遍历 registeredDrivers (注册驱动就是往 registeredDrivers 添加 DriverInfo 对象 ),之后在第 4 行调用 isDriverAllowed 方法进行校验,上述我们提到该 callerCL 是调用 DriverManagement#getConnection 方法的对象的字节码的 ClassLoader ,在 isDriverAllowed 方法中(该方法第7行)使用 callerCL ClassLoader 重新加载一遍驱动,如果加载后的字节码与驱动的字节码对象相符,那么说明该驱动校验通过了,倘若没有校验通过,那么就跳过这个驱动即可。

校验通过之后在下列代码的第 7 行,通过驱动的 Driver#connect(String, Properties) 方法来链接数据库,此时 JDBC 就与 MySQL 数据库驱动对接上了,具体的链接流程可以参考 MySQL 数据库驱动的实现。驱动的本质是客户端与数据库的通讯协议,将客户端查询的 sql 语句封装成一个packet包(二进制数据)并发送给数据库进行处理,客户端对数据库返回的消息进行解析得到一个查询结果。对于链接数据库流程是先与数据库建立一个 Socket 链接,之后通过用户名与密码封装成一个 packet 传输给数据库从而校验客户端的身份,身份校验成功之后,此时才算真正与数据库建立起了链接(connection),对于具体的实现细节还需读者自行探究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// java.sql.DriverManager#isDriverAllowed(java.sql.Driver, java.lang.ClassLoader)
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if (driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}

result = ( aClass == driver.getClass() ) ? true : false;
}

return result;
}

尾语

本文对 MySQL 数据库驱动对接 JDBC 流程进行了一个大致的分析,在阅读源码过程中能学习到 JDK 中优秀的设计思路,并且我们可以将这些优秀的设计思路来落地到我们的项目中。阅读源码不光能学到优秀的设计思路,还能达到知其然知其所以然效果,懂得了具体的实现原理,对于编写代码也会事半功倍。