preparedstatement 防 sql 注入的关键在于sql结构完全静态且所有参数均通过setxxx()绑定;表名、列名、排序、分页等语法结构不可用?占位,须白名单校验后拼接;类型需严格匹配,空值用setnull(),in子句限数量并addbatch();应复用实例以利用执行计划缓存。

PreparedStatement 防 SQL 注入,核心不是“用了没”,而是“参数是否全部走 setXxx() 绑定、SQL 结构是否完全静态”。数据库在预编译阶段就固定语法树,后续传入的值仅作为二进制数据注入,不参与任何 SQL 解析——哪怕传入 "admin' OR '1'='1",它也只会被当做一个完整字符串去匹配,不会改变查询逻辑。
所有用户输入必须走 setXxx(),不能拼接
这是最根本的防线。只要 SQL 字符串里出现 +、String.format()、replaceAll() 或任何变量插值,预编译即失效。
- ✅ 正确:
"SELECT * FROM user WHERE name = ? AND age > ?"→pstmt.setString(1, name)+pstmt.setInt(2, age) - ❌ 错误:
"SELECT * FROM " + tableName + " WHERE id = ?"—— 表名拼接,直接绕过防护 - ❌ 错误:
"WHERE status IN ('" + String.join("','", ids) + "')"—— 动态生成 IN 列表,高危
类型必须严格匹配,空值要显式声明
setXxx() 不只是赋值,更是向驱动声明 JDBC 类型。类型错可能引发隐式转换、全表扫描,甚至解析失败。
- 数字字段用
setInt()、setLong(),不用setString()强转 - 时间字段优先用
setTimestamp()或setObject(..., LocalDateTime.class) - 空值必须调用
setNull(index, Types.VARCHAR)等带类型的重载方法,不能向setInt()传null
结构化内容必须白名单校验,不能用 ? 占位
? 占位符只支持“值”,不支持表名、列名、ORDER BY 字段、ASC/DESC、LIMIT 参数等语法结构。这些地方一旦拼接用户输入,预编译立即归零。
- 排序字段:用
if ("name".equals(sortField)) sql += " ORDER BY name",而非"ORDER BY ?" - 分页参数:offset 和 limit 必须校验为非负整数,再拼进 SQL(如
" LIMIT " + limit + " OFFSET " + offset),但前提是已做严格校验 - IN 子句:不能靠字符串拼
"?, ?, ?";应限制最大数量(如 ≤ 100),再循环调用addBatch()
复用 PreparedStatement 实例,避免对象泄漏
每次 new PreparedStatement,等于放弃数据库端执行计划缓存 + 客户端对象复用双重优势。高频操作下还可能导致 Statement 泄漏、GC 压力上升。
- 循环批量插入时:创建一次 PreparedStatement,反复
setXxx()→addBatch()→executeBatch() - 常用查询可封装为工具类成员变量(注意线程安全,建议配合 ThreadLocal 或连接池 Scoped 实例)
- 不要在方法内临时创建、用完就 close,尤其在连接池环境中
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











