昨天用Statement跑通了查询,但网课今天一上来就讲SQL注入,看完演示之后后背有点凉------原来Statement拼接字符串的方式,在真实项目里就是颗定时炸弹。正好今天的内容就是怎么拆掉这颗炸弹,顺便把昨天那些繁琐的样板代码也一并收拾干净。
一、SQL注入:Statement的"原罪"
网课里演示了一个经典的登录绕过:如果在用户名框里输入 ' or '1'='1,再用Statement直接拼进SQL字符串,整条语句的逻辑就被篡改了,数据库直接放行。这种攻击不是数据库的bug,是代码把用户输入和SQL命令混在一起 造成的。 PreparedStatement解决这个问题的思路特别朴素:SQL语句里先挖好坑(用?占位),命令结构先定死,用户输入的数据只能往坑里填,永远没有机会接触到SQL的骨架。今天亲自试了一下,同样的注入字符串传进去,数据库把它当成普通文本处理,攻击直接失效。 在此之前我虽然听说过SQL注入攻击,但我从没想到竟然会如此的"朴实无华",原来黑客攻击离我们这么近。
二、?占位符与setObject
PreparedStatement的写法跟昨天差别不大,核心变化就两步。
第一步,SQL里不写具体值,全部换成?:
java
String sql = "INSERT INTO product(name, price, origin, stock) VALUES(?, ?, ?, ?)";
第二步,用setXxx()或者setObject()往坑里填数据:
java
pstmt.setObject(1, "香蕉");
pstmt.setObject(2, 3.5);
setObject今天用起来特别顺手,因为它不用管具体类型,Integer、Double、String统统往里塞,JDBC自己会去适配。这比昨天getInt、getString那种"对号入座"的写法省心多了。不过索引从1开始这个老规矩还在,写循环的时候得注意i + 1的偏移。
三、配置文件外置:终于不用改代码了
昨天URL、用户名、密码全部硬编码在Java文件里,每次环境变了都要重新编译,特别笨。今天把连接信息全部抽到了jdbc.properties里:
properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/farm?...
user=root
password=...
然后在JDBCUtils里用类加载器读取这个文件:
java
InputStream is = JDBCUtils.class.getClassLoader()
.getResourceAsStream("jdbc.properties");
这个写法一开始有点绕,ClassLoader和getResourceAsStream的关系我盯着看了好几遍才理解:它是在编译后的classpath根目录下去找那个配置文件。好处是打包成jar之后,配置依然能读出来,而且改数据库密码只需要动properties文件,不需要碰Java代码。 其实做这里的时候我还是感到有些困难,因为我忘记了之前的文件读取怎么写,又回去复习了IO流才完整写下来。
四、JDBCUtils:静态代码块与工具类封装
JDBCUtils这个类今天写了好几遍才顺。它的核心设计是:类一加载,配置就读完,驱动就注册好 ,后面谁要连接,直接调getConnection()就行。
java
static {
InputStream is = JDBCUtils.class.getClassLoader()
.getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(is);
url = prop.getProperty("url");
// ...
Class.forName(driver);
}
静态代码块只执行一次,这意味着驱动注册的开销被省掉了,后面每次获取连接都是纯"拿连接"的动作。close()方法还做了重载:两个参数的版本内部调用三个参数的版本,如果ResultSet是null就直接跳过。这种"兜底式"关闭比昨天手动写三行close()安全多了,至少不会因为某个对象是null而炸掉。
五、一行代码搞定增删改
DBUtil里的update方法是今天最爽的部分。它把连接、预编译、填参数、执行、关闭这五个步骤全部包进一个方法里:
java
public static int update(String sql, Object... params) {
// ...
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
return pstmt.executeUpdate();
}
Object... params这里我用这个是看网课里面说这个可以不用判断数据类型感觉用起来更灵活
java
DBUtil.update("INSERT ... VALUES(?, ?, ?, ?)", "香蕉", 3.5, "广西", 50);
DBUtil.update("UPDATE product SET price = ? WHERE id = ?", 9.9, 1);
DBUtil.update("DELETE FROM product WHERE id = ?", 3);
插入、修改、删除,三行调用就搞定了。跟昨天那段又臭又长的六步流程一比,简直像换了一门语言。
六、查询:ResultSet终于变成了Product对象
增删改可以封装成通用方法,但查询不行------因为每张表查出来的列不一样,没法统一。今天的做法是把查询结果逐列取出来,塞进一个Product对象里:
java
Product p = new Product();
p.setId(rs.getInt("id"));
p.setName(rs.getString("name"));
// ...
System.out.println(p); // 自动调用toString()
Product类今天补全了标准的JavaBean结构:私有属性、无参构造、有参构造、getter/setter、toString()。虽然getter/setter写起来有点烦,但IDEA能一键生成,而且toString()让打印结果从一堆零散字段变成了整齐的对象描述,调试的时候舒服多了。
七、今天最大的感受
如果说昨天是"跑通了JDBC",那今天就是"把JDBC收拾得能见人"了。从Statement到PreparedStatement,从硬编码到配置文件,从六步样板代码到DBUtil.update一行调用,代码里属于" plumbing(管道工)"的部分被越压越薄,属于业务逻辑的部分越来越清晰。
不过我也清楚,这还不是终点。真正的项目里可能还有连接池、事务管理、ORM框架这些更上层的东西。但至少现在,我敢说自己能独立写一个不硬编码、防注入、有工具类支撑的JDBC程序了。