在Java編程中,應用代碼絕大多數使用了PreparedStatement,不管你是直接使用JDBC仍是使用框架。
在Java編程中,絕大多數使用了使用了PreparedStatement鏈接MySQL的應用代碼沒有啓用預編譯,不管你是直接使用JDBC仍是使用框架。
java
在我所能見到的項目中,幾乎沒有見過啓用MySQL預編譯功能的。網上更有文章說MySQL不支持預編譯,實在是貽害不淺。mysql
要想知道你的應用是否真正的使用了預編譯,請執行:show global status like '%prepare%';看看曾經編譯過幾條,當前Prepared_stmt_count 是多少。大多數是0吧?sql
這篇文章分如下幾個方面:
一.MySQL是支持預編譯的
打開MySQL日誌功能,啓動MySQL,而後 tail -f mysql.log.path(默認:/var/log/mysql/mysql.log).
create table axman_test (ID int(4) auto_increment primary key, name varchar(20),age int(4));
insert into axman_test (name,age) values ('axman',1000);
prepare myPreparedStmt from 'select * from axman_test where name = ?';
set @name='axman';
execute myPreparedStmt using @name ;
控制檯能夠正確地輸出:
mysql> execute myPreparedStmt using @name ;
+----+-------+------+
| ID | name | age |
+----+-------+------+
| 1 | axman | 1000 |
+----+-------+------+
1 row in set (0.00 sec)
而log文件中也忠實地記錄以下:
111028 9:25:06 51 Query prepare myPreparedStmt from 'select * from axman_test where name = ?'
51 Prepare select * from axman_test where name = ?
51 Query set @name='axman'
111028 9:25:08 51 Query execute myPreparedStmt using @name
51 Execute select * from axman_test where name = 'axman'
二.經過JDBC自己是能夠預編譯的,這個不用多說。至關於咱們把控制檯輸入的命令直接經過JDBC語句來執行:
Class.forName("org.gjt.mm.mysql.Driver");
String url = "jdbc:mysql://localhost:3306/mysql";
Connection conn = null;
try {
conn = DriverManager.getConnection(url, "root", "12345678");
Statement stmt = conn.createStatement();
/*如下忽略返回值處理*/
stmt.executeUpdate("prepare mystmt from 'select * from axman_test where name = ?'");
stmt.execute("set @name='axman'");
stmt.executeQuery("execute mystmt using @name");
stmt.close();
} finally {
if (conn != null) {
conn.close();
}
}
看日誌輸出:
111028 9:30:19 52 Connect root@localhost on mysql
52 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
52 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
52 Query SHOW COLLATION
52 Query SET NAMES latin1
52 Query SET character_set_results = NULL
52 Query SET autocommit=1
52 Query SET sql_mode='STRICT_TRANS_TABLES'
52 Query prepare mystmt from 'select * from axman_test where name = ?'
52 Prepare select * from axman_test where name = ?
52 Query set @name='axman'
52 Query execute mystmt using @name
52 Execute select * from axman_test where name = 'axman'
52 Quit 數據庫
三.默認的PrearedStatement不能開啓MySQL預編譯功能:
雖然第二節中咱們經過JDBC手工指定MySQL進行預編譯,可是PrearedStatement卻並不自動幫咱們作這件事。
Class.forName("org.gjt.mm.mysql.Driver");
String url = "jdbc:mysql://localhost:3306/mysql";
Connection conn = null;
try {
conn = DriverManager.getConnection(url, "root", "12345678");
PreparedStatement ps = conn.prepareStatement("select * from axman_test where name = ?");
ps.setString(1, "axman' or 1==1");
ResultSet rs = ps.executeQuery();
if (rs.next()) {
System.out.println(rs.getString(1));
}
Thread.sleep(1000);
rs.close();
ps.clearParameters();
ps.setString(1, "axman");
rs = ps.executeQuery();
if (rs.next()) {
System.out.println(rs.getString(1));
}
rs.close();
ps.close();
} finally {
if (conn != null) {
conn.close();
}
}
廢話少說,直接看日誌:
111028 9:54:03 53 Connect root@localhost on mysql
53 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
53 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
53 Query SHOW COLLATION
53 Query SET NAMES latin1
53 Query SET character_set_results = NULL
53 Query SET autocommit=1
53 Query SET sql_mode='STRICT_TRANS_TABLES'
53 Query select * from axman_test where name = 'axman\' or 1==1'
111028 9:54:04 53 Query select * from axman_test where name = 'axman'
53 Quit
兩條語句都是直接執行,而沒有預編譯。注意個人第一條語句select * from axman_test where name = 'axman\' or 1==1',下面還會說到它。
接着咱們改變一下jdbc.url的選項:
String url = "jdbc:mysql://localhost:3306/mysql?cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=256";
執行上面的代碼仍是沒有開啓Mysql的預編譯。
四.只有使用了useServerPrepStmts=true才能開啓Mysql的預編譯。
上面的代碼其它不變,只修改String url = "jdbc:mysql://localhost:3306/mysql?useServerPrepStmts=true";
查看日誌:
111028 10:04:52 54 Connect root@localhost on mysql
54 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
54 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
54 Query SHOW COLLATION
54 Query SET NAMES latin1
54 Query SET character_set_results = NULL
54 Query SET autocommit=1
54 Query SET sql_mode='STRICT_TRANS_TABLES'
54 Prepare select * from axman_test where name = ?
54 Execute select * from axman_test where name = 'axman\' or 1==1'
111028 10:04:53 54 Execute select * from axman_test where name = 'axman'
54 Close stmt
54 Quit
若是useServerPrepStmts=true,ConneciontImpl在prepareStatement時會產生一個 ServerPreparedStatement.在這個ServerPreparedStatement對象構造時首先會把當前SQL語句發送給 MySQL進行預編譯,而後將返回的結果緩存起來,其中包含預編譯的名稱(咱們能夠當作是當前SQL語句編譯後的函數名),簽名(參數列表),而後執行的 時候就會直接把參數傳給這個函數請求MySQL執行這個函數。不然返回的是客戶端預編譯語句,它僅作參數化工做,見第五節。
ServerPreparedStatement在請求預編譯和執行預編譯後的SQL 函數時,雖然和咱們上面手工預編譯工做相同,但它與MySQL交互使用的是壓縮格式,如prepare指令碼是22,這樣能夠減小交互時傳輸的數據量。編程
注意上面的代碼中,兩次執行使用的是同一個PreparedStatement句柄.若是使用個不一樣的PreparedStatement句柄,把代碼改爲:
Class.forName("org.gjt.mm.mysql.Driver");
String url = "jdbc:mysql://localhost:3306/mysql?useServerPrepStmts=true";
Connection conn = null;
try {
conn = DriverManager.getConnection(url, "root", "12345678");
PreparedStatement ps = conn.prepareStatement("select * from axman_test where name = ?");
ps.setString(1, "axman' or 1==1");
ResultSet rs = ps.executeQuery();
if (rs.next()) {
System.out.println(rs.getString(1));
}
Thread.sleep(1000);
rs.close();
ps.close();
ps = conn.prepareStatement("select * from axman_test where name = ?");
ps.setString(1, "axman");
rs = ps.executeQuery();
if (rs.next()) {
System.out.println(rs.getString(1));
}
rs.close();
ps.close();
} finally {
if (conn != null) {
conn.close();
}
}
再看日誌輸出:
Connect root@localhost on mysql
55 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
55 Query /* @MYSQL_CJ_FULL_PROD_NAME@ ( Revision: @MYSQL_CJ_REVISION@ ) */SELECT @@session.auto_increment_increment
55 Query SHOW COLLATION
55 Query SET NAMES latin1
55 Query SET character_set_results = NULL
55 Query SET autocommit=1
55 Query SET sql_mode='STRICT_TRANS_TABLES'
55 Prepare select * from axman_test where name = ?
55 Execute select * from axman_test where name = 'axman\' or 1==1'
111028 10:10:24 55 Close stmt
55 Prepare select * from axman_test where name = ?
55 Execute select * from axman_test where name = 'axman'
55 Close stmt
55 Quit
55 Quit
同一個SQL語句發生了兩次預編譯。這不是咱們想要的效果,要想對同一SQL語句屢次執行不是每次都預編譯,就要使用 cachePrepStmts=true,這個選項可讓JVM端緩存每一個SQL語句的預編譯結果,說白了就是以SQL語句爲key, 將預編譯結果緩存起來,下次遇到相同的SQL語句時做爲key去get一下看看有沒有這個SQL語句的預編譯結果,有就直接合出來用。咱們仍是以事實來講 明:
上面的代碼只修改String url = "jdbc:mysql://localhost:3306/mysql?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=256";
這行代碼中有其它參數本身去讀文檔,我很少囉嗦,執行的結果:
111028 10:27:23 58 Connect root@localhost on mysql
58 Query /* mysql-connector-java-5.1.18 ( Revision: tonci.grgin@oracle.com-20110930151701-jfj14ddfq48ifkfq ) */SHOW VARIABLES WHERE Variable_name ='language' OR Variable_name = 'net_write_timeout' OR Variable_name = 'interactive_timeout' OR Variable_name = 'wait_timeout' OR Variable_name = 'character_set_client' OR Variable_name = 'character_set_connection' OR Variable_name = 'character_set' OR Variable_name = 'character_set_server' OR Variable_name = 'tx_isolation' OR Variable_name = 'transaction_isolation' OR Variable_name = 'character_set_results' OR Variable_name = 'timezone' OR Variable_name = 'time_zone' OR Variable_name = 'system_time_zone' OR Variable_name = 'lower_case_table_names' OR Variable_name = 'max_allowed_packet' OR Variable_name = 'net_buffer_length' OR Variable_name = 'sql_mode' OR Variable_name = 'query_cache_type' OR Variable_name = 'query_cache_size' OR Variable_name = 'init_connect'
58 Query /* mysql-connector-java-5.1.18 ( Revision: tonci.grgin@oracle.com-20110930151701-jfj14ddfq48ifkfq ) */SELECT @@session.auto_increment_increment
58 Query SHOW COLLATION
58 Query SET NAMES latin1
58 Query SET character_set_results = NULL
58 Query SET autocommit=1
58 Query SET sql_mode='STRICT_TRANS_TABLES'
58 Prepare select * from axman_test where name = ?
58 Execute select * from axman_test where name = 'axman\' or 1==1'
111028 10:27:24 58 Execute select * from axman_test where name = 'axman'
58 Quit
注意僅發生一次預編譯,儘管代碼自己在第一次執行後關閉了ps.close();但由於使用了cachePrepStmts=true,底層並無真實關閉。緩存
千萬注意,同一條SQL語句儘可能在一個全局的地方定義,而後在不一樣地方引用,這樣作一是爲了DBA方便地對SQL作統一檢查和優化,就象IBatis把 SQL語句定義在XML文件中同樣。二是同一語句不一樣寫法,即便空格不一樣,大小寫不一樣也會從新預編譯,由於JVM端緩存是直接以SQL自己爲key而不會 對SQL格式化之後再作爲key。安全
咱們來看下面的輸出:session
35 Prepare select * from axman_test where name = ?
35 Execute select * from axman_test where name = 'axman\' or 1==1'
111029 9:54:31 35 Prepare select * FROM axman_test where name = ?
35 Execute select * FROM axman_test where name = 'axman' oracle
第一條語句和第二條語句的差異是FROM在第二條語句中被大寫了,這樣仍是發生了兩次預編譯。app
37 Prepare select * from axman_test where name = ?
37 Execute select * from axman_test where name = 'axman\' or 1==1'
111029 9:59:00 37 Prepare select * from axman_test where name = ?
37 Execute select * from axman_test where name = 'axman'
這裏兩條語句只是第二條的from後面多了個空格,由於你如今看到是HTML格式,若是不加轉義符,兩個空格也顯示一個空格,因此你能可看不到區別,但你能夠在本身的機器上試一下。
五.即便沒有開啓MySQL的預編譯,堅持使用PreparedStatement仍然很是必要。 在第三節的最後我說到"注意個人第一條語句select * from axman_test where name = 'axman\' or 1==1',下面還會說到它。",如今咱們回過頭來看,即便沒有開啓MySQL端的預編譯,咱們仍然要堅持使用PreparedStatement,由於 JVM端對PreparedStatement的SQL語句進行了參數化,即用佔位符替換參數,之後任何內容輸入都是字符串或其它類型的值,而不會和原始 的SQL語句拚接產生SQL注入,對字符串中的任何字符都會作檢查,若是多是SQL語句使用的標識符,會進行轉義。而後發送一個合法的安全的SQL語句 給數據庫執行。