以下以MySQL为例并进行注释,基于jdk1.8,mysql连接jar包基于8.0.13。
step1
在加载DriverManager类时:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
通过SPI的方式(关于SPI可参考此处),读取当前计算机的JDBC驱动类,将其一一加载,并将驱动类信息添加到registeredDrivers变量
loadedDrivers.iterator()重写了Iterator类的hasNext()、next()还有remove()方法,返回ServiceLoader类对象- 在
driversIterator.hasNext()方法中,lookupIterator是ServiceLoader的内部类LazyIterator,lookupIterator.hasNext()调用hasNextService(),此方法中通过SPI的方式读取到需要加载的类全限定名 - 然后在
driversIterator.next()中,lookupIterator.next()方法加载了驱动类(即Class.forName,很经典的一个是com.mysql.cj.jdbc.Driver),并且将驱动类信息添加到registeredDrivers变量
step2
JDBC->main():
DriverManager.getConnection(url, user, password)
java.util.Properties继承了Hashtable,是一个线程安全类,aDriver是registeredDrivers的遍历对象名称
DriverManager->getConnection():
isDriverAllowed(aDriver.driver, callerCL)
- 调用者(即JDBC类)是否有权利读取驱动类信息,一般都返回true,如果没有的话就跳过
DriverManager->getConnection():
Connection con = aDriver.driver.connect(url, info);
// NonRegisteringDriver是驱动类com.mysql.cj.jdbc.Driver的父类
NonRegisteringDriver->connect():
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
- 根据url检查并判断是哪一种类型的驱动类,解析出它属于哪一种连接(返回
ConnectionUrl的对象),数据库名、端口号、用户名与数据库连接密码等一系列信息 - 然后switch case
ConnectionUrl的类型,实例化哪一种对象,这里返回的是new ConnectionImpl(hostInfo)
ConnectionImpl->public ConnectionImpl(HostInfo hostInfo):
// 1.创建本次会话的NativeSession
this.session = new NativeSession(hostInfo, this.propertySet);
......
createNewIO(false);
......
ConnectionImpl->createNewIO():
connectWithRetries(isForReconnect);
ConnectionImpl->connectWithRetries():
// 尝试建立连接,捕捉异常,失败的话尝试再次建立连接,直至失败三次
this.session.connect(this.origHostInfo, this.user, this.password, this.database, DriverManager.getLoginTimeout() * 1000, c);
......
NativeSession->connect():
// 根据前面的session创建socket连接
SocketConnection socketConnection = new NativeSocketConnection();
socketConnection.connect(this.hostInfo.getHost(), this.hostInfo.getPort(), this.propertySet, getExceptionInterceptor(), this.log, loginTimeout); // --1
// 创建连接协议,协议仅代管mysqlSocket发送/接收数据流
if (this.protocol == null) {
this.protocol = NativeProtocol.getInstance(this, socketConnection, this.propertySet, this.log, transactionManager);
} else {
this.protocol.init(this, socketConnection, this.propertySet, transactionManager);
}
this.protocol.connect(user, password, database);
......
NativeProtocol->connect():
this.authProvider.connect(this.serverSession, user, password, database);
NativeAuthenticationProvider->connect():
proceedHandshakeWithPluggableAuthentication(sessState, user, password, database, buf);
NativeAuthenticationProvider->proceedHandshakeWithPluggableAuthentication():
// 将用户名、密码,数据库名等连接信息加密之后以字节流打包的形式发送给MySQL服务器
this.protocol.send(last_sent, last_sent.getPosition());
......
challenge = this.protocol.checkErrorMessage();
NativeProtocol->checkErrorMessage():
resultPacket = readMessage(this.reusablePacket);
......
checkErrorMessage(resultPacket);
NativeSocketConnection->connect(): // --1
// 连接成功,创建并返回socket对象
this.mysqlSocket = this.socketFactory.connect(this.host, this.port, propSet, loginTimeout);
// 创建socket的inputStream和outputStream
this.mysqlInput = new FullReadInputStream(rawInputStream);
this.mysqlOutput = new BufferedOutputStream(this.mysqlSocket.getOutputStream(), 16384);
// 只有连接成功了,客户端才会有后续动作
......
- 读取MySQL服务器返回的报文(NativePacketPayload对象),从SocketConnection的mysqlInput流读取字节数组信息,解析内部字节信息流(byteBuffer成员变量)
- 如果收到的是OK数据包的话,说明连接成功了
- 然后做一些握手之后的动作,同时加上监听,以及一些安全机制,最后返回ConnectionImpl对象
step3
JDBC->main():
String sql = "SELECT * FROM tree WHERE id=?";
PreparedStatement ps = con.prepareStatement(sql);
-------------------------------------------------
ParseInfo->ParseInfo():
......
// 下列语句是对sql解析
for (i = this.statementStartPos; i < this.statementLength; ++i) {
......
解析出?的位置,将sql划分为两部分,?前为一部分,?后为一部分,并以字节数组形式赋值给staticSql变量的下标(staticSql是一个字节二维数组)
ClientPreparedStatement->initializeFromParseInfo():
// 设置当前Query类对象的参数绑定信息
((PreparedQuery<ClientPreparedQueryBindings>) this.query).setQueryBindings(new ClientPreparedQueryBindings(parameterCount, this.session));
- 最终返回的是ClientPreparedStatement类,它是PreparedStatement接口的实现类
step4
JDBC->main():
ps.setString(1, "2");
ClientPreparedStatement->setString():
// 得到上一步中的参数绑定信息,并设置其值,最终是bindValues的数组形式
((PreparedQuery<?>) this.query).getQueryBindings().setString(getCoreParameterIndex(parameterIndex), x);
ClientPreparedQueryBindings->setString():
// needsQuoted为true,会给参数两边加上'',否则不会
byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
: (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
: StringUtils.getBytes(parameterAsString, this.charEncoding));
setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
// 下面的方法中,第一个参数是sql对应第paramIndex+1个的?,第二个参数是?对应的字节数组值,第三个是对应的MySQL字段类型
AbstractQueryBindings->setValue(int paramIndex, byte[] val, MysqlType type):
// 给bindValues对应的下标设置字段值和字段类型
this.bindValues[paramIndex].setByteValue(val);
this.bindValues[paramIndex].setMysqlType(type);
step5
JDBC->main():
// 设置超时时间,当超过设定的时间时,会取消
ps.setQueryTimeout(seconds);
ResultSet rs = ps.executeQuery();
ClientPreparedStatement->executeQuery():
// 将sql语句中?的地方拼接值,以字节数组的形式展现,存到Message对象的byteBuffer变量中
// 具体实现利用了step3的staticSql和step4的bindValues
Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket();
......
// 执行sql语句,并获取MySQL发送过来的结果字节流,根据MySQL协议反序列化成ResultSet
this.results = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(), true, cachedMetadata, false);
- 与获取连接一样,同样是向MySQL服务器发送字节流,然后再接收返回字节流,并对返回的字节流解析
ClientPreparedStatement->executeInternal():
CancelQueryTask timeoutTask = null;
try {
// 如果没有设置超时时间,则返回null
// 否则新建一个CancelQueryTaskImpl线程,包含当前ClientPreparedStatement和seconds,用Timer管理
// 设置其执行时间为当前时间+seconds,达到这个时间,就会执行
timeoutTask = startQueryTimer(this, getTimeoutInMillis());
......
rs = ((NativeSession) locallyScopedConnection.getSession()).execSQL(this, null, maxRowsToRetrieve, (NativePacketPayload) sendPacket,
createStreamingResultSet, getResultSetFactory(), this.getCurrentCatalog(), metadata, isBatch);
if (timeoutTask != null) {
// 已经有结果了,取消超时执行线程
// 因为是异步线程,所以当拼装结果超时,也认为是超时抛出异常
stopQueryTimer(timeoutTask, true, true);
timeoutTask = null;
}
}
NativeSession->execSQL():
return ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, catalog, cachedMetadata,
this::getProfilerEventHandlerInstanceFunction, resultSetFactory);
NativeProtocol->sendQueryPacket():
NativePacketPayload resultPacket = sendCommand(queryPacket, false, 0); // --2
......
T rs = readAllResults(maxRows, streamResults, resultPacket, false, cachedMetadata, resultSetFactory); // --3
......
NativeProtocol->sendCommand(): // --2
// 发送
send(queryPacket, queryPacket.getPosition());
......
// 接收
returnPacket = checkErrorMessage(command);
NativeProtocol->checkErrorMessage():
// 接收返回包,并对字节流进行解析
resultPacket = readMessage(this.reusablePacket);
......
checkErrorMessage(resultPacket);
TextResultsetReader->read(): // --3
// 若是没有错误的话,接下来会从SocketConnection的mysqlInput流中读取到字段数量与字段具体信息
ColumnDefinition cdef = this.protocol.read(ColumnDefinition.class, new ColumnDefinitionFactory(columnCount, metadata));
......
// 然后读取所有的返回值,依然是从mysqlInput流读取到的
ArrayList<ResultsetRow> rowList = new ArrayList<>();
ResultsetRow row = this.protocol.read(ResultsetRow.class, trf);
// 这里相当于一次读取一条记录,将记录放到rowList中
while (row != null) {
if ((maxRows == -1) || (rowList.size() < maxRows)) {
rowList.add(row);
}
row = this.protocol.read(ResultsetRow.class, trf);
}
- 在查询完成之后,还会有一些统计时间等附加操作,最终返回的是ResultSetImpl对象,查询结果在rowData变量里面
CancelQueryTaskImpl->run()->new Thread()->run():
// 向MySQL服务器发送KILL QUERY的指令,取消本次查询
// origConnId与JDBC连接有关,即SHOW FULL PROCESSLIST的ID,该连接下向服务器发送的所有SQL都是这个ID
newSession.sendCommand(new NativeMessageBuilder().buildComQuery(newSession.getSharedSendPacket(), "KILL QUERY " + origConnId),
false, 0);
// 之后要么在查询的时候接收到MySQL返回的'Query execution was interrupted';
// 要么返回结果后在尝试stop timeoutTask的时候发现超时,抛出异常
step6
JDBC->main():
// rs.next()的实现主要利用了rowData,会将当前一行记录保存在thisRow变量里
while (rs.next()) {
// 字段名称最终会保存在columnDefinition变量里面,主要利用了之前的cdef
// 查询columnDefinition里是否有id这个字段名,返回其下标值,然后从thisRow变量获取其对应下标值的值
id = rs.getString("id");
pid = rs.getString("pid");
System.out.println("id=" + id + "; pid=" + pid);
}
step7
JDBC->main():
// 主要是将一些参数设置成null
// 最后一个则是向mysql服务器发送退出字节流,同时关闭socket连接
rs.close();
ps.close();
con.close();
总结:JDBC底层使用的是socket技术,不论是查询,还是修改,生成的sql语句都以字节数组的形式最终用outputStream发送,用inputStream接收
ConnectionImpl
->管理
NativeSession
->管理
NativeProtocol
->代管
SocketConnection
->管理
mysqlSocket、mysqlInput、mysqlOutput