`

mysql prepareStatement的源码实现分析

阅读更多

 

今天分析了一下mysql 5.1 版本驱动包的prepareStatement实现源码,发现驱动包并没有实现真正的服务器预编译,还是跟普通的Statement一样,在客户端拼装好完整的sql,底层还是用socket与服务器通过二进制协议流进行数据交互,然后把请求返回的结果,生成resultSet数据集合,以方便后续的数据迭代处理.

 

 

	public java.sql.ResultSet executeQuery() throws SQLException {
		checkClosed();
		
		ConnectionImpl locallyScopedConn = this.connection;
		
		checkForDml(this.originalSql, this.firstCharOfStmt);

		CachedResultSetMetaData cachedMetadata = null;

		// We need to execute this all together
		// So synchronize on the Connection's mutex (because
		// even queries going through there synchronize
		// on the same mutex.
		synchronized (locallyScopedConn.getMutex()) {
			clearWarnings();

			boolean doStreaming = createStreamingResultSet();
			
			this.batchedGeneratedKeys = null;

			// Adjust net_write_timeout to a higher value if we're
			// streaming result sets. More often than not, someone runs into
			// an issue where they blow net_write_timeout when using this
			// feature, and if they're willing to hold a result set open
			// for 30 seconds or more, one more round-trip isn't going to hurt
			//
			// This is reset by RowDataDynamic.close().
			
			if (doStreaming
					&& this.connection.getNetTimeoutForStreamingResults() > 0) {
				locallyScopedConn.execSQL(this, "SET net_write_timeout="
						+ this.connection.getNetTimeoutForStreamingResults(),
						-1, null, ResultSet.TYPE_FORWARD_ONLY,
						ResultSet.CONCUR_READ_ONLY, false, this.currentCatalog,
						null, false);
			}
			
              /*
               这里是通过方法 fillSendPacket 把相关的请求数据组装成二进制字节流,以方便后续发送给服务端,通过二进制协议,把sql进行了一层包装             
             */
			Buffer sendPacket = fillSendPacket();

			if (this.results != null) {
				if (!this.connection.getHoldResultsOpenOverStatementClose()) {
					if (!this.holdResultsOpenOverClose) {
						this.results.realClose(false);
					}
				}
			}

			String oldCatalog = null;

			if (!locallyScopedConn.getCatalog().equals(this.currentCatalog)) {
				oldCatalog = locallyScopedConn.getCatalog();
				locallyScopedConn.setCatalog(this.currentCatalog);
			}

			//
			// Check if we have cached metadata for this query...
			//
			if (locallyScopedConn.getCacheResultSetMetadata()) {
				cachedMetadata = locallyScopedConn.getCachedMetaData(this.originalSql);
			}

			Field[] metadataFromCache = null;
			
			if (cachedMetadata != null) {
				metadataFromCache = cachedMetadata.fields;
			}
			
			if (locallyScopedConn.useMaxRows()) {
				// If there isn't a limit clause in the SQL
				// then limit the number of rows to return in
				// an efficient manner. Only do this if
				// setMaxRows() hasn't been used on any Statements
				// generated from the current Connection (saves
				// a query, and network traffic).
				if (this.hasLimitClause) {
					this.results = executeInternal(this.maxRows, sendPacket,
							createStreamingResultSet(), true,
							metadataFromCache, false);
				} else {
					if (this.maxRows <= 0) {
						executeSimpleNonQuery(locallyScopedConn,
								"SET OPTION SQL_SELECT_LIMIT=DEFAULT");
					} else {
						executeSimpleNonQuery(locallyScopedConn,
								"SET OPTION SQL_SELECT_LIMIT=" + this.maxRows);
					}

					this.results = executeInternal(-1, sendPacket,
							doStreaming, true,
							metadataFromCache, false);

					if (oldCatalog != null) {
						this.connection.setCatalog(oldCatalog);
					}
				}
			} else {
				this.results = executeInternal(-1, sendPacket,
						doStreaming, true,
						metadataFromCache, false);
			}

			if (oldCatalog != null) {
				locallyScopedConn.setCatalog(oldCatalog);
			}
			
			if (cachedMetadata != null) {
				locallyScopedConn.initializeResultsMetadataFromCache(this.originalSql,
						cachedMetadata, this.results);
			} else {
				if (locallyScopedConn.getCacheResultSetMetadata()) {
					locallyScopedConn.initializeResultsMetadataFromCache(this.originalSql,
							null /* will be created */, this.results);
				}
			}
		}

		this.lastInsertId = this.results.getUpdateID();

		return this.results;
	}

 

 

 

下面我们再来一齐分析看看,fillsendPacket如何进行请求数据的包装.

 

 

	/*

	这里的 parameterValues parameterStreams存放了对应的sql与数据值.
	        
	parameterValues 就是存放着下述的sql,不过它会根据 ? 号把sql分拆存到parameterValues二维数组去.	
		sql = "select * from userjf where aid = ?";
                PreparedStatement ps = conn.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY,  
                        ResultSet.CONCUR_READ_ONLY);

	parameterStreams 存放着下述的数据值,通过二维数组对应存放
	ps.setString(1, "param1');
	ps.setString(2, "param2');
		
	*/

	protected Buffer fillSendPacket() throws SQLException {
		return fillSendPacket(this.parameterValues, this.parameterStreams,
				this.isStream, this.streamLengths);
	}

 

 

 

	protected Buffer fillSendPacket(byte[][] batchedParameterStrings,
			InputStream[] batchedParameterStreams, boolean[] batchedIsStream,
			int[] batchedStreamLengths) throws SQLException {
		/*
		 这里是获取数据发送缓冲区
		*/
		Buffer sendPacket = this.connection.getIO().getSharedSendPacket();
		/*
		缓冲区的事前清理
		*/
		sendPacket.clear();
		/*
		下面开始组装二进制协议流,第一字节写入了3,表示这是查询协议
		*/
		sendPacket.writeByte((byte) MysqlDefs.QUERY);

		boolean useStreamLengths = this.connection
				.getUseStreamLengthsInPrepStmts();

		//
		// Try and get this allocation as close as possible
		// for BLOBs
		//
		int ensurePacketSize = 0;

		String statementComment = this.connection.getStatementComment();
		
		byte[] commentAsBytes = null;
		
		if (statementComment != null) {
			if (this.charConverter != null) {
				commentAsBytes = this.charConverter.toBytes(statementComment);
			} else {
				commentAsBytes = StringUtils.getBytes(statementComment, this.charConverter,
						this.charEncoding, this.connection
								.getServerCharacterEncoding(), this.connection
								.parserKnowsUnicode());
			}
			
			ensurePacketSize += commentAsBytes.length;
			ensurePacketSize += 6; // for /*[space] [space]*/
		}
	
		for (int i = 0; i < batchedParameterStrings.length; i++) {
			if (batchedIsStream[i] && useStreamLengths) {
				ensurePacketSize += batchedStreamLengths[i];
			}
		}

		if (ensurePacketSize != 0) {
			sendPacket.ensureCapacity(ensurePacketSize);
		}

		if (commentAsBytes != null) {
			sendPacket.writeBytesNoNull(Constants.SLASH_STAR_SPACE_AS_BYTES);
			sendPacket.writeBytesNoNull(commentAsBytes);
			sendPacket.writeBytesNoNull(Constants.SPACE_STAR_SLASH_SPACE_AS_BYTES);
		}
		
		/*
		 这里有两个重要的数据,staticSqlStrings batchedParameterStreams,均为二维数组,分别记录的sql与对应值.
		 例如我们 sql为 select * from user where id = ? and name = ?,对应设置的值为 123, jack则staticSqlStrings batchedParameterStreams两个二维数组记录的内容为:
			staticSqlStrings: {"select * from user where id =","","and name = ",""}
			batchedParameterStreams: {123,"jack"}

		 下面接下来的处理,实际上就是把 staticSqlStrings与batchedParameterStreams的数据进行组装,组装成实际的sql,select * from user where id = 123 and name = 'jack',然后
		 再拼接到请求协议数据sendPacket缓存去.
		*/
		for (int i = 0; i < batchedParameterStrings.length; i++) {
			if ((batchedParameterStrings[i] == null)
					&& (batchedParameterStreams[i] == null)) {
				throw SQLError.createSQLException(Messages
						.getString("PreparedStatement.40") //$NON-NLS-1$
						+ (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS);
			}

			sendPacket.writeBytesNoNull(this.staticSqlStrings[i]);

			if (batchedIsStream[i]) {
				streamToBytes(sendPacket, batchedParameterStreams[i], true,
						batchedStreamLengths[i], useStreamLengths);
			} else {
				sendPacket.writeBytesNoNull(batchedParameterStrings[i]);
			}
		}

		sendPacket.writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]);
		

		return sendPacket;
	}

 

 

因此从这里我们可以发现,实际上mysql的prepareStatement,只是把 sql与对应的参数值进行了组装,变成了完整的sql,然后再进行数据请求.

针对普通的Statement,存在着依赖注入的问题.如:

登录模块
如:
User validUser = login.getUserInfo(user.getName());这里的user.getName()是前台从Textfield控件中获得得值,没有做任何处理,于是再看看getUserInfo的方法,如下:

 public User getUserInfo(String userName)
    {
        User validUser = null;
        String sql = "Select * from WEB_USER where NAME='" + userName + "'";
        Database db = null;
        ResultSet rs = null;
        try {
            db = new Database("XXX");
            rs = db.execQuery(sql);   
            if (rs != null && rs.next()) {
                 validUser = new User();
                 ....
              }
            }
         }

 我们看到从前台传过来的userName没有经过任何处理而直接凭凑的SQL语句,所以如果输入者精心构造的话,就可以突破第一个屏障,生成一个有效的用户对象,比如:

输入: cjcj' or '1'='1

这样的字符串输入到后台的SQL语句就为:
select * from web_user where name='cjcj' or '1'='1'
显然,这个rs肯定是非空的。我们完成了突破第一个屏障的任务。

 

而在prepareStatement得到了改善,它在进行参数设置时,对存在依赖注入风险的参数值进行了特殊处理,如上述输入: cjcj' or '1'='1 ,它会处理成 cjcj\' or \'1\'=\'1 ,从而最后拼装成sql时,变成了 select * from web_user where name='cjcj\' or \'1\'=\'1',下面我们抽取 prepareStatement.setString(int parameterIndex, String x)方法 一齐来看看源码实现.

	/**
	 * Set a parameter to a Java String value. The driver converts this to a SQL
	 * VARCHAR or LONGVARCHAR value (depending on the arguments size relative to
	 * the driver's limits on VARCHARs) when it sends it to the database.
	 * 
	 * @param parameterIndex
	 *            the first parameter is 1...
	 * @param x
	 *            the parameter value
	 * 
	 * @exception SQLException
	 *                if a database access error occurs
	 */
	public void setString(int parameterIndex, String x) throws SQLException {
		// if the passed string is null, then set this column to null
		if (x == null) {
			setNull(parameterIndex, Types.CHAR);
		} else {
			checkClosed();
			
			int stringLength = x.length();

			if (this.connection.isNoBackslashEscapesSet()) {
				// Scan for any nasty chars

				boolean needsHexEscape = isEscapeNeededForString(x,
						stringLength);

				if (!needsHexEscape) {
					byte[] parameterAsBytes = null;

					StringBuffer quotedString = new StringBuffer(x.length() + 2);
					quotedString.append('\'');
					quotedString.append(x);
					quotedString.append('\'');
					
					if (!this.isLoadDataQuery) {
						parameterAsBytes = StringUtils.getBytes(quotedString.toString(),
								this.charConverter, this.charEncoding,
								this.connection.getServerCharacterEncoding(),
								this.connection.parserKnowsUnicode());
					} else {
						// Send with platform character encoding
						parameterAsBytes = quotedString.toString().getBytes();
					}
					
					setInternal(parameterIndex, parameterAsBytes);
				} else {
					byte[] parameterAsBytes = null;

					if (!this.isLoadDataQuery) {
						parameterAsBytes = StringUtils.getBytes(x,
								this.charConverter, this.charEncoding,
								this.connection.getServerCharacterEncoding(),
								this.connection.parserKnowsUnicode());
					} else {
						// Send with platform character encoding
						parameterAsBytes = x.getBytes();
					}
					
					setBytes(parameterIndex, parameterAsBytes);
				}

				return;
			}

			String parameterAsString = x;
			boolean needsQuoted = true;
			
			if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
				needsQuoted = false; // saves an allocation later
				
				/*
				1 创建缓冲区 buf
				2 缓存区开始位置放入 ' 号(你懂的)
				3 把字符串类型的参数值X,一个字符一个字符的进行判断处理,若为特殊符号则进行转义处理,如遇到 ' 号,则转义为 \' .
				4 缓存区最后位置放入  ' 号(你懂的)

				  若x值为 cjcj' or '1'='1 ,则最后缓存区内容为 'cjcj\' or \'1\'=\'1'
				*/
				StringBuffer buf = new StringBuffer((int) (x.length() * 1.1));
				buf.append('\'');
	
				for (int i = 0; i < stringLength; ++i) {
					char c = x.charAt(i);
	
					switch (c) {
					case 0: /* Must be escaped for 'mysql' */
						buf.append('\\');
						buf.append('0');
	
						break;
	
					case '\n': /* Must be escaped for logs */
						buf.append('\\');
						buf.append('n');
	
						break;
	
					case '\r':
						buf.append('\\');
						buf.append('r');
	
						break;
	
					case '\\':
						buf.append('\\');
						buf.append('\\');
	
						break;
	
					case '\'':
						buf.append('\\');
						buf.append('\'');
	
						break;
	
					case '"': /* Better safe than sorry */
						if (this.usingAnsiMode) {
							buf.append('\\');
						}
	
						buf.append('"');
	
						break;
	
					case '\032': /* This gives problems on Win32 */
						buf.append('\\');
						buf.append('Z');
	
						break;
	
					default:
						buf.append(c);
					}
				}
	
				buf.append('\'');
	
				parameterAsString = buf.toString();
			}

			byte[] parameterAsBytes = null;

			if (!this.isLoadDataQuery) {
				if (needsQuoted) {
					parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString,
						'\'', '\'', this.charConverter, this.charEncoding, this.connection
								.getServerCharacterEncoding(), this.connection
								.parserKnowsUnicode());
				} else {
					parameterAsBytes = StringUtils.getBytes(parameterAsString,
							this.charConverter, this.charEncoding, this.connection
									.getServerCharacterEncoding(), this.connection
									.parserKnowsUnicode());
				}
			} else {
				// Send with platform character encoding
				parameterAsBytes = parameterAsString.getBytes();
			}

			setInternal(parameterIndex, parameterAsBytes);
			
			this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.VARCHAR;
		}
	}

 

 

 

分享到:
评论

相关推荐

    JAVA连mysql源码包JDBC

    在Java编程领域,JDBC...总之,Java通过JDBC连接MySQL提供了灵活且强大的数据访问能力,而源码包的分析则有助于开发者深入理解底层机制,提升技能水平。学习并掌握这部分内容对于任何Java开发者来说都是极其有价值的。

    加入数据库mysql实现android注册登陆功能的客户端服务器源码与解析

    PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, username); stmt.setString(2, encryptedPassword); stmt.setString(3, email); stmt.executeUpdate(); } catch (SQLException e) { ...

    mysql oracle 插入blob数据实例源码

    OraclePreparedStatement pstmt = (OraclePreparedStatement) conn.prepareStatement(sql); pstmt.setInt(1, 1); // 假设id为1 File imageFile = new File("path_to_your_image.jpg"); OracleLob lob = Oracle...

    JDBC.txt是链接mysql数据库的源码,复制粘贴即可,需要mysql.jar包

    PreparedStatement ptmt=conn.prepareStatement(sql); ResultSet rs= ptmt.executeQuery(); //查看结果 while(rs.next()){ System.out.println( rs.getString("name")); //资源回收 } rs.close(); ptmt....

    向数据库插入blob数据实例源码--在mysql中通过

    Blob(Binary Large Object)在数据库中用于存储二进制大对象,如图片、音频或视频文件等...源码中可能包含了类似的实现,通过解压提供的"向数据库插入blob数据实例源码"压缩包,你可以查看并运行完整的Java代码示例。

    系统实战开发之使用MySQL实现保存源代码

    PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, "Demo.java"); pstmt.setString(2, "John Doe"); pstmt.setString(3, "public class Demo { public static void main(String[] args...

    MySql Connector Java 5.1.23.rar

    7. **执行 SQL 语句**:通过 `java.sql.Connection` 接口创建的连接对象,可以调用 `createStatement()` 或 `prepareStatement()` 方法来创建 `Statement` 或 `PreparedStatement` 对象,然后使用它们来执行 SQL ...

    mysql在java中的调用源码

    ps = conn.prepareStatement(sql); // 创建PreparedStatement对象 ps.setString(1, loginName); ps.setString(2, loginPwd); rs = ps.executeQuery(); // 执行SQL语句 if (rs.next()) { loginSuccess = true; ...

    jdbc实现数据库增删改查源码

    Java Database Connectivity(JDBC)是Java编程语言中用于与各种数据库进行交互的一种API。它为开发者提供了一套标准...在提供的源码中,你可以找到具体实现这些操作的示例,这对于学习和提升JDBC使用技巧非常有帮助。

    mysql与oracle数据互导

    PreparedStatement oracleStmt = oracleConn.prepareStatement("INSERT INTO mytable (col1, col2) VALUES (?, ?)"); while (rs.next()) { oracleStmt.setString(1, rs.getString("col1")); oracleStmt....

    jdbc连接MySQL的增删改查操作源码

    PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, user.getName()); pstmt.setString(2, user.getEmail()); pstmt.executeUpdate(); } catch (Exception e) { e.printStackTrace...

    安卓SQLite数据库相关-基于android平台通过JDBC连接MySQL数据库实现增删查改操作.rar

    PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } } // 其他增删查改的方法,如insert, delete, select等 } ``` 请注意...

    Java+JDBC+MySQL连接教程word版+源码.zip

    PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM mytable WHERE id = ?"); ``` 5. **执行SQL**: 使用`executeQuery()`或`executeUpdate()`方法执行SQL语句。前者用于查询,返回`ResultSet`;后...

    PreparedStatement和Statement

    PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, "John Doe"); pstmt.setInt(2, 30); pstmt.executeUpdate(); ``` 3. **比较与选择** - **性能**:由于预编译,`...

    纯sql分页源码java版

    本资源提供的"纯sql分页源码java版"聚焦于如何在Java中利用SQL语句实现分页查询,支持Oracle、MySQLser(可能是MySQL Server的误写)和MySQL三种不同的数据库系统。以下将详细讲解这个主题。 首先,分页的核心思想...

    Java连接SQL源码

    try (PreparedStatement pstmt = conn.prepareStatement( "INSERT INTO Employees (id, name, age) VALUES (?, ?, ?)")) { pstmt.setInt(1, 100); pstmt.setString(2, "John Doe"); pstmt.setInt(3, 30); pstmt...

    web 开发 mysql+jdbc 实现信息分页显示

    PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, pageSize); pstmt.setInt(2, (currentPage - 1) * pageSize); ResultSet rs = pstmt.executeQuery(); while (rs.next()) { // 处理...

    Mysql分页案例

    PreparedStatement pstmt = connection.prepareStatement(sql); pstmt.setInt(1, (pageNo - 1) * pageSize); // pageNo为当前页数,pageSize为每页大小 pstmt.setInt(2, pageSize); ResultSet rs = pstmt....

    excel转换成JAVA到Mysql

    PreparedStatement pstmt = conn.prepareStatement("INSERT INTO your_table (column1, column2) VALUES (?, ?)"); for (Row row : sheet) { if (row.getRowNum() == 0) continue; // 跳过表头 Cell cell1 = ...

    java访问MySQL数据库的JDBC驱动程序

    5. **执行SQL语句**:`java.sql.Connection`接口提供了`createStatement()`,`prepareStatement()`等方法创建SQL语句对象。`Statement`用于执行简单SQL,而`PreparedStatement`则用于预编译的SQL,能提高性能并防止...

Global site tag (gtag.js) - Google Analytics