為什么要使用PreparedStatement?
一、通過PreparedStatement提升性能
Statement主要用於執行靜態SQL語句,即內容固定不變的SQL語句。Statement每執行一次都要對傳入的SQL語句編譯一次,效率較差。
某些情況下,SQL語句只是其中的參數有所不同,其余子句完全相同,適用於PreparedStatement。
PreparedStatement的另外一個好處就是預防sql注入攻擊
PreparedStatement是接口,繼承自Statement接口。
使用PreparedStatement時,SQL語句已提前編譯,三種常用方法 execute、 executeQuery 和 executeUpdate 已被更改,以使之不再需要參數。
PreparedStatement 實例包含已事先編譯的 SQL 語句,SQL 語句可有一個或多個 IN 參數,IN參數的值在 SQL 語句創建時未被指定。該語句為每個 IN 參數保留一個問號(“?”)作為占位符。
每個問號的值必須在該語句執行之前,通過適當的setInt或者setString 等方法提供。
由於 PreparedStatement 對象已預編譯過,所以其執行速度要快於 Statement 對象。因此,多次執行的 SQL 語句經常創建為 PreparedStatement 對象,以提高效率。
通常批量處理時使用PreparedStatement。
1 //SQL語句已發送給數據庫,並編譯好為執行作好准備 2 PreparedStatement pstmt = con.prepareStatement( 3 "UPDATE emp SET job= ? WHERE empno = ?"); 4 //對占位符進行初始化 5 pstmt.setLong(1, "Manager"); 6 pstmt.setInt(2,1001); 7 //執行SQL語句 8 pstmt.executeUpdate();
小案例:分別向數據庫插入1000條記錄。分別記錄執行時間,然后進行比較。
新建項目:
使用Statement的 執行效率的類INSERT3:
1 package com.cnblogs.daliu_it; 2 3 import java.sql.Connection; 4 import java.sql.PreparedStatement; 5 import java.sql.Statement; 6 7 /** 8 * 使用Statement的 執行效率 9 * 10 */ 11 public class INSERT3 { 12 13 public static void main(String[] args) { 14 Connection conn = null; 15 try { 16 conn = DBUtility.getConnection(); 17 Statement state = conn.createStatement(); 18 long start = System.currentTimeMillis(); 19 20 for (int i = 7000; i < 8000; i++) { 21 String sql = "INSERT INTO user VALUES" + "(" + i + "," 22 + "'test" + i + "'," + "'12345'," + "5000," + "'test" 23 + i + "@qq.com'" + ")"; 24 state.executeUpdate(sql); 25 } 26 System.out.println("插入完畢"); 27 long end = System.currentTimeMillis(); 28 System.out.println("耗時:" + (end - start)); 29 } catch (Exception e) { 30 System.out.println("插入數據失敗"); 31 e.printStackTrace(); 32 } finally { 33 DBUtility.closeConnection(conn); 34 } 35 } 36 }
測試數據:
使用預編譯PreparedStatement SQL提高執行效率的類INSERT2:
1 package com.cnblogs.daliu_it; 2 3 import java.sql.Connection; 4 import java.sql.PreparedStatement; 5 6 /** 7 * 使用預編譯PreparedStatement SQL提高執行效率 8 * 9 */ 10 public class INSERT2 { 11 public static void main(String[] args) { 12 Connection conn = null; 13 try { 14 conn = DBUtility.getConnection(); 15 // Statement state= conn.createStatement(); 16 17 String sql = "INSERT INTO user VALUES(?,?,'123456',?,?)"; 18 /* 19 * 根據給定的預編譯SQL語句創建一個 PreparedStatement 20 */ 21 PreparedStatement ps = conn.prepareStatement(sql); 22 23 long start = System.currentTimeMillis(); 24 25 for (int i = 9000; i < 10000; i++) { 26 ps.setInt(1, i); 27 ps.setString(2, "test" + i); 28 ps.setInt(3, 5000); 29 ps.setString(4, "test" + i + "@qq.com"); 30 ps.executeUpdate(); 31 } 32 System.out.println("插入完畢"); 33 long end = System.currentTimeMillis(); 34 System.out.println("耗時:" + (end - start)); 35 } catch (Exception e) { 36 System.out.println("插入數據失敗"); 37 e.printStackTrace(); 38 } finally { 39 DBUtility.closeConnection(conn); 40 } 41 } 42 }
測試效果:
二、通過PreparedStatement防止SQL Injection
對JDBC而言,SQL注入攻擊只對Statement有效,對PreparedStatement無效,因為PreparedStatement不允許在插入參數時改變SQL語句的邏輯結構。
使用預編譯的語句對象時,用戶傳入的任何數據不會和原SQL語句發生匹配關系,無需對輸入的數據做過濾。如果用戶將”or 1 = 1”傳入賦值給占位符,下述SQL語句將無法執行:select * from t where username = ? and password = ?;
PreparedStatement是Statement的子類,表示預編譯的SQL語句的對象。在使用PreparedStatement對象執行SQL命令時,命令被數據庫編譯和解析,並放入命令緩沖區。緩沖區中的預編譯SQL命令可以重復使用。
1 sql = "select * from users where NAME = ? and PWD = ?"; 2 System.out.println(sql); 3 4 5 con = DBUtility.getConnection(); 6 7 //通過Statement 的改為prepareStatement 8 stmt = con.prepareStatement(sql); 9 10 11 // rs = stmt.executeQuery(sql); 12 13 stmt.setString(1, username); 14 stmt.setString(2, password); 15 rs = stmt.executeQuery();
使用PreparedStatement來執行SQL語句。在SQL語句中有2個問號,在代碼中要給它們分別設置值,規則是:從左到右,對應1,2,...。
對於JDBC而言,SQL注入攻擊只對Statement有效,對PreparedStatement是無效的,這是因為PreparedStatement不允許在插入時改變查詢的邏輯結構。
例子:使用PreparedStatement實現用戶名和密碼的驗證功能。
(1) 使用Statement實現用戶名和密碼的驗證功能,並測試用戶名為“Tom”、密碼為“123”以及用戶名為“Tom”、密碼為“a' OR 'b'='b”是否能登錄成功。
(2)使用PreparedStatement實現用戶名和密碼的驗證功能,並測試用戶名為“Tom”、密碼為“a' OR 'b'='b”是否能登錄成功。
1.新建一個java項目,配置文件,導入所需要的jar包。如下圖:
2.首先創建user表:
1 2 create table users( 3 id int(4) auto_increment, 4 name varchar(50), 5 pwd varchar(50), 6 phone varchar(50) 7 ); 8 9 desc users; 10 11 select * from users; 12 13 insert into users(id,username,password)values(1,'Tom','123','110'); 14 insert into users(id,username,password)values(2,'Jerry','abc','119'); 15 insert into users(id,username,password)values(3,'Andy','456','112'); 16 17 18 select * from users;
3.連接數據庫類DBUtility:
1 package com.cnblogs.daliu_it; 2 3 import java.io.IOException; 4 import java.sql.Connection; 5 import java.sql.SQLException; 6 import java.util.Properties; 7 8 import org.apache.commons.dbcp.BasicDataSource; 9 /** 10 * 工具類 11 * @author daliu_it 12 * 13 */ 14 public class DBUtility { 15 private static BasicDataSource dataSource = null; 16 17 public DBUtility() { 18 } 19 public static void init() { 20 21 Properties dbProps = new Properties(); 22 // 取配置文件可以根據實際的不同修改 23 try { 24 dbProps.load(DBUtility.class.getClassLoader().getResourceAsStream( 25 "com/cnblogs/daliu_it/db.properties")); 26 } catch (IOException e) { 27 e.printStackTrace(); 28 } 29 30 try { 31 String driveClassName = dbProps.getProperty("jdbc.driverClassName"); 32 String url = dbProps.getProperty("jdbc.url"); 33 String username = dbProps.getProperty("jdbc.username"); 34 String password = dbProps.getProperty("jdbc.password"); 35 36 String initialSize = dbProps.getProperty("dataSource.initialSize"); 37 String minIdle = dbProps.getProperty("dataSource.minIdle"); 38 String maxIdle = dbProps.getProperty("dataSource.maxIdle"); 39 String maxWait = dbProps.getProperty("dataSource.maxWait"); 40 String maxActive = dbProps.getProperty("dataSource.maxActive"); 41 42 dataSource = new BasicDataSource(); 43 dataSource.setDriverClassName(driveClassName); 44 dataSource.setUrl(url); 45 dataSource.setUsername(username); 46 dataSource.setPassword(password); 47 48 // 初始化連接數 49 if (initialSize != null) 50 dataSource.setInitialSize(Integer.parseInt(initialSize)); 51 52 // 最小空閑連接 53 if (minIdle != null) 54 dataSource.setMinIdle(Integer.parseInt(minIdle)); 55 56 // 最大空閑連接 57 if (maxIdle != null) 58 dataSource.setMaxIdle(Integer.parseInt(maxIdle)); 59 60 // 超時回收時間(以毫秒為單位) 61 if (maxWait != null) 62 dataSource.setMaxWait(Long.parseLong(maxWait)); 63 64 // 最大連接數 65 if (maxActive != null) { 66 if (!maxActive.trim().equals("0")) 67 dataSource.setMaxActive(Integer.parseInt(maxActive)); 68 } 69 } catch (Exception e) { 70 e.printStackTrace(); 71 System.out.println("創建連接池失敗!請檢查設置!!!"); 72 } 73 } 74 75 /** 76 * 數據庫連接 77 * @return 78 * @throws SQLException 79 */ 80 public static synchronized Connection getConnection() throws SQLException { 81 if (dataSource == null) { 82 init(); 83 } 84 Connection conn = null; 85 if (dataSource != null) { 86 conn = dataSource.getConnection(); 87 } 88 return conn; 89 } 90 91 /** 92 * 關閉數據庫 93 * @param conn 94 */ 95 public static void closeConnection(Connection conn){ 96 if(conn!=null){ 97 try { 98 conn.close(); 99 } catch (SQLException e) { 100 System.out.println("關閉資源失敗"); 101 e.printStackTrace(); 102 } 103 } 104 } 105 106 }
4.使用Statement實現驗證用戶名密碼是否存在的方法的類UserDAO:
1 package com.cnblogs.daliu_it; 2 3 import java.sql.Connection; 4 import java.sql.ResultSet; 5 import java.sql.SQLException; 6 import java.sql.Statement; 7 8 public class UserDAO { 9 10 /** 11 * 使用Statement實現驗證用戶名密碼是否存在的方法 12 * 13 * @param username 14 * @param password 15 */ 16 public void login(String username, String password) { 17 18 // Statement 19 Connection con = null; 20 Statement stmt = null; 21 ResultSet rs = null; 22 23 // 定義sql語句,用來查詢用戶名和密碼 24 String sql = null; 25 26 try { 27 sql = "select * from users where NAME = '" + username 28 + "' and PWD= '" + password + "'"; 29 30 // 檢查一下sql語句是否拼寫正確 31 System.out.println(sql); 32 33 // 獲得數據庫的連接 34 con = DBUtility.getConnection(); 35 36 stmt = con.createStatement(); 37 38 // 執行sql語句 39 rs = stmt.executeQuery(sql); 40 41 // 進行結果的遍歷,並給出相應的提示 42 if (rs.next()) { 43 System.out.println("登錄成功!"); 44 } else { 45 System.out.println("登錄失敗!"); 46 } 47 48 } catch (SQLException e) { 49 50 System.out.println("數據庫訪問異常!"); 51 throw new RuntimeException(e); 52 53 } finally { 54 55 // 最后關閉一下資源 56 if (con != null) { 57 DBUtility.closeConnection(con); 58 } 59 } 60 } 61 }
5.使用PreparedStatement實現驗證用戶名密碼是否存在的方法的類 UserDAO2:
1 package com.cnblogs.daliu_it; 2 3 import java.sql.Connection; 4 import java.sql.PreparedStatement; 5 import java.sql.ResultSet; 6 import java.sql.SQLException; 7 8 public class UserDAO2 { 9 10 /** 11 * 使用PreparedStatement實現驗證用戶名密碼是否存在的方法 12 * 13 * @param username 14 * @param password 15 */ 16 public void login(String username, String password) { 17 18 Connection con = null; 19 20 // 通過Statement 的改為prepareStatement 21 PreparedStatement stmt = null; 22 ResultSet rs = null; 23 24 String sql = null; 25 26 try { 27 // sql = "select * from users where NAME = '" + username+ 28 // "' and PWD= '" + password + "'"; 29 sql = "select * from users where NAME = ? and PWD = ?"; 30 // 使用PreparedStatement是將 "aa' or '1' = '1" 31 // 作為一個字符串賦值給問號“?”,使其作為"用戶名"字段的對應值,這樣來防止SQL注入。 32 33 System.out.println(sql); 34 con = DBUtility.getConnection(); 35 36 // 對於JDBC而言,SQL注入攻擊只對Statement有效,對PreparedStatement是無效的,這是因為PreparedStatement不允許在插入時改變查詢的邏輯結構。 37 stmt = con.prepareStatement(sql); 38 // rs = stmt.executeQuery(sql); 39 stmt.setString(1, username); 40 stmt.setString(2, password); 41 rs = stmt.executeQuery(); 42 43 // 進行結果的遍歷,並給出相應的提示 44 if (rs.next()) { 45 System.out.println("登錄成功!"); 46 } else { 47 System.out.println("登錄失敗!"); 48 } 49 50 System.out.println("執行完畢!"); 51 } catch (SQLException e) { 52 53 System.out.println("數據庫訪問異常!"); 54 throw new RuntimeException(e); 55 56 } finally { 57 58 // 最后關閉一下資源 59 if (con != null) { 60 DBUtility.closeConnection(con); 61 } 62 } 63 } 64 }
6.配置文件db.properties:
1 #Oracle 2 #jdbc.driverClassName=oracle.jdbc.OracleDriver 3 #jdbc.url=jdbc:oracle:thin:@localhost:1521:orcl 4 #jdbc.username=root 5 #jdbc.password=123456 6 7 #Mysql 8 jdbc.driverClassName=com.mysql.jdbc.Driver 9 jdbc.url=jdbc:mysql://localhost:3306/csdn 10 jdbc.username=root 11 jdbc.password=123456 12 13 dataSource.initialSize=10 14 dataSource.maxIdle=20 15 dataSource.minIdle=5 16 dataSource.maxActive=50 17 dataSource.maxWait=1000
7.測試類testCase:
1 package com.daliu_it.test; 2 3 import java.sql.SQLException; 4 5 import org.junit.Test; 6 7 import com.cnblogs.daliu_it.DBUtility; 8 import com.cnblogs.daliu_it.UserDAO; 9 import com.cnblogs.daliu_it.UserDAO2; 10 11 public class testCase { 12 13 /** 14 * 測試是否連接 15 * 16 * @throws SQLException 17 */ 18 @Test 19 public void testgetConnection() throws SQLException { 20 DBUtility db = new DBUtility(); 21 System.out.println(db.getConnection()); 22 } 23 24 /** 25 * 測試使用Statement實現驗證用戶名密碼是否存在的方法 26 */ 27 @Test 28 public void testStatementLogin() { 29 30 UserDAO dao = new UserDAO(); 31 // 用戶名不正確 32 dao.login("Tom1", "123"); 33 // 用戶名不正確 34 dao.login("Tom", "1234"); 35 // 正確 36 dao.login("Tom", "123"); 37 38 /** 39 * 這個也能登陸成功,不過這里會存在一個sql注入的問題 40 */ 41 dao.login("Tom", " a' OR 'b'='b "); 42 43 } 44 45 @Test 46 public void testPreparedStatementLogin() { 47 48 UserDAO2 dao = new UserDAO2(); 49 // 用戶名不正確 50 dao.login("Tom1", "123"); 51 // 用戶名不正確 52 dao.login("Tom", "1234"); 53 // 正確 54 dao.login("Tom", "123"); 55 // 測試是否還存在sql注入問題,不能登陸成功,說明我們已經解決了sql注入問題 56 dao.login("Tom", " a' OR 'b'='b "); 57 /** 58 * 實現機制不同,注入只對SQL語句的准備(編譯)過程有破壞作用,而PreparedStatement已經准備好了, 59 * 執行階段只是把輸入串作為數據處理,不再需要對SQL語句進行解析、准備,因此也就避免了SQL注入問題。 60 */ 61 62 } 63 64 }
測試效果:
(1)連接效果:
(2)測試使用Statement實現驗證用戶名密碼是否存在的方法
(3)測試使用PreparedStatement實現驗證用戶名密碼是否存在的方法
原文作者:daliu_it
博文出處:http://www.cnblogs.com/liuhongfeng/p/4175765.html
本文版權歸作者和博客園共有,但未經作者同意轉載必須保留以上的聲明且在放在文章頁面明顯位置。謝謝合作。