mysql jdbc驱动源码分析(获取链接 connection)

时间:2022-09-19 11:56:28

在前一篇中我们分析了驱动的加载,这篇我们看看数据库链接的获取即Connection的获取,废话少说进入正题。

一、获取链接的方式有三种:


1、getConnection(String url,java.util.Properties info);

2、getConnection(String url,String user, String password);

3、getConnection(String url);

三种方式其中我们平时使用jdbc链接的时候用的最多的就是 1和3两种,下面我们首先来看看这三个方法的源码:

二、源码分析

1、getConnection(String url,java.util.Properties info) 方法的源码:

 

 public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {

// Gets the classloader of the code that called this method, may
// be null.
ClassLoader callerCL = DriverManager.getCallerClassLoader();

return (getConnection(url, info, callerCL));
}
DriverManager调用方法获取类加载器。然后在调用getConnection(url,info,callerCL)方法获取Connection 链接,其中的<span style="font-family: Arial, Helvetica, sans-serif;">DriverManager.getCallerClassLoader()这个方法是native 方法。</span>

2、getConnection(String url,String user, String password) 方法源码:

 

 public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();

// Gets the classloader of the code that called this method, may
// be null.
ClassLoader callerCL = DriverManager.getCallerClassLoader();

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

return (getConnection(url, info, callerCL));
}

同样首先调用方法获取了类加载器,其次将链接数据库的用户名密码添加到  Properties 中。

Properties 类表示了一个持久的属性集。Properties 可保存在流中或从流中加载。属性列表中每个键及其对应值都是一个字符串

3、getConnection(String url) 方法源码:

 public static Connection getConnection(String url)
throws SQLException {

java.util.Properties info = new java.util.Properties();

// Gets the classloader of the code that called this method, may
// be null.
ClassLoader callerCL = DriverManager.getCallerClassLoader();

return (getConnection(url, info, callerCL));
}

通过这三个源码我们可以看到主要的有两个,一个是获取列加载器的 getCallerClassLoader() 方法一个是 getConnection(url, info, callerCL) 获取链接的方法,下面来看看getConnection(url, info, callerCL)  方法的源码:


//  Worker method called by the public getConnection() methods.
// 实际是通过这个方法来获取connection的
// 通过之前的方法我们可以看到,所有的方法都调用了这个方法即请求地址,properties 和类加载器
private static Connection getConnection(
String url, java.util.Properties info, ClassLoader callerCL) 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.
*/
// 同步代码块
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if(callerCL == null) {
// 如果类加载器是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;
// 这里的这个集合就是 刚开始 调用resigerDriver方法将加载的驱动存放到这个集合中
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
// 调用isDriverAllowed 这个方法判断是不是同一个类,即Class的对象是不是相等。
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
// 获取链接,这里调用了Driver的父类的方法。
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");
}

这个方法判断驱动如果驱动类相等则调用Driver的父类进行和数据库的链接如果不相等则抛出异常链接结束。

判断Class 的类型的对象是不是相等的方法源码如下:

   private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
// 这里正好用到了反射的两个东西我们来看看
// object.getClass() 获取对象的类型即Class类的对象,通过调用Class类提供的方法可以获取这个类的类名称(包名+类名)以及类加载器。
// 使用给定的类加载器,返回与带有给定字符串名的类或接口相关联的 Class 对象。
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
//比较两个Class 的实例是不是相等,其实就是判断 是不是同一个类即:包名+类名+加载器名称,只要类名获取类加载器不同就不是同一个类。
result = ( aClass == driver.getClass() ) ? true : false;
}

return result;
}

ok 到这里该判断的都判断了,该比较的都比较了,现在我们看看 Driver的父类的conect方法,源代码如下:

    /**
* Try to make a database connection to the given URL. The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given
* URL. This will be common, as when the JDBC driverManager is asked to connect to a given URL, it passes the URL to each loaded driver in turn.
*
* <p>
* The driver should raise an SQLException if the URL is null or if it is the right driver to connect to the given URL, but has trouble connecting to the
* database.
* </p>
*
* <p>
* The java.util.Properties argument can be used to pass arbitrary string tag/value pairs as connection arguments. These properties take precedence over any
* properties sent in the URL.
* </p>
*
* <p>
* MySQL protocol takes the form:
*
* <PRE>
* jdbc:mysql://host:port/database
* </PRE>
*
* </p>
*
* @param url
* the URL of the database to connect to
* @param info
* a list of arbitrary tag/value pairs as connection arguments
*
* @return a connection to the URL or null if it isn't us
*
* @exception SQLException
* if a database access error occurs or the url is null
*
* @see java.sql.Driver#connect
*/
// 获取给定url的数据库的链接,inro 就是用户名和密码等参数,如果url 为null 则会返回一个异常信息。
public java.sql.Connection connect(String url, Properties info) throws SQLException {
// url: jdbc:mysql://ip:port/dbname
if (url == null) {
throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null);
}
// 判断url前缀是不是满足url的格式
if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) {
return connectLoadBalanced(url, info);
} else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) {
return connectReplicationConnection(url, info);
}

Properties props = null;
// parseUrl(url,info)方法解析url ,用户名密码以及其他的参数,并添加到Properties集合中
if ((props = parseURL(url, info)) == null) {
return null;
}

if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) {
return connectFailover(url, info);
}

try {
// 这里获取具体的链接 类是ConnectionImpl
// 其中调用的host port database 方法获取对应的参数的值
Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);

return newConn;
} catch (SQLException sqlEx) {
// Don't wrap SQLExceptions, throw
// them un-changed.
throw sqlEx;
} catch (Exception ex) {
SQLException sqlEx = SQLError.createSQLException(
Messages.getString("NonRegisteringDriver.17") + ex.toString() + Messages.getString("NonRegisteringDriver.18"),
SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null);

sqlEx.initCause(ex);

throw sqlEx;
}
}

我们据需跟进 com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);这个方法,源码如下:


 /**
* Creates a connection instance -- We need to provide factory-style methods
* so we can support both JDBC3 (and older) and JDBC4 runtimes, otherwise
* the class verifier complains when it tries to load JDBC4-only interface
* classes that are present in JDBC4 method signatures.
*/
// 参数有: 主机 端口号 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的构造函数来创建一个Connection实例,即向mysql服务获取链接:
<pre name="code" class="java">/**     * Creates a connection to a MySQL Server.     *      * @param hostToConnectTo     *            the hostname of the database server     * @param portToConnectTo     *            the port number the server is listening on     * @param info     *            a Properties[] list holding the user and password     * @param databaseToConnectTo     *            the database to connect to     * @param url     *            the URL of the connection     * @param d     *            the Driver instantation of the connection     * @exception SQLException     *                if a database access error occurs     */ // 构造函数来创建链接实例    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) {            cleanup(ex);            // don't clobber SQL exceptions            throw ex;        } catch (Exception ex) {            cleanup(ex);            StringBuilder mesg = new StringBuilder(128);            if (!getParanoid()) {                mesg.append("Cannot connect to MySQL server on ");                mesg.append(this.host);                mesg.append(":");                mesg.append(this.port);                mesg.append(".\n\n");                mesg.append("Make sure that there is a MySQL server ");                mesg.append("running on the machine/port you are trying ");                mesg.append("to connect to and that the machine this software is running on ");                mesg.append("is able to connect to this host/port (i.e. not firewalled). ");                mesg.append("Also make sure that the server has not been started with the --skip-networking ");                mesg.append("flag.\n\n");            } else {                mesg.append("Unable to connect to database.");            }            SQLException sqlEx = SQLError.createSQLException(mesg.toString(), SQLError.SQL_STATE_COMMUNICATION_LINK_FAILURE, getExceptionInterceptor());            sqlEx.initCause(ex);            throw sqlEx;        }        NonRegisteringDriver.trackConnection(this);    }


 

我们发现具体的链接没有在这个方法中实现,而是在createNewIO()这个方法中实现的,具体代码如下:

ConnectionImpl 类中的createNewIO() 方法如下:

  /**
* Creates an IO channel to the server
*
* @param isForReconnect
* is this request for a re-connect
* @return a new MysqlIO instance connected to a server
* @throws SQLException
* if a database access error occurs
* @throws CommunicationsException
*/
// 创建io流,在调用这个方法的时候传的参数是false
public void createNewIO(boolean isForReconnect) throws SQLException {
// 同步代码块
synchronized (getConnectionMutex()) {
// Synchronization Not needed for *new* connections, but defintely for connections going through fail-over, since we might get the new connection up
// and running *enough* to start sending cached or still-open server-side prepared statements over to the backend before we get a chance to
// re-prepare them...

Properties mergedProps = exposeAsProperties(this.props);

if (!getHighAvailability()) {
connectOneTryOnly(isForReconnect, mergedProps);

return;
}
// 调用方法尝试多次链接
connectWithRetries(isForReconnect, mergedProps);
}
}
调用ConnectionImpl 类的 connectWithRetries方法进行多次的尝试链接:

// 链接的具体实现
private void connectWithRetries(boolean isForReconnect, Properties mergedProps) throws SQLException {
double timeout = getInitialTimeout();
boolean connectionGood = false;

Exception connectionException = null;
// 在没有获的链接的情况下尝试链接多次,知道最大尝试次数
for (int attemptCount = 0; (attemptCount < getMaxReconnects()) && !connectionGood; attemptCount++) {
try {
if (this.io != null) {
this.io.forceClose();
}
// 调用这个方法进行链接获取
coreConnect(mergedProps);
pingInternal(false, 0);

boolean oldAutoCommit;
int oldIsolationLevel;
boolean oldReadOnly;
String oldCatalog;

synchronized (getConnectionMutex()) {
this.connectionId = this.io.getThreadId();
this.isClosed = false;

// save state from old connection
oldAutoCommit = getAutoCommit();
oldIsolationLevel = this.isolationLevel;
oldReadOnly = isReadOnly(false);
oldCatalog = getCatalog();

this.io.setStatementInterceptors(this.statementInterceptors);
}

// Server properties might be different from previous connection, so initialize again...
initializePropsFromServer();

if (isForReconnect) {
// Restore state from old connection
setAutoCommit(oldAutoCommit);

if (this.hasIsolationLevels) {
setTransactionIsolation(oldIsolationLevel);
}

setCatalog(oldCatalog);
setReadOnly(oldReadOnly);
}

connectionGood = true;

break;
} catch (Exception EEE) {
connectionException = EEE;
connectionGood = false;
}

if (connectionGood) {
break;
}

if (attemptCount > 0) {
try {
Thread.sleep((long) timeout * 1000);
} catch (InterruptedException IE) {
// ignore
}
}
} // end attempts for a single host

if (!connectionGood) {
// We've really failed!
SQLException chainedEx = SQLError.createSQLException(
Messages.getString("Connection.UnableToConnectWithRetries", new Object[] { Integer.valueOf(getMaxReconnects()) }),
SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, getExceptionInterceptor());
chainedEx.initCause(connectionException);

throw chainedEx;
}

if (getParanoid() && !getHighAvailability()) {
this.password = null;
this.user = null;
}

if (isForReconnect) {
//
// Retrieve any 'lost' prepared statements if re-connecting
//
Iterator<Statement> statementIter = this.openStatements.values().iterator();

//
// We build a list of these outside the map of open statements, because in the process of re-preparing, we might end up having to close a prepared
// statement, thus removing it from the map, and generating a ConcurrentModificationException
//
Stack<Statement> serverPreparedStatements = null;

while (statementIter.hasNext()) {
Statement statementObj = statementIter.next();

if (statementObj instanceof ServerPreparedStatement) {
if (serverPreparedStatements == null) {
serverPreparedStatements = new Stack<Statement>();
}

serverPreparedStatements.add(statementObj);
}
}

if (serverPreparedStatements != null) {
while (!serverPreparedStatements.isEmpty()) {
((ServerPreparedStatement) serverPreparedStatements.pop()).rePrepare();
}
}
}
}

ConnectionImpl类中获取链接I/O流的核心代码:

    // 链接的核心代码
private void coreConnect(Properties mergedProps) throws SQLException, IOException {
// 默认端口号
int newPort = 3306;
// 默认主机名 localhost
String newHost = "localhost";
// 协议
String protocol = mergedProps.getProperty(NonRegisteringDriver.PROTOCOL_PROPERTY_KEY);

if (protocol != null) {
// "new" style URL

if ("tcp".equalsIgnoreCase(protocol)) {
newHost = normalizeHost(mergedProps.getProperty(NonRegisteringDriver.HOST_PROPERTY_KEY));
newPort = parsePortNumber(mergedProps.getProperty(NonRegisteringDriver.PORT_PROPERTY_KEY, "3306"));
} else if ("pipe".equalsIgnoreCase(protocol)) {
setSocketFactoryClassName(NamedPipeSocketFactory.class.getName());

String path = mergedProps.getProperty(NonRegisteringDriver.PATH_PROPERTY_KEY);

if (path != null) {
mergedProps.setProperty(NamedPipeSocketFactory.NAMED_PIPE_PROP_NAME, path);
}
} else {
// normalize for all unknown protocols
newHost = normalizeHost(mergedProps.getProperty(NonRegisteringDriver.HOST_PROPERTY_KEY));
newPort = parsePortNumber(mergedProps.getProperty(NonRegisteringDriver.PORT_PROPERTY_KEY, "3306"));
}
} else {

String[] parsedHostPortPair = NonRegisteringDriver.parseHostPortPair(this.hostPortPair);
newHost = parsedHostPortPair[NonRegisteringDriver.HOST_NAME_INDEX];

newHost = normalizeHost(newHost);

if (parsedHostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX] != null) {
newPort = parsePortNumber(parsedHostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX]);
}
}

this.port = newPort;
this.host = newHost;

// reset max-rows to default value
this.sessionMaxRows = -1;
// io创建获取和mysql 服务器的链接
// 方法getSocketFactoryClassName() 获取类名
/** The I/O abstraction interface (network conn to MySQL server */
// 获得和mysql服务的链接
this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(),
this.largeRowSizeThreshold.getValueAsInt());
this.io.doHandshake(this.user, this.password, this.database);
if (versionMeetsMinimum(5, 5, 0)) {
// error messages are returned according to character_set_results which, at this point, is set from the response packet
this.errorMessageEncoding = this.io.getEncodingForHandshake();
}
}

到这里我们发现创建了一个MysqlIO 类,从这个类中获取了io流,而具体的socket 是调用了socketFactory 这个接口的实现类StandardSocketFactory 这个类的实例的connect 方法获取了一个指定的IP ,Port的socket 链接。

StandardSocketFactory类的connect 方法如下:


 /**
* @see com.mysql.jdbc.SocketFactory#createSocket(Properties)
*/
public Socket connect(String hostname, int portNumber, Properties props) throws SocketException, IOException {

if (props != null) {
this.host = hostname;

this.port = portNumber;

String localSocketHostname = props.getProperty("localSocketAddress");
InetSocketAddress localSockAddr = null;
if (localSocketHostname != null && localSocketHostname.length() > 0) {
localSockAddr = new InetSocketAddress(InetAddress.getByName(localSocketHostname), 0);
}

String connectTimeoutStr = props.getProperty("connectTimeout");

int connectTimeout = 0;

if (connectTimeoutStr != null) {
try {
connectTimeout = Integer.parseInt(connectTimeoutStr);
} catch (NumberFormatException nfe) {
throw new SocketException("Illegal value '" + connectTimeoutStr + "' for connectTimeout");
}
}

if (this.host != null) {
InetAddress[] possibleAddresses = InetAddress.getAllByName(this.host);

if (possibleAddresses.length == 0) {
throw new SocketException("No addresses for host");
}

// save last exception to propagate to caller if connection fails
SocketException lastException = null;

// Need to loop through all possible addresses. Name lookup may return multiple addresses including IPv4 and IPv6 addresses. Some versions of
// MySQL don't listen on the IPv6 address so we try all addresses.
for (int i = 0; i < possibleAddresses.length; i++) {
try {
this.rawSocket = createSocket(props);

configureSocket(this.rawSocket, props);

InetSocketAddress sockAddr = new InetSocketAddress(possibleAddresses[i], this.port);
// bind to the local port if not using the ephemeral port
if (localSockAddr != null) {
this.rawSocket.bind(localSockAddr);
}

this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout));

break;
} catch (SocketException ex) {
lastException = ex;
resetLoginTimeCountdown();
this.rawSocket = null;
}
}

if (this.rawSocket == null && lastException != null) {
throw lastException;
}

resetLoginTimeCountdown();

return this.rawSocket;
}
}

throw new SocketException("Unable to create socket");
}

到这里发现通过mysql 实现驱动的加载获取 驱动类实例后通过各种判断最终就是获取了指定IP,PORT的socket 的获取。获得Socket链接后我们也就获的了和mysql server的链接。