preparedstatement能防sql注入,因其将sql模板与参数值彻底分离:预编译阶段仅解析固定结构,运行时通过setxxx()传入的参数被数据库视为纯数据而非sql代码,即使含恶意字符串如"'; drop table users; --"也仅作字面量处理;关键须全程仅对参数值使用?占位符和类型明确的set方法,表名、列名、order by等动态结构必须经白名单校验。

PreparedStatement 为什么能防 SQL 注入
因为它把 SQL 语句结构和参数彻底分开:编译阶段只解析语句模板,运行时才绑定参数值。数据库不会把 setString() 传入的值当 SQL 语法解析,哪怕里面是 "'; DROP TABLE users; --",也只会作为字符串字面量处理。
关键点在于:PreparedStatement 底层依赖数据库预编译机制(如 PostgreSQL 的 PREPARE、MySQL 的 binary protocol),不是靠字符串拼接或简单转义。
怎么写才算真正用对了 PreparedStatement
常见错误是“假预编译”——看起来用了 PreparedStatement,但实际还是拼接 SQL:
- ❌ 错误:用
String.format()或+拼接表名/列名/排序字段后再传给prepareStatement() - ✅ 正确:仅对**参数值**使用
setXxx()方法;表名、列名、ORDER BY子句等必须走白名单校验或硬编码 - ⚠️ 注意:
setString(1, userInput)安全;"SELECT * FROM " + tableName不安全,无论后面是否用PreparedStatement
示例对比:
String sql = "SELECT * FROM users WHERE name = ? AND age > ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, request.getParameter("name")); // ✅ 安全
ps.setInt(2, Integer.parseInt(request.getParameter("age"))); // ✅ 安全
哪些参数类型容易踩坑
setObject() 看似通用,但行为依赖 JDBC 驱动实现,可能绕过类型约束:
- ❌ 避免
ps.setObject(1, userInput, Types.VARCHAR)—— 某些旧版驱动会退化为字符串拼接 - ✅ 优先用明确类型方法:
setString()、setInt()、setBoolean()等 - ⚠️ 特别注意
NULL值:用setNull(1, Types.VARCHAR),而不是setString(1, null)(后者在部分驱动中行为未定义)
时间类型也常出问题:setTimestamp() 比 setString() 传 ISO 格式字符串更可靠,避免因数据库时区或格式解析导致意外结果。
连接池和 PreparedStatement 缓存的影响
很多连接池(如 HikariCP、Druid)支持 cachePrepStmts=true,会缓存已编译的 PreparedStatement 对象。这能提升性能,但要注意:
- 缓存的是「SQL 模板」,不是执行结果,所以不影响安全性
- 但如果应用动态生成 SQL 模板(比如根据条件拼接
WHERE子句),缓存可能失效或爆炸,反而降低性能 - Oracle 驱动在开启
implicitCachingEnabled时,对相同 SQL 字符串复用物理句柄,此时模板必须完全一致(空格、换行都算不同)
真正该警惕的,是以为用了 PreparedStatement 就一劳永逸——只要参数进了 SQL 拼接环节,哪怕只拼一个单引号,防注入就失效了。











