PreparedStatement是用来执行SQL查询语句的API之一,java提供了 Statement、PreparedStatement 和 CallableStatement三种方式来执行查询语句,其中 Statement 用于通用查询, PreparedStatement 用于执行参数化查询,而 CallableStatement则是用于存储过程。同时PreparedStatement还经常会在Java面试被提及,譬如:Statement与PreparedStatement的区别以及如何避免SQL注入式攻击?这篇教程中我们会讨论为什么要用PreparedStatement?使用PreparedStatement有什么样的优势?PreparedStatement又是如何避免SQL注入攻击的?% ~; I9 Z* g' N/ Q. Y+ C
- m2 d' z: e* P
PreparedStatement是什么? ; c* M( N; i6 y, X 1 l6 G, O l; c6 F7 a' j& K2 DPreparedStatement是java.sql包下面的一个接口,用来执行SQL语句查询,通过调用connection.preparedStatement(sql)方法可以获得PreparedStatment对象。数据库系统会对sql语句进行预编译处理(如果JDBC驱动支持的话),预处理语句将被预先编译好,这条预编译的sql查询语句能在将来的查询中重用,这样一来,它比Statement对象生成的查询速度更快。下面是一个例子:0 C6 j& B: i) B5 Q& F8 }) P- T
$ w4 E5 m" w+ k! W) E: `, A
3 @: E1 X& `4 s% L6 lpublic class PreparedStmtExample { 6 q k# J2 {9 x. f3 _$ u3 { $ S$ _8 G @0 M6 P9 z public static void main(String args[]) throws SQLException { ! O' A; B/ w: ?! U- | Connection conn = DriverManager.getConnection("mysql:\\localhost:1520", "root", "root");/ Q: W4 F/ I& k J/ n
PreparedStatement preStatement = conn.prepareStatement("select distinct loan_type from loan where bank=?");$ R7 M# r0 v" J1 u, X& T8 z
preStatement.setString(1, "Citibank");8 S' _' A0 h/ D8 l) u
$ |1 f/ u$ Y# i y5 K0 C7 B6 p( _
ResultSet result = preStatement.executeQuery();/ P X1 S8 C \' S4 Q7 u2 m
" m A2 j4 S Y' _0 ~* b0 z! a) u
while(result.next()){, i/ d8 ~- N. ^( Q& S4 \
System.out.println("Loan Type: " + result.getString("loan_type"));2 u. \* M* R* S+ R% g# Y, {
} 1 _8 e% G5 Q/ b }& `% u( e& h) o8 }( w
} / x% l' L0 y0 }* V G+ c3 f
Output: y9 n8 G, Z" \% Z( l) P
Loan Type: Personal Loan * a0 L' W6 v5 f; \Loan Type: Auto Loan+ F0 Y! e6 p0 d2 ~: O, I4 m
Loan Type: Home Loan 3 q0 k! f$ Q, O9 FLoan Type: Gold Loan7 J: f2 W" T) [! l* ?/ Q' h
这个例子中,如果还是用 PreparedStatement 做同样的查询,哪怕参数值不一样,比如:”Standard Chated” 或者”HSBC”作为参数值,数据库系统还是会去调用之前编译器编译好的执行语句(系统库系统初次会对查询语句做最大的性能优化)。默认会返回”TYPE_FORWARD_ONLY”类型的结果集( ResultSet ),当然你也可以使用preparedstatment()的重载方法返回不同类型的结果集。 t9 Y% s n, d) R' l6 i
* T+ ^1 k6 z: a' Q" M8 A预处理语句的优势- _! J+ p/ z" G5 B, W" B
2 q( _, u4 n* ]# K5 z
PreparedStatement提供了诸多好处,企业级应用开发中强烈推荐使用PreparedStatement来做SQL查询,下面列出PreparedStatement的几点优势。 3 [5 c5 X2 M# L2 @$ a & L. q! p1 @8 I9 k$ E$ H' \PreparedStatement可以写动态参数化的查询 $ ~% }- T ~! ?8 _9 w. K% Y用PreparedStatement你可以写带参数的sql查询语句,通过使用相同的sql语句和不同的参数值来做查询比创建一个不同的查询语句要好,下面是一个参数化查询: 2 Y& t' x W, u! ]13 B! @. e) b1 T% F/ z# D
SELECT interest_rate FROM loan WHERE loan_type=?2 E8 A8 d1 O* h! h0 w! I1 N& e
现在你可以使用任何一种loan类型如:”personal loan”,”home loan” 或者”gold loan”来查询,这个例子叫做参数化查询,因为它可以用不同的参数调用它,这里的”?”就是参数的占位符。6 P9 ^, R9 x1 }" ^7 x
- I' E& S. T; \PreparedStatement比 Statement 更快 3 k/ Q" V( N6 R- O使用 PreparedStatement 最重要的一点好处是它拥有更佳的性能优势,SQL语句会预编译在数据库系统中。执行计划同样会被缓存起来,它允许数据库做参数化查询。使用预处理语句比普通的查询更快,因为它做的工作更少(数据库对SQL语句的分析,编译,优化已经在第一次查询前完成了)。为了减少数据库的负载,生产环境中德JDBC代码你应该总是使用PreparedStatement 。值得注意的一点是:为了获得性能上的优势,应该使用参数化sql查询而不是字符串追加的方式。下面两个SELECT 查询,第一个SELECT查询就没有任何性能优势。 / N" D* e7 F. c. i% A: n' w9 eSQL Query 1:字符串追加形式的PreparedStatement 1 y" h m. T+ q3 f4 s3 v3 s1 ; ^% Y: b1 i- Z: v2 F" D$ t2 Y' _! a. g6 @String loanType = getLoanType();' V6 @+ S5 q$ H. o
PreparedStatement prestmt = conn.prepareStatement("select banks from loan where loan_type=" + loanType);8 e+ x( }/ L5 P G' `' G3 |
SQL Query 2:使用参数化查询的PreparedStatement 6 V) P: _" {3 t4 t9 @$ h* r3 h" y4 s
1 5 V7 r1 f" u3 [% b" j. x+ x8 M) Q$ F2 3 @9 X. U% K2 T3 E6 z/ [PreparedStatement prestmt = conn.prepareStatement("select banks from loan where loan_type=?"); : n: V1 Z/ a! _& K% O0 \7 Tprestmt.setString(1,loanType); $ \6 o/ p7 w) W/ J D: D+ o9 J第二个查询就是正确使用PreparedStatement的查询,它比SQL1能获得更好的性能。 e( n5 h/ t* C. J% V
0 |0 @% C4 P1 |: G" I) h
PreparedStatement可以防止SQL注入式攻击) d ?* ?9 l d4 O
如果你是做Java web应用开发的,那么必须熟悉那声名狼藉的SQL注入式攻击。去年Sony就遭受了SQL注入攻击,被盗用了一些Sony play station(PS机)用户的数据。在SQL注入攻击里,恶意用户通过SQL元数据绑定输入,比如:某个网站的登录验证SQL查询代码为: % \' F8 g+ s% K0 u& [/ HstrSQL = "SELECT * FROM users WHERE name = '" + userName + "' and pw = '"+ passWord +"';"8 N8 p) B1 q) v# J, C, K9 |
恶意填入: 7 V/ _2 g+ `7 g& |6 i4 N+ Q7 y) _- u3 j. Y9 b) g' {
userName = "1' OR '1'='1";- t0 u" k* Y2 n. s- k; A6 `
passWord = "1' OR '1'='1";8 p4 t, q; j' r* N+ C
那么最终SQL语句变成了:3 R* d( e5 e9 B! h4 ?3 b5 p9 P
$ [3 f7 l) S* B' L% L6 U7 S( S
strSQL = "SELECT * FROM users WHERE name = '1' OR '1'='1' and pw = '1' OR '1'='1';" 0 M7 d2 Z8 i5 ^: @1 ^' b% X: p3 i/ Z( g因为WHERE条件恒为真,这就相当于执行: ! @; D1 F/ |" ?, @ # ~" b- Y7 x2 W5 m0 B3 `- IstrSQL = "SELECT * FROM users;" % I/ y0 j+ U% k- a" _因此可以达到无账号密码亦可登录网站。如果恶意用户要是更坏一点,用户填入: 1 G" S1 M6 O" N- z7 p 7 F) }& U- m, m u* }strSQL = "SELECT * FROM users;": e* V- A |5 l9 n3 w
SQL语句变成了: + T4 G: [7 Q# z8 } 1 D9 a5 b& [- e1 AstrSQL = "SELECT * FROM users WHERE name = 'any_value' and pw = ''; DROP TABLE users" 8 F/ Q( p" r {* ~这样一来,虽然没有登录,但是数据表都被删除了。# r1 q5 R1 W& ^& n
; r3 t) |$ Z/ K; J3 i' @* L+ H然而使用PreparedStatement的参数化的查询可以阻止大部分的SQL注入。在使用参数化查询的情况下,数据库系统(eg:MySQL)不会将参数的内容视为SQL指令的一部分来处理,而是在数据库完成SQL指令的编译后,才套用参数运行,因此就算参数中含有破坏性的指令,也不会被数据库所运行。 & b& D( f: i3 s( D$ E; F( A. Q补充:避免SQL注入的第二种方式: 6 B' m8 U2 T B) I% K在组合SQL字符串的时候,先对所传入的参数做字符取代(将单引号字符取代为连续2个单引号字符,因为连续2个单引号字符在SQL数据库中会视为字符中的一个单引号字符,譬如:% k0 n. ^0 X: N' \$ G
9 V6 y; I" d5 H" f0 i% @0 }strSQL = "SELECT * FROM users WHERE name = '" + userName + "';" G0 m& ~& Z* g* v/ _传入字符串: ' M' ?, c0 m' k$ u. c 3 ~; U B$ _3 x1 a& `userName = " 1' OR 1=1 ": o, v$ y1 @# p0 Q
把userName做字符替换后变成: - W; m: ]/ B% f, S" i' V; b" w, k' w6 ^! M
userName = " 1'' OR 1=1"0 i6 b) N: N0 a4 S# m8 v/ Y
最后生成的SQL查询语句为:' L2 x& O& z" L- }) S s
, z. l( t5 s% f
strSQL = "SELECT * FROM users WHERE name = '1'' OR 1=1'+ b4 W9 G; M9 w' r# t
这样数据库就会去系统查找name为“1′ ‘ OR 1=1”的记录,而避免了SQL注入。 & }$ _& `' b0 I+ j4 {6 I& ?: S+ m6 p- {$ L) Y. C2 s: z5 J
比起凌乱的字符串追加似的查询,PreparedStatement查询可读性更好、更安全。* F2 j4 B/ A0 Y
PreparedStatement的局限性# | l$ `7 i' a2 N, F
& m6 B* o, j3 z; P' A+ F尽管PreparedStatement非常实用,但是它仍有一定的限制。1 A7 `% R4 s' G
1. 为了防止SQL注入攻击,PreparedStatement不允许一个占位符(?)有多个值,在执行有**IN**子句查询的时候这个问题变得棘手起来。下面这个SQL查询使用PreparedStatement就不会返回任何结果2 D5 o2 J8 I \9 b5 } \
, v0 a( Q" V: V* V/ f8 p$ b
SELECT * FROM loan WHERE loan_type IN (?) * e* T. L1 k* F, h! ?; R- N: qpreparedSatement.setString(1, "'personal loan', 'home loan', 'gold loan'");2 t* x* J1 j6 U" C+ t
那如何解决这个问题呢?请你继续关注本博客,下期告诉你答案。 & a Y/ p, G9 D' d4 a" ^ j* M( N6 {( i& i% F& L. _$ g' F
不算总结的总结8 G8 C) Y8 |5 }2 T) o3 r) i8 u
5 _! F2 @# e% F! X关于PreparedStatement接口,需要重点记住的是: # w* X3 T7 J$ U, T) j" q1. PreparedStatement可以写参数化查询,比Statement能获得更好的性能。; y) U; Y u7 q' Q: C
2. 对于PreparedStatement来说,数据库可以使用已经编译过及定义好的执行计划,这种预处理语句查询比普通的查询运行速度更快。 4 e2 A2 Z. v' d. w9 q3. PreparedStatement可以阻止常见的SQL注入式攻击。; I7 Q3 i3 G' y
4. PreparedStatement可以写动态查询语句' ?6 _+ ~6 H3 z& a: B9 P/ t
5. PreparedStatement与java.sql.Connection对象是关联的,一旦你关闭了connection,PreparedStatement也没法使用了。* s& e8 n0 `+ [( j, C. r+ e- q
6. “?” 叫做占位符。7 U% J( I$ |& O6 \ b
7. PreparedStatement查询默认返回FORWARD_ONLY的ResultSet,你只能往一个方向移动结果集的游标。当然你还可以设定为其他类型的值如:”CONCUR_READ_ONLY”。 6 V; w2 ?; u. [6 Z8. 不支持预编译SQL查询的JDBC驱动,在调用connection.prepareStatement(sql)的时候,它不会把SQL查询语句发送给数据库做预处理,而是等到执行查询动作的时候(调用executeQuery()方法时)才把查询语句发送个数据库,这种情况和使用Statement是一样的。 + W7 Z! O, N: c8 x [" j& n E9. 占位符的索引位置从1开始而不是0,如果填入0会导致*java.sql.SQLException invalid column index*异常。所以如果PreparedStatement有两个占位符,那么第一个参数的索引时1,第二个参数的索引是2. 6 K) K: l5 \" f- {( E) _/ C ) z# O6 A( }4 y2 ?% s' P以上就是为什么要使用PreparedStatement的全部理由,不过你仍然可以使用Statement对象用来做做测试。但是在生产环境下你一定要考虑使用 PreparedStatement 。 h. g' ?; V6 F Z' ~7 m! J+ k