本篇讲诉为何在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注入也算是一门有趣的学问,建议平时对这方面多学习些,也有利于我们更好地加强自己开发中的安全性。