JDBC探究

831 阅读5分钟

以下以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