从mysql-connector-java中学习
做过javaweb开发的同学,对mysql的使用一定不陌生。今天,我们来聊一聊jdbc连接mysql。
mysql的驱动注册
学习jdbc时,网上有一大堆例子,教你如何创建连接。大部分例子如下所示:
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
里到底有什么东西。
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驱动。猜想一下,也就是只有向驱动管理器注册的驱动才能被我们使用。那么,我们再看看驱动管理器做了什么事。
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的连接设计成驱动管理的好处是什么呢?我理解就是能够统一管理驱动,避免在一个项目中创建多个同样的驱动。那为什么要设置成在项目启动时动态加载驱动呢?原因有两个:
- 项目的测试环境和线上环境有一定差别,线上的驱动可能是在运行环境的classpath中的,如tomcat/lib。
- jdbc是java的一种规范,通俗一点说就是JDK在
java.sql.*
下提供了一系列的接口,但没有提供任何实现。对于在代码中写死Class.forName("com.mysql.jdbc.Driver")
也是没有解耦的,以后我们想换成别的驱动也不太方便,一般对实现类的加载都是写在properties中,如果需要更换驱动,我们只需要修改properties文件,重启项目就可以了。
获得Connection连接
如果你了解了驱动的注册,那Connection的获取就更简单了。让我们看看源码吧!
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的实现吧!
//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")
,笔者这里展示两种不用在代码里注册驱动也能正常加载驱动的方法。
- 在启动项目时指定虚拟机参数
-Djdbc.drivers=com.mysql.jdbc.Driver
- 使用mysql-connector-java版本>=
5.1.6
的jar包。
不信可以试一试。
为什么这样也可以正常加载驱动呢?再来看看驱动管理器和mysql-connector-java的jar吧
// 驱动管理器
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,笔者还会专门写一篇文章来介绍,欢迎大家来信探讨。