3

Recently, in order to read the MyBatis source code, I probably read the JDBC code to understand the principle of implementing SQL queries in Java without using the ORM framework.

JDBC is defined as follows:

JDBC (Java DataBase Connectivity, java database connection) is a Java API used to execute SQL statements, which can provide unified access to a variety of relational databases. It consists of a set of classes and interfaces written in Java language. JDBC provides a benchmark from which more advanced tools and interfaces can be built to enable database developers to write database applications.

That is to say, JDBC is a series of specifications proposed by SUN, but it only defines the interface specifications, and the specific implementation is left to each database manufacturer.

1. Use process

JDBC process:

  1. Load Driver driver class via SPI.
  2. Establish a database connection and obtain the Connection object.
  3. Create a Statement object through the connection, execute the SQL statement, and get the returned result.
  4. Free up resources.

Add a dependency on the mysql driver in Maven:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.23</version>
</dependency>

Database Table:
学生表

1.1 General query

@Test
public void query() throws Exception {
    // 打印至控制台
    DriverManager.setLogWriter(new PrintWriter(System.out));
    // 建立连接
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
    // 执行 SQL 查询,获取返回结果
    Statement statement = conn.createStatement();
    statement.setQueryTimeout(60);
    ResultSet resultSet = statement.executeQuery("select * from t_student");
    while (resultSet.next()) {
        System.out.println("id:" + resultSet.getInt(1) + " address:" + resultSet.getString(2) + " name:" + resultSet.getString(4));
    }
}

Results of the:

DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb")
    trying com.mysql.cj.jdbc.Driver
getConnection returning com.mysql.cj.jdbc.Driver
id:1 address:hunan name:zhangsan
id:2 address:guangzhou name:lisi
id:3 address:四川 name:大大

1.2 Parameter query

@Test
public void queryByParam() throws SQLException {
    DriverManager.setLogWriter(new PrintWriter(System.out));
    // 建立连接
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
    // 执行 SQL 查询,获取返回结果
    PreparedStatement preparedStatement = conn.prepareStatement("select * from t_student where id = ? and age = ?");
    preparedStatement.setString(1, "1");
    preparedStatement.setInt(2, 11);
    ResultSet resultSet = preparedStatement.executeQuery();
    while (resultSet.next()) {
        System.out.println("id:" + resultSet.getInt(1) + " address:" + resultSet.getString(2) + " name:" + resultSet.getString(4));
    }
}

1.3 Batch update

@Test
public void updateBatch() throws SQLException {
    DriverManager.setLogWriter(new PrintWriter(System.out));
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
    PreparedStatement preparedStatement = conn.prepareStatement("update t_student set age = ? where id = ?");

    preparedStatement.setInt(1, 10);
    preparedStatement.setString(2, "1");
    preparedStatement.addBatch();

    preparedStatement.setInt(1, 10);
    preparedStatement.setString(2, "2");
    preparedStatement.addBatch();

    int[] result = preparedStatement.executeBatch();
    System.out.println("result = " + result.length);
}

2. Source code analysis

2.1 Load the driver

Main process:

  1. Load the Driver class in the MySQL driver package via SPI.
  2. Register the Driver class to the DriverManager of JDBC.

2.1.1 SPI loading driver class

Static code blocks are defined in DriverManager, and database driver classes will be loaded through SPI.

java.sql.DriverManager

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

java.sql.DriverManager#loadInitialDrivers

private static void loadInitialDrivers() {
    // ...
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            // 采用 SPI 机制加载数据库驱动
            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;
        }
    });
    // ...
}

Load the configuration file META-INF/services/java.sql.Driver through SPI. Since the project has a jar package for MySql driver, here is the configuration file read:
mysql-connector-java-8.0.23.jar!\META-INF\services\java.sql.Driver
The content in the file is com.mysql.cj.jdbc.Driver , which is obtained by reflection and instantiated.

2.1.2 Registering the driver class

When instantiating the MySQL driver, the driver will be registered through the DriverManager#registerDriver method.

com.mysql.cj.jdbc.Driver

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

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

The underlying implementation is to package the MySQL driver Driver object as DriverInfo and store it in the DriverManager#registeredDrivers collection.

java.sql.DriverManager#registerDriver

// List of registered JDBC drivers
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);

}

2.2 Establish a connection

Use the DriverManager#getConnection method to establish a connection with the MySQL server.

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");

Which traverse CopyOnWriteArrayList<DriverInfo> registeredDrivers registered in the database-driven, here is com.mysql.cj.jdbc.Driver instance.

java.sql.DriverManager#getConnection

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());
    }

}

To establish a connection through the MySQL database driver:

  1. Construct a ConnectionUrl object based on parameters such as database address, user name and password, and the object is of type SINGLE_CONNECTION by default.
  2. According to the ConnectionUrl object, create a database connection instance ConnectionImpl and establish a Socket connection.

2.2.1 Constructing the ConnectionUrl object

com.mysql.cj.jdbc.NonRegisteringDriver#connect

public java.sql.Connection connect(String url, Properties info) throws SQLException {
    // ...
    ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
    switch (conStr.getType()) {
        // 单个
        case SINGLE_CONNECTION:
            return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());
        // 失效转移 
        case FAILOVER_CONNECTION:
        case FAILOVER_DNS_SRV_CONNECTION:
            return FailoverConnectionProxy.createProxyInstance(conStr);
        // 负载均衡        
        case LOADBALANCE_CONNECTION:
        case LOADBALANCE_DNS_SRV_CONNECTION:
            return LoadBalancedConnectionProxy.createProxyInstance(conStr);
        // 复制
        case REPLICATION_CONNECTION:
        case REPLICATION_DNS_SRV_CONNECTION:
            return ReplicationConnectionProxy.createProxyInstance(conStr);
        default:
            return null;
    }
    // ...
}

2.2.2 MySQL driver protocol description

1. Failover
jdbc:mysql://[primary-host]:[port],[secondary-host]:[port],.../[database]?[property=<value>]&[property=<value>]

That is, when the client link fails, it will try to establish a link with other hosts. This process is transparent to the application. Read (write) operations always only occur on one host.

2. Load Balancing
jdbc:mysql:loadbalance://[host]:[port],[host]:[port],...[/database]?[property=<value>]&[property=<value>]

The format is similar to failover\replication, all hosts are equal without primary and secondary, and support parameter control.

The load balancing strategy defines the BalanceStrategy interface, and mysql supports the following strategies that have implemented this interface:

  • BestResponseTimeBalanceStrategy: Select the host with the fastest transaction response
  • RandomBalanceStrategy: (default) randomly select a host
  • SequentialBalanceStrategy: Randomly select the next one in sequence after the first time to repeat
3. Replication
jdbc:mysql:replication://[master-host]:[port],[slave-host]:[port],.../database?[property=<value>]

The specific format is similar to failover. The major change is that the first host is the master library in write/read mode, and the slave library is in read mode afterwards, which also supports parameter configuration.

The replication protocol is based on failover and loadbalance, adapting to the needs of the Replication architecture to solve read-write separation and load balancing scenarios.
In the transaction read only mode, the request will be diverted to the slave host. In the case of multiple slaves, a round-robin (polling) strategy is adopted.
All non-read only requests (write/read) will be redirected to the master host.

The version after 6.5.1.27 supports multiple masters, and the load balance strategy is adopted under multiple masters. For details, please refer to the introduction of loadbalance protocol.
After the 7.5.1.28 version, it supports dynamic addition of nodes, that is, the program runs dynamically adding a new host to the URL without restarting the server. We often talk about dynamic data source scenarios.

References in this section:
loadbalance and replication of mysql driver protocol
Chapter 8 Multi-Host Connections

2.3 Construction of ConnectionImpl instance

Construct a ConnectionImpl instance, which will create a Socket connection.

com.mysql.cj.jdbc.ConnectionImpl#getInstance
com.mysql.cj.jdbc.ConnectionImpl#ConnectionImpl(com.mysql.cj.conf.HostInfo)

public ConnectionImpl(HostInfo hostInfo) throws SQLException {
    // ...
    createNewIO(false); // 关键位置
    // ...
}

com.mysql.cj.jdbc.ConnectionImpl#createNewIO
com.mysql.cj.jdbc.ConnectionImpl#connectOneTryOnly

To establish a session, BIO is used here.

com.mysql.cj.NativeSession#connect
com.mysql.cj.protocol.a.NativeSocketConnection#connect
com.mysql.cj.protocol.StandardSocketFactory#connect

@SuppressWarnings("unchecked")
public <T extends Closeable> T connect(String hostname, int portNumber, PropertySet pset, int loginTimeout) throws IOException {

    this.loginTimeoutCountdown = loginTimeout;

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

        this.port = portNumber;

        String localSocketHostname = pset.getStringProperty(PropertyKey.localSocketAddress).getValue();
        InetSocketAddress localSockAddr = null;
        if (localSocketHostname != null && localSocketHostname.length() > 0) {
            localSockAddr = new InetSocketAddress(InetAddress.getByName(localSocketHostname), 0);
        }

        int connectTimeout = pset.getIntegerProperty(PropertyKey.connectTimeout).getValue();

        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(pset);

                    configureSocket(this.rawSocket, pset);

                    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)); // 建立 Socket 连接

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

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

            resetLoginTimeCountdown();

            this.sslSocket = this.rawSocket;
            return (T) this.rawSocket;
        }
    }

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

2.3 execute statement

2.3.1 Statement

Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery("select * from t_student");

Initiate a SQL query and get the returned result:

com.mysql.cj.jdbc.StatementImpl#executeQuery

public java.sql.ResultSet executeQuery(String sql) throws SQLException {
    // ...
    this.results = ((NativeSession) locallyScopedConn.getSession()).execSQL(this, sql, this.maxRows, null, 
        createStreamingResultSet(), getResultSetFactory(), cachedMetaData, false);
}

There are two ways to initiate SQL query: 1. String; 2. Binary data packet.

com.mysql.cj.NativeSession#execSQL

public <T extends Resultset> T execSQL(Query callingQuery, String query, int maxRows, NativePacketPayload packet, boolean streamResults, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory, ColumnDefinition cachedMetadata, boolean isBatch) {
    // ...
    return packet == null
            ? ((NativeProtocol) this.protocol).sendQueryString(callingQuery, query, this.characterEncoding.getValue(), maxRows, streamResults, cachedMetadata, resultSetFactory)
            : ((NativeProtocol) this.protocol).sendQueryPacket(callingQuery, packet, maxRows, streamResults, cachedMetadata, resultSetFactory);
}

Since the SQL statement is select * from t_student , first execute the sendQueryString() method to convert the string into a secondary package (NativePacketPayload class instance), and then call the sendQueryPacket() method to send the data.

com.mysql.cj.protocol.a.NativeProtocol#sendQueryString
com.mysql.cj.protocol.a.NativeProtocol#sendQueryPacket

public final <T extends Resultset> T sendQueryPacket(Query callingQuery, NativePacketPayload queryPacket, int maxRows, boolean streamResults, ColumnDefinition cachedMetadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
    // ...
    // Send query command and sql query string
    NativePacketPayload resultPacket = sendCommand(queryPacket, false, 0);
    T rs = readAllResults(maxRows, streamResults, resultPacket, false, cachedMetadata, resultSetFactory);
    return rs;
}

Send data packets to the mysql server.

com.mysql.cj.protocol.a.NativeProtocol#sendCommand(com.mysql.cj.protocol.Message, boolean, int)
com.mysql.cj.protocol.a.NativeProtocol#send(com.mysql.cj.protocol.Message, int)
com.mysql.cj.protocol.a.TimeTrackingPacketSender#send(byte[], int, byte)
com.mysql.cj.protocol.a.SimplePacketSender#send(byte[], int, byte)

public void send(byte[] packet, int packetLen, byte packetSequence) throws IOException {
    PacketSplitter packetSplitter = new PacketSplitter(packetLen);
    while (packetSplitter.nextPacket()) {
        this.outputStream.write(NativeUtils.encodeMysqlThreeByteInteger(packetSplitter.getPacketLen()));
        this.outputStream.write(packetSequence++);
        this.outputStream.write(packet, packetSplitter.getOffset(), packetSplitter.getPacketLen());
    }
    this.outputStream.flush();
}

2.3.2 PreparedStatement

The function of PreparedStatement is similar to Statement, but the difference is that PreparedStatement can use placeholders. The placeholders identify the location where data needs to be entered, and then fill in the data one by one. Of course, PreparedStatement can also execute SQL statements without placeholders.

PreparedStatement preparedStatement = conn.prepareStatement("select * from t_student where id = ? and age = ?");
preparedStatement.setString(1, "1");
preparedStatement.setInt(2, 11);
ResultSet resultSet = preparedStatement.executeQuery();

The corresponding implementation class of the interface java.sql.PreparedStatement in the MySQL driver is com.mysql.cj.jdbc.ClientPreparedStatement

Use PreparedStatement to query:

com.mysql.cj.jdbc.ClientPreparedStatement#executeQuery

public java.sql.ResultSet executeQuery() throws SQLException {
    // ...
    Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket();
    this.results = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(), true, cachedMetadata, false);
    return this.results;
}

Among them, first fill in the placeholders in SQL, and then send them as a binary package:

com.mysql.cj.AbstractPreparedQuery#fillSendPacket

sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, bindValues[i].getByteValue());

For raw SQL: select * from t_student where id = ? and age = ?
Split into three strings according to the placeholders, then traverse each string and bind the parameters.

  1. select * from t_student where id =
  2. and age =
  3. Empty string

After adding the parameters, it will be marked as a binary package and sent to the database server.
The subsequent process is consistent with using Statement:

com.mysql.cj.jdbc.ClientPreparedStatement#executeInternal
com.mysql.cj.NativeSession#execSQL
com.mysql.cj.protocol.a.NativeProtocol#sendQueryPacket

2.4 Get response

After sending data to MySQL, read the response.

  1. First read the number of columns in the response (that is, how many fields there are).

com.mysql.cj.protocol.a.NativeProtocol#sendQueryPacket

public final <T extends Resultset> T sendQueryPacket(Query callingQuery, NativePacketPayload queryPacket, int maxRows, boolean streamResults, ColumnDefinition cachedMetadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException {
    // ...
    // Send query command and sql query string
    NativePacketPayload resultPacket = sendCommand(queryPacket, false, 0);
    T rs = readAllResults(maxRows, streamResults, resultPacket, false, cachedMetadata, resultSetFactory); // 读取列数
    return rs;
}
  1. Read the specific data of each row (field format and field content).

com.mysql.cj.protocol.a.NativeProtocol#readAllResults
com.mysql.cj.protocol.a.NativeProtocol#read
com.mysql.cj.protocol.a.TextResultsetReader#read

public Resultset read(int maxRows, boolean streamResults, NativePacketPayload resultPacket, ColumnDefinition metadata, ProtocolEntityFactory<Resultset, NativePacketPayload> resultSetFactory) throws IOException {
    Resultset rs = null;
    long columnCount = resultPacket.readInteger(IntegerDataType.INT_LENENC); // 获取字段的个数
    if (columnCount > 0) {
        // Build a result set with rows.

        // Read in the column information // 读取全部字段的类型信息
        ColumnDefinition cdef = this.protocol.read(ColumnDefinition.class, new ColumnDefinitionFactory(columnCount, metadata));

        // There is no EOF packet after fields when CLIENT_DEPRECATE_EOF is set
        if (!this.protocol.getServerSession().isEOFDeprecated()) {
            this.protocol.skipPacket();
            //this.protocol.readServerStatusForResultSets(this.protocol.readPacket(this.protocol.getReusablePacket()), true);
        }

        ResultsetRows rows = null;
        if (!streamResults) {
            TextRowFactory trf = new TextRowFactory(this.protocol, cdef, resultSetFactory.getResultSetConcurrency(), false);
            ArrayList<ResultsetRow> rowList = new ArrayList<>();

            // 依次读取每一行数据,获取全部字段的值信息(二进制格式)
            ResultsetRow row = this.protocol.read(ResultsetRow.class, trf);
            while (row != null) {
                if ((maxRows == -1) || (rowList.size() < maxRows)) {
                    rowList.add(row);
                }
                row = this.protocol.read(ResultsetRow.class, trf);
            }
            rows = new ResultsetRowsStatic(rowList, cdef); // 对象封装:ResultsetRow -> ResultsetRows

        } else {
            rows = new ResultsetRowsStreaming<>(this.protocol, cdef, false, resultSetFactory);
            this.protocol.setStreamingData(rows);
        }
        /*
         * Build ResultSet from ResultsetRows
         */
        rs = resultSetFactory.createFromProtocolEntity(rows); // 对象封装:ResultsetRows -> Resultset
    }
    return rs;
}        

2.4.1 Read field information

Loop through each column to read and parse the type information of the field.

com.mysql.cj.protocol.a.NativeProtocol#read
com.mysql.cj.protocol.a.ColumnDefinitionReader#read

@Override
public ColumnDefinition read(ProtocolEntityFactory<ColumnDefinition, NativePacketPayload> sf) {

    ColumnDefinitionFactory cdf = (ColumnDefinitionFactory) sf;

    long columnCount = cdf.getColumnCount();
    ColumnDefinition cdef = cdf.getColumnDefinitionFromCache();

    if (cdef != null && !cdf.mergeColumnDefinitions()) {
        for (int i = 0; i < columnCount; i++) {
            this.protocol.skipPacket();
        }
        return cdef;
    }

    /* read the metadata from the server */
    Field[] fields = null;
    boolean checkEOF = !this.protocol.getServerSession().isEOFDeprecated();

    // Read in the column information

    fields = new Field[(int) columnCount];

    for (int i = 0; i < columnCount; i++) { // 循环遍历每一列,对字段的类型信息进行读取和解析
        NativePacketPayload fieldPacket = this.protocol.readMessage(null); // 读取二进制包
        // next check is needed for SSPS
        if (checkEOF && fieldPacket.isEOFPacket()) {
            break;
        }
        fields[i] = unpackField(fieldPacket, this.protocol.getServerSession().getCharacterSetMetadata()); // 解析二进制包
    }

    return cdf.createFromFields(fields);
}

For example, read the id field information of the t_student table of the testdb library and encapsulate it into the Field class as follows:
Field

2.4.2 Reading the value of a field

From the binary data packet, the values of multiple fields are parsed.

com.mysql.cj.protocol.a.ResultsetRowReader#read

@Override
public ResultsetRow read(ProtocolEntityFactory<ResultsetRow, NativePacketPayload> sf) throws IOException {
    AbstractRowFactory rf = (AbstractRowFactory) sf;
    NativePacketPayload rowPacket = null;
    NativePacketHeader hdr = this.protocol.getPacketReader().readHeader();

    // read the entire packet(s) // 读到一整行完整的数据(二进制格式)
    rowPacket = this.protocol.getPacketReader()
            .readMessage(rf.canReuseRowPacketForBufferRow() ? Optional.ofNullable(this.protocol.getReusablePacket()) : Optional.empty(), hdr);
    this.protocol.checkErrorMessage(rowPacket);
    // Didn't read an error, so re-position to beginning of packet in order to read result set data
    rowPacket.setPosition(rowPacket.getPosition() - 1);

    // exit early with null if there's an EOF packet
    if (!this.protocol.getServerSession().isEOFDeprecated() && rowPacket.isEOFPacket()
            || this.protocol.getServerSession().isEOFDeprecated() && rowPacket.isResultSetOKPacket()) {
        this.protocol.readServerStatusForResultSets(rowPacket, true);
        return null;
    }

    return sf.createFromMessage(rowPacket); // 解析数据
}

For example, the table data with id 1 is as follows:

学生表
The binary content read here is shown on the left as follows:

01 31 05 68 75 6e 61 6e     . 1 . h u n a n
02 31 31 08 7a 68 61 6e     . 1 1 . z h a n
67 73 61 6e                 g s a n

Parse the binary data packet:

com.mysql.cj.protocol.a.TextRowFactory#createFromMessage

@Override
public ResultsetRow createFromMessage(NativePacketPayload rowPacket) {

    // use a buffer row for reusable packets (streaming results), blobs and long strings
    // or if we're over the threshold
    boolean useBufferRow = this.canReuseRowPacketForBufferRow || this.columnDefinition.hasLargeFields()
            || rowPacket.getPayloadLength() >= this.useBufferRowSizeThreshold.getValue();

    if (this.resultSetConcurrency == Concurrency.UPDATABLE || !useBufferRow) {
        byte[][] rowBytes = new byte[this.columnDefinition.getFields().length][];

        for (int i = 0; i < this.columnDefinition.getFields().length; i++) { // 根据字段的个数,拆分二进制包
            rowBytes[i] = rowPacket.readBytes(StringSelfDataType.STRING_LENENC);
        }

        return new ByteArrayRow(rowBytes, this.exceptionInterceptor); // 将拆分后的二进制数据,存储在 ByteArrayRow 对象中。
    }

    return new TextBufferRow(rowPacket, this.columnDefinition, this.exceptionInterceptor, this.valueDecoder);
}

2.4.3 Constructing the Resultset

Finally, back to the com.mysql.cj.protocol.a.TextResultsetReader#read method.
The ResultsetRow object obtained after parsing each row of data is stored in the collection and stored in the ResultsetRows object. Then construct the Resultset object based on the ResultsetRows object.

Namely: ResultsetRow -> ResultsetRows -> Resultset

com.mysql.cj.jdbc.result.ResultSetFactory#createFromProtocolEntity
com.mysql.cj.jdbc.result.ResultSetFactory#createFromResultsetRows

public ResultSetImpl createFromResultsetRows(int resultSetConcurrency, int resultSetType, ResultsetRows rows) throws SQLException {

    ResultSetImpl rs;

    StatementImpl st = this.stmt;

    if (rows.getOwner() != null) {
        st = ((ResultSetImpl) rows.getOwner()).getOwningStatement();
    }

    switch (resultSetConcurrency) {
        case java.sql.ResultSet.CONCUR_UPDATABLE:
            rs = new UpdatableResultSet(rows, this.conn, st);
            break;

        default:
            // CONCUR_READ_ONLY
            rs = new ResultSetImpl(rows, this.conn, st); // 对象封装:ResultsetRows -> ResultSet
            break;
    }

    rs.setResultSetType(resultSetType);
    rs.setResultSetConcurrency(resultSetConcurrency);

    if (rows instanceof ResultsetRowsCursor && st != null) {
        rs.setFetchSize(st.getFetchSize());
    }
    return rs;
}

3. Summary

  1. JDBC uses the SPI mechanism to load the database driver. This is to solve the problem that the BootstrapClassloader cannot load third-party classes, and delegates the third-party classes to the thread context class loader to load.
  2. Obtaining the database connection object Connection is actually establishing a Socket connection to the database. You can use the database connection pool to reuse Connection.
  3. To query data through Statement, the bottom layer is to write binary data to the Socket, and then read the binary data from the Socket, and encapsulate it in the Resultset object.

Author: Sumkor
Link: https://segmentfault.com/a/1190000040580622


Sumkor
148 声望1.3k 粉丝

会写点代码