最近为了浏览 MyBatis 源码,大略看了下 JDBC 的代码,理解在不应用 ORM 框架的状况下 Java 实现 SQL 查问的原理。
JDBC 定义如下:
JDBC(Java DataBase Connectivity,java数据库连贯)是一种用于执行 SQL 语句的 Java API,能够为多种关系数据库提供对立拜访,它由一组用 Java 语言编写的类和接口组成。JDBC 提供了一种基准,据此能够构建更高级的工具和接口,使数据库开发人员可能编写数据库应用程序。
也就是说 JDBC 是 SUN 公司提出来的一系列标准,但它只定义了接口标准,具体的实现则交给各个数据库的厂商去做。
1. 应用流程
JDBC 流程:
- 通过 SPI 加载 Driver 驱动类。
- 建设数据库连贯,获取 Connection 连贯对象。
- 通过连贯创立 Statement 对象,执行 SQL 语句,获取返回后果。
- 开释资源。
在 Maven 中增加对 mysql 驱动的依赖:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version></dependency>
数据库表:
1.1 一般查问
@Testpublic 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)); }}
执行后果:
DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb") trying com.mysql.cj.jdbc.DrivergetConnection returning com.mysql.cj.jdbc.Driverid:1 address:hunan name:zhangsanid:2 address:guangzhou name:lisiid:3 address:四川 name:大大
1.2 参数查问
@Testpublic 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 批量更新
@Testpublic 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. 源码解析
2.1 加载驱动
次要流程:
- 通过 SPI 加载 MySQL 驱动包中的 Driver 类。
- 将 Driver 类注册到 JDBC 的 DriverManager 之中。
2.1.1 SPI 加载驱动类
DriverManager 中定义了动态代码块,会通过 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; } }); // ...}
通过 SPI,加载配置文件 META-INF/services/java.sql.Driver,因为我的项目中具备 MySql 驱动的 jar 包,这里是读取到配置文件:mysql-connector-java-8.0.23.jar!\META-INF\services\java.sql.Driver
文件中的内容为 com.mysql.cj.jdbc.Driver
,反射获取失去该类,并进行实例化。
2.1.2 注册驱动类
实例化 MySQL 驱动的时候,会通过 DriverManager#registerDriver 办法来注册驱动。
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() }}
其底层实现是,将 MySQL 驱动 Driver 对象包装为 DriverInfo,存储在 DriverManager#registeredDrivers 汇合中。
java.sql.DriverManager#registerDriver
// List of registered JDBC driversprivate 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 建设连贯
应用 DriverManager#getConnection 办法来建设与 MySQL 服务器的连贯。
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "test", "test");
其中遍历 CopyOnWriteArrayList<DriverInfo> registeredDrivers
中已注册的数据库驱动,这里是 com.mysql.cj.jdbc.Driver 实例。
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()); }}
通过 MySQL 数据库驱动来建设连贯:
- 依据数据库地址、用户名明码等参数,构建 ConnectionUrl 对象,该对象默认是 SINGLE_CONNECTION 类型。
- 依据 ConnectionUrl 对象,创立数据库连贯实例 ConnectionImpl,建设 Socket 连贯。
2.2.1 构建 ConnectionUrl 对象
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 驱动协定阐明
1. Failover
jdbc:mysql://[primary-host]:[port],[secondary-host]:[port],.../[database]?[property=<value>]&[property=<value>]
即 Client 链接生效时,将会尝试与其余 host 建设链接,这个过程对 application 是通明的。读(写)操作总是只产生在一个 host 上。
2. Load Balancing
jdbc:mysql:loadbalance://[host]:[port],[host]:[port],...[/database]?[property=<value>]&[property=<value>]
格局同 failover\replication 相似,所有 host 没有主次之分都是平级,反对参数管制。
负载平衡策略定义了 BalanceStrategy 接口,mysql 反对曾经实现该接口的策略有:
- BestResponseTimeBalanceStrategy:选中事务响应最快的 host
- RandomBalanceStrategy:(默认)随机选中一个 host
- SequentialBalanceStrategy:第一次随机之后程序选后一个至周而复始
3. Replication
jdbc:mysql:replication://[master-host]:[port],[slave-host]:[port],.../database?[property=<value>]
具体格局相似 failover,比拟大的变动是第一个 host 为 master 库是 write/read 模式,前面都是 slave 库是 read 模式,也是反对参数进行配置。
replication 协定是建设在 failover 和 loadbalance 根底上,适应 Replication 架构须要为解决读写拆散、负载平衡场景的。
在事务 read only 模式下申请会被转向到 slave host,若多个 slave 状况下采纳 round-robin(轮询)策略。
对于非 read only 申请(write/read)都将转向到 master host。
6.5.1.27 后版本反对多个 master,多个 master 下采纳 load balance 策略,具体参考 loadbalance 协定介绍。
7.5.1.28 版本后又反对动静增加节点,也就是程序运行是动静增加新的 host 到 URL 中而不须要重启服务器,咱们常常会聊的动静数据源场景。
本节参考:
mysql驱动协定之loadbalance和replication
Chapter 8 Multi-Host Connections
2.3 构建 ConnectionImpl 实例
构建 ConnectionImpl 实例,其中会创立 Socket 连贯。
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
建设会话,这里用的是 BIO。
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 执行语句
2.3.1 Statement
Statement statement = conn.createStatement();ResultSet resultSet = statement.executeQuery("select * from t_student");
发动 SQL 查问,获取返回后果:
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);}
发动 SQL 查问,分为两种形式:1. 字符串;2. 二进制数据包。
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);}
因为 SQL 语句为 select * from t_student
,先执行 sendQueryString() 办法将字符串转换为二级制包(NativePacketPayload 类实例),再调用 sendQueryPacket() 办法发送数据。
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;}
向 mysql 服务器发送数据包。
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
PreparedStatement 的性能相似 Statement,但不同的是 PreparedStatement 能够应用占位符,它是由占位符标识须要输出数据的地位,而后再逐个填入数据。当然,PreparedStatement 也能够执行没有占位符的 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();
接口 java.sql.PreparedStatement 在 MySQL 驱动中对应的实现类为 com.mysql.cj.jdbc.ClientPreparedStatement
应用 PreparedStatement 进行查问:
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;}
其中,首先对 SQL 中的占位符进行填补,再打成二进制包进行发送:
com.mysql.cj.AbstractPreparedQuery#fillSendPacket
sendPacket.writeBytes(StringLengthDataType.STRING_FIXED, bindValues[i].getByteValue());
对于原始的 SQL:select * from t_student where id = ? and age = ?
依据占位符拆分成三个字符串,再遍历各个字符串,绑定上参数。
- select * from t_student where id =
- and age =
- 空字符串
补充完参数之后,再打成一个二进制包,发送给数据库服务器。
后续流程与应用 Statement 统一:
com.mysql.cj.jdbc.ClientPreparedStatement#executeInternal
com.mysql.cj.NativeSession#execSQL
com.mysql.cj.protocol.a.NativeProtocol#sendQueryPacket
2.4 获取响应
向 MySQL 发送完数据之后,读取响应。
- 先读取响应的列数(即有多少个字段)。
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;}
- 读取具体每一行的数据(字段格局和字段内容)。
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 读取字段信息
循环遍历每一列,对字段的类型信息进行读取和解析。
com.mysql.cj.protocol.a.NativeProtocol#read
com.mysql.cj.protocol.a.ColumnDefinitionReader#read
@Overridepublic 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);}
例如,读取到 testdb 库的 t_student 表的 id 字段的信息,封装成 Field 类如下:
2.4.2 读取字段的值
从二进制数据包中,解析失去多个字段的值。
com.mysql.cj.protocol.a.ResultsetRowReader#read
@Overridepublic 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); // 解析数据}
例如 id 为 1 的表数据如下:
这里读取到的二进制内容如下右边所示:
01 31 05 68 75 6e 61 6e . 1 . h u n a n02 31 31 08 7a 68 61 6e . 1 1 . z h a n67 73 61 6e g s a n
解析二进制数据包:
com.mysql.cj.protocol.a.TextRowFactory#createFromMessage
@Overridepublic 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 结构 Resultset
最初,回到 com.mysql.cj.protocol.a.TextResultsetReader#read 办法中。
将每一行数据解析后失去的 ResultsetRow 对象,存入汇合,寄存在 ResultsetRows 对象中。再依据 ResultsetRows 对象来结构 Resultset 对象。
即: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. 总结
- JDBC 应用 SPI 机制加载数据库驱动,这是为了解决 BootstrapClassloader 无奈加载第三方的类的问题,将第三方的类委托给线程上下文类加载器来加载。
- 获取数据库连贯对象 Connection,实际上是对数据库建设 Socket 连贯。能够应用数据库连接池,以反复利用 Connection。
- 通过 Statement 来查问数据,底层是向 Socket 写入二进制数据,再从 Socket 读取二进制数据,封装在 Resultset 对象之中。
作者:Sumkor
链接:https://segmentfault.com/a/11...