从mysql-connector-java中学习

做过javaweb开发的同学,对mysql的使用一定不陌生。今天,我们来聊一聊jdbc连接mysql。

mysql的驱动注册

学习jdbc时,网上有一大堆例子,教你如何创建连接。大部分例子如下所示:

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
Connection conn = null;

String sql;

// MySQL的JDBC URL编写方式:jdbc:mysql://主机名称:连接端口/数据库的名称?参数=值

// 避免中文乱码要指定useUnicode和characterEncoding

String url = "jdbc:mysql://localhost:3306/test?"

+ "user=root&password=root&useUnicode=true&characterEncoding=UTF8";

try {

// 之所以要使用下面这条语句,是因为要使用MySQL的驱动,所以我们要把它驱动起来,

// 可以通过Class.forName把它加载进去,也可以通过初始化来驱动起来,下面三种形式都可以

Class.forName("com.mysql.jdbc.Driver");// 动态加载mysql驱动

// or:

// com.mysql.jdbc.Driver driver = new com.mysql.jdbc.Driver();

// or:

// new com.mysql.jdbc.Driver();

System.out.println("成功加载MySQL驱动程序");

// 一个Connection代表一个数据库连接

conn = DriverManager.getConnection(url);

// Statement里面带有很多方法,比如executeUpdate可以实现插入,更新和删除等

Statement stmt = conn.createStatement();

// Do something

}

} catch (SQLException e) {

System.out.println("MySQL操作错误");

e.printStackTrace();

} catch (Exception e) {

e.printStackTrace();

} finally {

try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}

}

上边的例子中,我们需要加载mysql驱动。Class.forName("com.mysql.jdbc.Driver")。这是为什么呢?让我们看看com.mysql.jdbc.Driver里到底有什么东西。

1
2
3
4
5
6
7
8
9
10
11
12
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}

static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}

在代码中,我们发现有一个静态代码块,字面意思就是向驱动管理器注册mysql驱动。猜想一下,也就是只有向驱动管理器注册的驱动才能被我们使用。那么,我们再看看驱动管理器做了什么事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
registerDriver(driver, null);
}

public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}

很简单吧,就是如果CopyOnWriteArrayList写时复制列表中没有该驱动,就向其中添加添加驱动。

将jdbc的连接设计成驱动管理的好处是什么呢?我理解就是能够统一管理驱动,避免在一个项目中创建多个同样的驱动。那为什么要设置成在项目启动时动态加载驱动呢?原因有两个:

  1. 项目的测试环境和线上环境有一定差别,线上的驱动可能是在运行环境的classpath中的,如tomcat/lib。
  2. jdbc是java的一种规范,通俗一点说就是JDK在java.sql.*下提供了一系列的接口,但没有提供任何实现。对于在代码中写死Class.forName("com.mysql.jdbc.Driver")也是没有解耦的,以后我们想换成别的驱动也不太方便,一般对实现类的加载都是写在properties中,如果需要更换驱动,我们只需要修改properties文件,重启项目就可以了。

获得Connection连接

如果你了解了驱动的注册,那Connection的获取就更简单了。让我们看看源码吧!

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
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {
return (getConnection(url, info, Reflection.getCallerClass()));
}


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;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

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

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

// 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");
}

因为驱动都在CopyOnWriteArrayList写时复制列表,我们只要遍历列表中的驱动,只要其中一个驱动能够给我们连接,那我们就得到了连接,如果都没得到连接,那就要报错了。

至于驱动的实现是如何判断能否给出连接,那就凭各自驱动的本事了,什么意思呢?在上边的代码中,我们看到给驱动的有效参数就两个,一个是url,一个是info。驱动的实现需要靠这两个参数决定是否生成连接。

我们来看看mysql的实现吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//com.mysql.jdbc.NonRegisteringDriver
Properties props = null;
if ((props = this.parseURL(url, info)) == null) {
return null;
} else {
try {
com.mysql.jdbc.Connection newConn = ConnectionImpl.getInstance(this.host(props), this.port(props), props, this.database(props), url);
return newConn;
} catch (SQLException var6) {
throw var6;
} catch (Exception var7) {
SQLException sqlEx = SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + var7.toString() + Messages.getString("NonRegisteringDriver.18"), "08001");
sqlEx.initCause(var7);
throw sqlEx;
}
}

我们看到,mysql的实现中,会先解析url和info,生成props,如果没有生成,那说明给定的url不能被mysql的驱动解析。

再谈驱动注册

我们在第一节中说到,必须要注册驱动Class.forName("com.mysql.jdbc.Driver"),笔者这里展示两种不用在代码里注册驱动也能正常加载驱动的方法。

  1. 在启动项目时指定虚拟机参数-Djdbc.drivers=com.mysql.jdbc.Driver
  2. 使用mysql-connector-java版本>=5.1.6的jar包。

不信可以试一试。

为什么这样也可以正常加载驱动呢?再来看看驱动管理器和mysql-connector-java的jar吧

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
// 驱动管理器

static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} 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("")) {
return;
}
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);
}
}
}

驱动管理器中提供了两种默认加载驱动的方法,一个就是通过虚拟机参数jdbc.drivers来加载驱动。另一个是通过java的spi接口加载驱动。第一种方法比较简单就不用探讨了,第二种方法我们来具体了解一下。

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。有很多开源框架都用到了这种服务或者是这种服务的自定义扩展。它允许在JAR包的”src/META-INF/services/“目录下建立一个文件,文件名是接口的全限定名,文件的内容可以有多行,每行都是该接口对应的具体实现类的全限定名。

再来看看我们的jar包,在版本大于等于5.1.6的jar包中,有这么一个文件:mysql-connector-java\5.1.6\mysql-connector-java-5.1.6.jar!\META-INF\services\java.sql.Driver。在这个文件中,就是Driver接口实现类的全限定名com.mysql.jdbc.Driver

关于SPI,笔者还会专门写一篇文章来介绍,欢迎大家来信探讨。