JDBC获取连接对象

mysql

我们也听说过 Java 的面向接口编程,即官方提供一系列接口给第三方去实现,然后利用接口去调用第三方的实现类,如果有多个第三方都实现了该接口,按摩对于官方来说,都可以利用该接口调用这些第三方的实现类,且不关心第三方是如何具体实现的,我只管调用就行。

JDBC 正是利用了接口编程思想,JDBC只是接口,JDBC驱动才是该接口的实现类,每个数据库都有其对应的 JDBC 驱动,没有 JDBC 驱动是没有办法连接数据库的!

JDBC

JDBC接口核心的API

Driver接口

每个驱动程序必须实现的接口,每个驱动程序都应该提供一个实现 Driver 接口的类,它有一个很重要的方法:

Connection connect(String url, Properties info) throws SQLException

此方法是连接数据库的方法,并返回一个与数据库已连接的连接对象;参数 url 表示连接数据的地址,其写法:
jdbc协议:数据库子协议://主机: 端口/数据库,如 MySql 的数据库地址可以写成:

url: jdbc:mysql://localhost:3306/test

info 封装了连接数据库的用户名和密码,也就是说该方法是 Java 与数据库连接的桥梁。

DriverManage 类

顾名思义,就是数据库驱动的管理类,负责管理是由注册的驱动程序,主要有如下方法:

static void registerDirver(Dirver dirver); // 这是注册驱动的方法
static Connection getConnection(String url, String user, String password); // 获取连接对象

此方法内部调用了驱动程序实现了 Driver 接口的 Connection connection(String url, Properties info) 方法,从而获得连接对象。

Connection接口

表示 java 连接数据库的对象,并可执行 sql 语句并返回结果,其主要方法:

Statement createStatement(); // 创建 Statement 对象
PreparedStatement preparedStatement(String sql) // 创建 PreparedStatement 对象

第二个方法用得最多,因为它可以预编译 sql 语句,这大大节省了数据库的开销。
而 PreparedStatement 接口的主要方法如下:

int executeUpdate(); // 执行预编译的更新sql语句 (DDL, DML)
ResultSet executeQuery(); // 执行预编译的查询 sql 语句 (DQL)

获取连接对象源码分析

DriverManager 源码

DriverManager 启动管理类可以注册所有实现了 JDBC 接口的数据库启动,并通过 DriverManager.getConnection() 方法启动获取与之相对应的数据库的连接对象,这就是面向接口编程的体现。

我们从入口 DriverManager.getConnection() 开始进入驱动源码的世界:

publicclass DriverManager { // 此处省略部分代码 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())); } private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws SQLException { // 获取类加载器 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; // 同步块,获取当前线程的类加载器 synchronized(DriverManager.class) { if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } } // 此处省略部分代码 // 这里遍历的是在registerDriver(Driver driver)方法中注册的驱动对象 // 每个DriverInfo包含了驱动对象和其信息 for(DriverInfo aDriver : registeredDrivers) { // 判断是否为当前线程类加载器加载的驱动类 if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); // 获取连接对象,这里调用了Driver的父类的方法 Connection con = aDriver.driver.connect(url, info); if (con != null) { // 打印连接成功信息 println("getConnection returning " + aDriver.driver.getClass().getName()); // 返回连接对像 return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } // 此处省略部分代码 } }

MySql Driver 驱动源码

Mysql 的 Driver 实现类(此类主要任务是注册驱动):

public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); // 注册驱动 } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } }

由于 mysql 数据库驱动类 com.mysql.jdbc.Driver 在获取连接对象之前要加载到 jvm 中,所以在获取对象之前驱动类已注册到驱动管理类中了,加载驱动如下:

Class.forName("com.mysql.jdbc.Driver")

执行这行代码后,驱动类的静态代码块随之执行,也就把驱动注册了。但是高级版本的 MySql 已经不需要执行这句代码了,把驱动类名卸载相应的配置文件里,当 jvm 加载 DriverManager 类时会自动执行类中今天代码块加载驱动,源码如下:

priver DriverManager(){} static { loadInitialDrivers(); printIn("JDBC DriverManager initialized"); }

从上面可看出,DriverManager 初始化时会执行静态代码块中的代码,loadInitialDrivers() 是用于加载外部实现的驱动类的方法。
其父类(此类主要任务是实现连接细节):

public class NonRegisteringDriver implements java.sql.Driver { // 此处省略部分代码 // 实现connect方法 public java.sql.Connection connect(String url, Properties info) throws SQLException { // 此处省略部分代码 try { // 这里获取具体的链接 类是ConnectionImpl,调用其getInstance方法创建并返回Connection对象 Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance( host(props), port(props), props, database(props), url); return newConn; } catch (SQLException sqlEx) { // 此处省略部分代码 } catch (Exception ex) { // 此处省略部分代码 } } // 此处省略部分代码 }

JDBC的反射及他的作用

通过反射 com.mysql.jdbc.Driver 类,实例化该类的时候会执行该类内部的静态代码块,该代码块会在 java 实现的 DriverManager 类中注册自己,DriverManager 管理所有已经注册的驱动类,当调用 DriverManager.getConnection 方法时会遍历这些驱动类,并尝试去连接数据库,只要有一个能连接成功,返回 Connection 对象,否则则报异常。

MySql connection 接口实现源码

mysql 的 ConnectionImpl 类实现了 Connection 接口,getInstace 方法的细节如下:

// 参数有: 主机 端口号 properties database url protected static Connection getInstance(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException { // 调用util 类的方法判断,驱动类是否能够找到 // 创建ConnectionImpl对象 if (!Util.isJdbc4()) { return new ConnectionImpl(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url); } return (Connection) Util.handleNewInstance(JDBC_4_CONNECTION_CTOR, new Object[] { hostToConnectTo, Integer.valueOf(portToConnectTo), info, databaseToConnectTo, url }, null); }
ConnectionImpl类构造方法:
public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException { this.connectionCreationTimeMillis = System.currentTimeMillis(); if (databaseToConnectTo == null) { databaseToConnectTo = ""; } // Stash away for later, used to clone this connection for Statement.cancel and Statement.setQueryTimeout(). // this.origHostToConnectTo = hostToConnectTo; // host this.origPortToConnectTo = portToConnectTo; //port this.origDatabaseToConnectTo = databaseToConnectTo; //数据库名 try { Blob.class.getMethod("truncate", new Class[] { Long.TYPE }); this.isRunningOnJDK13 = false; } catch (NoSuchMethodException nsme) { this.isRunningOnJDK13 = true; } this.sessionCalendar = new GregorianCalendar(); this.utcCalendar = new GregorianCalendar(); this.utcCalendar.setTimeZone(TimeZone.getTimeZone("GMT")); // // Normally, this code would be in initializeDriverProperties, but we need to do this as early as possible, so we can start logging to the 'correct' // place as early as possible...this.log points to 'NullLogger' for every connection at startup to avoid NPEs and the overhead of checking for NULL at // every logging call. // // We will reset this to the configured logger during properties initialization. // this.log = LogFactory.getLogger(getLogger(), LOGGER_INSTANCE_NAME, getExceptionInterceptor()); this.openStatements = new HashMap<Statement, Statement>(); if (NonRegisteringDriver.isHostPropertiesList(hostToConnectTo)) { Properties hostSpecificProps = NonRegisteringDriver.expandHostKeyValues(hostToConnectTo); Enumeration<?> propertyNames = hostSpecificProps.propertyNames(); while (propertyNames.hasMoreElements()) { String propertyName = propertyNames.nextElement().toString(); String propertyValue = hostSpecificProps.getProperty(propertyName); info.setProperty(propertyName, propertyValue); } } else { if (hostToConnectTo == null) { this.host = "localhost"; this.hostPortPair = this.host + ":" + portToConnectTo; } else { this.host = hostToConnectTo; if (hostToConnectTo.indexOf(":") == -1) { this.hostPortPair = this.host + ":" + portToConnectTo; } else { this.hostPortPair = this.host; } } } // 获取了所有链接数据库需要的参数 this.port = portToConnectTo; this.database = databaseToConnectTo; this.myURL = url; this.user = info.getProperty(NonRegisteringDriver.USER_PROPERTY_KEY); this.password = info.getProperty(NonRegisteringDriver.PASSWORD_PROPERTY_KEY); if ((this.user == null) || this.user.equals("")) { this.user = ""; } if (this.password == null) { this.password = ""; } this.props = info; initializeDriverProperties(info); // We store this per-connection, due to static synchronization issues in Java's built-in TimeZone class... this.defaultTimeZone = TimeUtil.getDefaultTimeZone(getCacheDefaultTimezone()); this.isClientTzUTC = !this.defaultTimeZone.useDaylightTime() && this.defaultTimeZone.getRawOffset() == 0; if (getUseUsageAdvisor()) { this.pointOfOrigin = LogUtils.findCallingClassAndMethod(new Throwable()); } else { this.pointOfOrigin = ""; } try { this.dbmd = getMetaData(false, false); // 进行数据库的链接 initializeSafeStatementInterceptors(); // 创建io流 createNewIO(false); unSafeStatementInterceptors(); } catch (SQLException ex) { // 此处省略部分代码 } catch (Exception ex) { // 此处省略部分代码 } NonRegisteringDriver.trackConnection(this); }

创建连接的具体实现就在createNewIO();方法中,继续往下:

public void createNewIO(boolean isForReconnect) throws SQLException { synchronized(this.getConnectionMutex()) { Properties mergedProps = this.exposeAsProperties(this.props); if(!this.getHighAvailability()) { // 尝试一次链接 this.connectOneTryOnly(isForReconnect, mergedProps); } else { // 尝试多次链接 this.connectWithRetries(isForReconnect, mergedProps); } } }

无论调用一次还是多次连接,都会调用 ConnectionImpl 类中获取链接 I/O 流的核心代码:

// 链接核心代码 private void coreConnect(Properties mergedProps) throws SQLException, IOException { int newPort = 3306; String newHost = "localhost"; String protocol = mergedProps.getProperty("PROTOCOL"); if(protocol != null) { if("tcp".equalsIgnoreCase(protocol)) { newHost = this.normalizeHost(mergedProps.getProperty("HOST")); newPort = this.parsePortNumber(mergedProps.getProperty("PORT", "3306")); } else if("pipe".equalsIgnoreCase(protocol)) { this.setSocketFactoryClassName(NamedPipeSocketFactory.class.getName()); String path = mergedProps.getProperty("PATH"); if(path != null) { mergedProps.setProperty("namedPipePath", path); } } else { newHost = this.normalizeHost(mergedProps.getProperty("HOST")); newPort = this.parsePortNumber(mergedProps.getProperty("PORT", "3306")); } } else { String[] parsedHostPortPair = NonRegisteringDriver.parseHostPortPair(this.hostPortPair); newHost = parsedHostPortPair[0]; newHost = this.normalizeHost(newHost); if(parsedHostPortPair[1] != null) { newPort = this.parsePortNumber(parsedHostPortPair[1]); } } this.port = newPort; this.host = newHost; this.sessionMaxRows = -1; // 通过ip, 端口等属性获取链接数据库得IO流, this.io = new MysqlIO(newHost, newPort, mergedProps, this.getSocketFactoryClassName(), this.getProxy(), this.getSocketTimeout(), this.largeRowSizeThreshold.getValueAsInt()); this.io.doHandshake(this.user, this.password, this.database); if(this.versionMeetsMinimum(5, 5, 0)) { this.errorMessageEncoding = this.io.getEncodingForHandshake(); } }

在这里可以发现,coreConnect()方法通过创建 MysqlIo 类获取了链接数据库的 io 流,而具体的 socket 是调用了 socketFactory 这个接口的实现类 StandardSocketFactory 这个类的实例的 connect 方法获取了一个指定的IP, Port 的 socket 链接。

到这里,我们发现获取 MySql 链接时通过 DriverManager.getConnection() -> 调用 com.mysql.jdbc.Driver 驱动 -> 创建 Connection 实现类,并通过指定的IP、端口获取与数据库的 Socket 链接。

参考文章

JDBC获取连接对象源码分析

参考文章