本篇講訴為何在JDBC操作數據庫的過程中,要使用PreparedStatement對象來代替Statement對象。
在前面的JDBC學習中,對於Statement對象,我們已經知道是封裝SQL語句並發送給數據庫執行的功能,但是實際開發中,這個功能我們更經常用的是Statement類的子類PreparedStatement類的對象來實現,而不是采用Statement對象。
Statement和PreparedStatement的關系與區別在於:
① PreparedStatement類是Statement類的子類,擁有更多強大的功能。
② PreparedStatement類可以防止SQL注入攻擊的問題,后面會說到。
③ PreparedStatement會對SQL語句進行預編譯,以減輕數據庫服務器的壓力,而Statement則無法做到。
例1 :使用PreparedStatement對代碼中Statement進行更換
構建一張user表,接着我們要在程序中使用JDBC對數據庫進行User對象數據的添加,先用Statement對象來展示,后使用PreparedStatement對象,以此來比較兩者的不同。
在MySQL數據庫中新建jdbcdemo庫,並構建user表:
1:創建一個數據庫 create database jdbcdemo; 2:使用剛創建的數據庫 use jdbcdemo; 3:創建一個user表 create table user( id int primary key, name varchar(40), age int );
創建工程,在工程中導入數據庫連接驅動的jar包。在【src】目錄下新建一個database.properties文件,內容如下:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbcdemo
username=root
password=root
構建JDBC的工具類,包括注冊驅動,獲取連接,釋放資源和連接等,這部分同《JDBC操作數據庫的學習(2)》中相同,代碼如下:

1 public class JdbcUtils { 2
3 private static Properties config = new Properties(); 4
5 static{ 6 InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("database.properties"); 7 try{ 8 config.load(in); 9 Class.forName(config.getProperty("driver")); 10
11
12 }catch (Exception e) { 13 throw new ExceptionInInitializerError(e); 14 } 15 } 16
17 public static Connection getConnection() throws SQLException { 18 String url = config.getProperty("url"); 19 String username = config.getProperty("username"); 20 String password = config.getProperty("password"); 21 Connection conn = DriverManager.getConnection(url, username, password); 22 return conn; 23 } 24
25 public static void release(Connection conn,Statement st,ResultSet rs) { 26 if(rs!=null) { 27 try{ 28 rs.close(); 29 }catch (Exception e) { 30 e.printStackTrace(); 31 } 32 } 33 if(st!=null) { 34 try{ 35 st.close(); 36 }catch (Exception e) { 37 e.printStackTrace(); 38 } 39 } 40 if(conn!=null) { 41 try{ 42 conn.close(); 43 }catch (Exception e) { 44 e.printStackTrace(); 45 } 46 } 47 } 48 }
要添加User對象,那必須在工程中定義的domain包中做出User類的JavaBean:
1 public class User { 2 private int id; 3 private String name; 4 private int age; 5 。。。//省略getter方法和setter方法
6 }
回歸正題,例如我們需要在UserDao層的實現類UserDaoImpl中對User對象進行數據庫的增刪改查,這里僅展示對User對象的添加,先使用Statement對象:

1 public class UserDaoImpl { 2 public void insert(User user) throws SQLException { 3 Connection conn = null; 4 Statement st = null; 5 ResultSet rs = null; 6
7 try{ 8 conn = JdbcUtils.getConnection(); 9 st = conn.createStatement(); 10 String sql = "insert into user(id,name,age) values("+user.getId()+",'"+user.getName()+"','"+user.getAge()+"')"; //這里簡直讓人奔潰
11 int num = st.executeUpdate(sql); 12 if(num>0){ 13 System.out.println("添加成功"); 14 }else{ 15 System.out.println("添加失敗"); 16 } 17 }finally{ 18 JdbcUtils.release(conn, st, rs); 19 } 20 } 21 }
-------------------------------------我是使用PreparedStatement的分割線-------------------------------
使用PreparedStatement對上面例子的代碼進行更改:

1 public class UserDaoImpl { 2 public void insert(User user) throws SQLException { 3 Connection conn = null; 4 PreparedStatement st = null; 5 ResultSet rs = null; 6 try{ 7 conn = JdbcUtils.getConnection(); 8 String sql = "insert into user(id,name,age) values(?,?,?)"; 9 st = conn.prepareStatement(sql); 10 st.setInt(1, user.getId()); 11 st.setString(2, user.getName()); 12 st.setInt(3, user.getAge()); 13 int num = st.executeUpdate(); 14 if(num>0){ 15 System.out.println("添加成功"); 16 }else{ 17 System.out.println("添加失敗"); 18 } 19
20 }finally{ 21 JdbcUtils.release(conn, st, rs); 22 } 23 } 24 }
在上面的代碼中,使用PreparedStatement與Statement代碼不同的地方都已經用紅字標出:
⑴ 首先我們在封裝sql語句的對象應該使用PreparedStatement。
⑵ 我們在SQL語句的字符串中,以問號“?”來替代數據,相當於在國際化中的占位符,可以看到如果使用Statement的SQL語句字符串,要不斷地使用字符串連接符,整個SQL字符串看上去簡直讓人奔潰。
⑶ 使用Connection對象的prepareStatement(String sql)語句來創建PreparedStatement對象,在創建的工程中就要傳入sql語句作為參數,這點和Statement不同。
⑷ 通過PreparedStatement對象的setXXX方法來對SQL語句字符串中的占位符進行替換,根據在數據庫中的類型不同而采用不同的set方法。通過這種方式,我們可以 清晰地看清楚在SQL語句中每個占位符和一一對應在數據庫中的列數據。
⑸ 最后使用PreparedStatement的executeUpdate()方法發送給數據庫執行,注意和Statement不同,這里無需再向executeUpdate方法中傳入參數,因為在創建PreparedStatement對象時已經傳入了SQL字符串語句。如果使用查詢executeQuery方法也是無需傳入參數。
通過上面的例子我們可以看到使用PreparedStatement在編寫JDBC操作數據庫的SQL字符串語句會非常好用,也使對應的數據顯得非常清楚。
不僅如此,PreparedStatement是能極大的減輕數據庫服務器的壓力的。從PreparedStatement的名字來看是叫“預編譯”,如果要解釋為什么預編譯能優化數據庫,那么就要從數據庫說起,在操作數據庫時,我們的每一條SQL語句都要在數據庫中進行編譯並執行,這一點和程序是類似的。對於JDBC來說,使用Statement對象那么只對SQL語句進行封裝然后發送給數據庫編譯並執行,如果對數據庫操作頻繁,那么數據庫都要做編譯和執行兩個步驟;而使用PreparedStatement對象的話,則由程序對SQL語句先進行編譯,然后再發送給數據庫服務器執行,這樣就極大地減輕了數據庫服務器的壓力。
除了能給數據庫服務器帶來極大的優化作用之外,PreparedStatement另外一個極大的功能就在於能防止SQL注入攻擊。
所謂的SQL注入,就是把SQL語句輸入到WEB表單、或者輸入域名或頁面請求的查詢字符串等等,在請求發送給服務器的過程中, 達到欺騙數據庫服務器執行惡意的SQL命令。
例2:使用SQL注入進行用戶登錄
要使SQL注入能成功,必須得是Statement對象才行。
還是以例1創建user表為案例,如果我們開發好web層,做好前端頁面顯示,比如用戶登錄,就需要填寫用戶登錄的用戶名,而這里用戶名就可以用惡意的SQL語句,假設我們在該處填寫的用戶名為: ' or 1=1 or name='
在點擊提交或者登錄按鈕后,會以表單形式從web工程中層層傳下,到dao層將表單的數據進行操作數據庫,以匹配是否存在該用戶,在dao層對User對象操作的UserDaoImpl實現類中,查找用戶的代碼如下:

1 public class UserDaoImpl { 2 public User find(String name) throws SQLException { 3 Connection conn = null; 4 Statement st = null; 5 ResultSet rs = null; 6 try{ 7 conn = JdbcUtils.getConnection(); 8 String sql = "select * from user where name='" +name+ "'"; 9 st = conn.createStatement(); 10 rs = st.executeQuery(sql); 11 if(rs.next()) { 12 User user = new User(); 13 user.setId(rs.getInt("id")); 14 user.setName(rs.getString("name")); 15 user.setAge(rs.getInt("age")); 16 return user; 17 }else{ 18 return null; 19 } 20 }finally{ 21 JdbcUtils.release(conn, st, rs); 22 } 23 } 24 }
通過上述代碼,經過測試,根據頁面帶過來的表單數據name值為' or 1=1 or name='是可以進行用戶登錄的。
分析:我們若將表單填寫的數據帶到代碼中的SQL語句,就形成如下的SQL命令:
select * from user where name='' or 1=1 or name=''
可以看到使用Statement對象就是將兩個字符串拼接形成的SQL語句,這樣做很可能會將判斷條件改變,如上面的命令,在where語句中出現了or 1=1 這樣一定會返回true的語句,就如同程序發送一條“select * from user where true”的句子,那么數據庫執行這條語句根本不需要篩選條件,只要數據庫有任意用戶,都可以告訴程序你找到了該指定用戶,那么我們連密碼都不用填的只需要惡意SQL語句即可登錄網站。這就是一個SQL注入的典型例子。
而使用PreparedStatement則不會,因為PreparedStatement的預編譯,會將表單中所填寫的數據進行編譯,這種編譯是包含字符過濾的編譯,就好像對html進行過濾轉義一樣,這字符過濾最關鍵的因素在於PreparedStatement使用的是占位符,而不會像Statement那樣因為拼接字符串而引入了引號,可以看到在PreparedStatement中即使接收的表單數據中SQL語句以引號包圍,由於程序中的SQL語句使用占位符,因此就相當於條件為where name=' or 1=1 or name=',顯然數據庫並沒有這樣的記錄,因此防止了SQL注入的問題。
關於SQL注入也算是一門有趣的學問,建議平時對這方面多學習些,也有利於我們更好地加強自己開發中的安全性。