文章目录
概要整体架构流程技术名词解释技术细节小结概要
1.什么是prepareStatement
prepareStatement是表示预编译的 SQL 语句的对象。prepareStatement方法是Java中用于准备执行SQL语句的方法。它可以避免SQL注入攻击,提高执行效率,且支持占位符,可以方便地设置参数。
2.为什么要使用prepareStatement
Statement与preparedStatement的区别:
1. preparedStatement可以写动态参数化的查询
用preparedStatement你可以写带参数的sql查询语句,通过使用相同的sql语句和不同的参数值来做查询比创建一个不同的查询语句要好
2. preparedStatement比 Statement 更快
preparedStatement用来执行SQL语句查询的时候。数据库系统会对sql语句进行预编译处理,预处理语句将被预先编译好,这条预编译的sql查询语句能在将来的查询中重用,这样一来,它比Statement对象生成的查询速度更快。
3.preparedStatement解决SQL注入逻辑漏洞
如果你是做Java web应用开发的,那么必须熟悉那声名狼藉的SQL注入式攻击。Sony就遭受了SQL注入攻击,被盗用了一些Sony play station(PS机)用户的数据。在SQL注入攻击里,恶意用户通过SQL元数据绑定输入。
4.preparedStatement提前传入SQL
这可以帮助我们提前获取sql语句,范围更广,安全性更高。
PreparedStatement preState = conn.prepareStatement(sql);
Statement statement = conn.createStatement();
尽管PreparedStatement非常实用,但是它仍有一定的限制。
为了防止SQL注入攻击,PreparedStatement不允许一个占位符(?)有多个值,在执行有IN子句查询的时候这个问题变得棘手起来。
注意:占位符索引位置从1开始,而不是从0开始。
整体架构流程
1.解决SQL注入问题
下面我们来看一个sql案例:
SELECT * FROM t_users WHERE username = 'cc' OR '1=1' AND PASSWORD = 'cc' OR '1=1'
这个SQL语句很普通,有人会认为登录不成功,但是结果显示登录成功。这样我们数据库里的数据就会被盗用,这样很不安全。让我们探究一下为什么?
SELECT * FROM t_users WHERE FALSE OR TRUE AND FALSE OR TRUE
我们很容易推出上面的sql语句,最后发现查询的居然是总表。
SELECT * FROM t_users
这样我们在代码里面来意识到sql注入问题,最后解决问题。
首先还是创建用户类:
package com.hp.pojo;public class User {private int id;private String username;private String password;@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", password='" + password + '\'' +'}';}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
通过调用之前的查询方法selectRow(),当输入账号cc,密码cc,帮我们判断是否登录成功。
public void testLogin(){String username="cc";String password="cc";User user = selectRow("SELECT * FROM t_users WHERE username = 'cc' OR '1=1' AND PASSWORD = 'cc' OR '1=1'", User.class,username,password);System.out.println(user!=null?"登陆成功":"登录失败");}
结果:
让我们看看为什么这次显示登录失败?
//查询一行public static <T> T selectRow(String sql,Class<T> c,Object ...params) {try {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/salary?characterEncoding=utf-8", "root", "root");PreparedStatement preState = conn.prepareStatement(sql);//在执行前,给sql传递参数for (int i = 0; i < params.length; i++) {preState.setObject(i+1,params[i]);}//statement执行sql,返回结果集ResultSet rs = preState.executeQuery();//结果集rs得到结果集元数据ResultSetMetaData md = rs.getMetaData();//获取结果集的总列数int columnCount = md.getColumnCount();//解析rs(循环打印)// 根据每一行的数据,封装成一个实体对象T t=null;if (rs.next()) {//读取结果集的光标向下移动,光标默认在哪一行,列名所在的那一行t = c.newInstance();//1.取出某一行的每一列数据,封装到对象t的属性中for (int i = 1; i <= columnCount; i++) {//通过列的序号,获取每一列的值Object value = rs.getObject(i);if (value!=null){//通过列的序号,获取每一列的列名String columnName = md.getColumnName(i);//因为列名和实体类t中的属性名一致,为每一个属性构造一个反射中的set方法Field f = c.getDeclaredField(columnName);//赋予私有属性的赋值权限f.setAccessible(true);//使用反射,把value给到对象t的属性中f.set(t,value);//理解为:把value赋值给对象t的columnName属性,相当于set方法}}}//关闭资源preState.close();conn.close();return t;} catch (Exception e) {e.printStackTrace();}return null;}
这里我们的方法采用了prepareStatement()方法和可变形参数组,完善了代码块,导致没有发生sql注入问题。
2.占位符不够用
这里我们用Object ...params可变形参数组,这样我们在调用函数时,可以传入任意个任意类型的参数。同时也完善prepareStatement方法的缺陷,更放下心去使用它。
下面是更新后的jdbc工具类升级版:
package com.hp.utils;import com.hp.pojo.User;import org.junit.Test;import java.lang.reflect.Field;import java.sql.*;import java.util.ArrayList;import java.util.List;//jdbc工具类 升级版/*statement 父类PreparedStatement 子类1.提前传入sql,执行的时候,不传入sql2.支持传入sql中的参数3.解决sql注入逻辑漏洞4.提高执行效率Object ...params 可变形参数组在调用函数时,可以传入任意个任意类型的参数*/public class JdbcUtilPlus {@Test//测试登录public void testLogin(){String username="aa";String password="aa";User user = selectRow("select * from t_users where username=? and password=?", User.class,username,password);System.out.println(user!=null?"登陆成功":"登录失败");}//查询多行多列public static <T> List<T> list(String sql,Class<T> c,Object ...params) {//创建一个集合,存放所有对象List<T> tList = new ArrayList<>();try {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/salary?characterEncoding=utf-8", "root", "root");PreparedStatement preState = conn.prepareStatement(sql);for (int i = 0; i < params.length; i++) {preState.setObject(i+1,params[i]);}//statement执行sql,返回结果集ResultSet rs = preState.executeQuery();//结果集rs得到结果集元数据ResultSetMetaData md = rs.getMetaData();//获取结果集的总列数int columnCount = md.getColumnCount();//解析rs(循环打印)while (rs.next()) {//读取结果集的光标向下移动,光标默认在哪一行,列名所在的那一行//根据每一行的数据,封装成一个实体对象T t = c.newInstance();//1.取出某一行的每一列数据,封装到对象t的属性中for (int i = 1; i <= columnCount; i++) {//通过列的序号,获取每一列的值Object value = rs.getObject(i);if (value!=null){//通过列的序号,获取每一列的列名String columnName = md.getColumnName(i);//因为列名和实体类t中的属性名一致,为每一个属性构造一个反射中的set方法Field f = c.getDeclaredField(columnName);//赋予私有属性的赋值权限f.setAccessible(true);//使用反射,把value给到对象t的属性中f.set(t,value);//理解为:把value赋值给对象t的columnName属性,相当于set方法}}//把对象存入集合中tList.add(t);}//关闭资源preState.close();conn.close();} catch (Exception e) {e.printStackTrace();}return tList;}//查询一行public static <T> T selectRow(String sql,Class<T> c,Object ...params) {try {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/salary?characterEncoding=utf-8", "root", "root");PreparedStatement preState = conn.prepareStatement(sql);//在执行前,给sql传递参数for (int i = 0; i < params.length; i++) {preState.setObject(i+1,params[i]);}//statement执行sql,返回结果集ResultSet rs = preState.executeQuery();//结果集rs得到结果集元数据ResultSetMetaData md = rs.getMetaData();//获取结果集的总列数int columnCount = md.getColumnCount();//解析rs(循环打印)// 根据每一行的数据,封装成一个实体对象T t=null;if (rs.next()) {//读取结果集的光标向下移动,光标默认在哪一行,列名所在的那一行t = c.newInstance();//1.取出某一行的每一列数据,封装到对象t的属性中for (int i = 1; i <= columnCount; i++) {//通过列的序号,获取每一列的值Object value = rs.getObject(i);if (value!=null){//通过列的序号,获取每一列的列名String columnName = md.getColumnName(i);//因为列名和实体类t中的属性名一致,为每一个属性构造一个反射中的set方法Field f = c.getDeclaredField(columnName);//赋予私有属性的赋值权限f.setAccessible(true);//使用反射,把value给到对象t的属性中f.set(t,value);//理解为:把value赋值给对象t的columnName属性,相当于set方法}}}//关闭资源preState.close();conn.close();return t;} catch (Exception e) {e.printStackTrace();}return null;}//查询一列public static <T> List<T> selectColumn(String sql,Class<T> c,Object ...params) {//创建一个集合,存放所有对象List<T> tList = new ArrayList<>();try {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/salary?characterEncoding=utf-8", "root", "root");PreparedStatement preState = conn.prepareStatement(sql);//在执行前,给sql传递参数for (int i = 0; i < params.length; i++) {preState.setObject(i+1,params[i]);}//statement执行sql,返回结果集ResultSet rs = preState.executeQuery();//结果集rs得到结果集元数据ResultSetMetaData md = rs.getMetaData();//获取结果集的总列数int columnCount = md.getColumnCount();//解析rs(循环打印)while (rs.next()) {//读取结果集的光标向下移动,光标默认在哪一行,列名所在的那一行//通过列的序号,获取每一列的值T t = (T) rs.getObject(1);//把对象存入集合中tList.add(t);}//关闭资源preState.close();conn.close();} catch (Exception e) {e.printStackTrace();}return tList;}//查询单个public static <T> T selectOne(String sql,Class<T> c,Object ...params) {try {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/salary?characterEncoding=utf-8", "root", "root");PreparedStatement preState = conn.prepareStatement(sql);//在执行前,给sql传递参数for (int i = 0; i < params.length; i++) {preState.setObject(i+1,params[i]);}//statement执行sql,返回结果集ResultSet rs = preState.executeQuery(sql);//结果集rs得到结果集元数据ResultSetMetaData md = rs.getMetaData();//获取结果集的总列数int columnCount = md.getColumnCount();//解析rs(循环打印)T t=null;if (rs.next()){t = (T) rs.getObject(1);}//关闭资源preState.close();conn.close();return t;} catch (Exception e) {e.printStackTrace();}return null;}//增 删 改public static int update(String sql,Object ...params) {try {Class.forName("com.mysql.jdbc.Driver");Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/salary?characterEncoding=utf-8", "root", "root");PreparedStatement preState = conn.prepareStatement(sql);//在执行前,给sql传递参数for (int i = 0; i < params.length; i++) {preState.setObject(i+1,params[i]);}//statement执行sql,返回结果集int i = preState.executeUpdate(sql);//关闭资源preState.close();conn.close();return i;} catch (Exception e) {e.printStackTrace();}return 0;}}
技术名词解释
可变形参个数
1.格式,对于方法的形参,数据类型...形参名
2.可变个数的形参方法于同名的方法构成重载
3.可变参数在调用时从0开始,到无穷都可以
4.使用可变多个形参的方法与方法的形参使用数组是一致的
5.方法中存在可变个数的形参,一定 声明在方法形参的最后
6.在一个方法中最多只能有一个可变形参的方法
技术细节
给大家留个小问题,为什么要在后端大费周章的去实现增删改查的功能,而不是直接在前端实现,明天我们一起探究tomcat的神奇之处,一起配置部署tomcat和实现表格数据的增删改的操作吧。
完整代码:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><script src="/vue/vue.min.js"></script><script src="/vue/axios.min.js"></script></head><body><div id="app"><button @click="showDept">点击显示数据</button><table border="1" cellpadding="20" cellspacing="0"><tr><th>序号</th><th>部门名称</th><th>部门位置</th><th>部门领导</th></tr><tr v-for="dept in deptList"><td>{{dept.did}}</td><td>{{dept.dname}}</td><td>{{dept.dlocation}}</td><td>{{dept.leader}}</td></tr></table></div></body><script>new Vue({el:"#app",data:{deptList:[]},methods:{showDept(){axios.get('http://localhost:8080/deptC').then((resp)=>{console.log(resp,resp.data)this.deptList=resp.data})}}})</script></html>
效果图:
小结
本节小课堂我们发掘问题,意识到sql注入问题的严重性,用子类功能更强大的PreparedStatement替换父类Statement解决了问题,同时我们知道了它的功能强大之处,也了解了它的缺陷,通过可变形参数组传入任意多个参数,保证正确调用函数。最后留了课堂作业给大家,前后端分离,前端负责页面各种效果,后端与数据库相连接,保证数据的安全性。这样使代码更加简洁,逻辑性更强。