preparedstatement防sql注入的核心是分离sql结构与用户数据:全程用?占位、禁止字符串拼接、参数须严格类型匹配、动态结构需白名单校验、批量操作复用同一语句。

Java 中用 PreparedStatement 防 SQL 注入,核心就一条:让 SQL 结构和用户数据彻底分开。数据库只按你写死的模板执行,参数只是“填空”,不参与语法解析。
必须全程用 ? 占位,禁止任何字符串拼接
占位符 ? 只能代表值,不能代表表名、列名或 ORDER BY 后的 ASC/DESC。只要你在 SQL 字符串里拼了变量,哪怕只拼一个,整个预编译就失效了。
- ✅ 正确写法:"SELECT * FROM user WHERE status = ? AND created_at > ?" → 后续用
setInt(1, 1)、setTimestamp(2, ts) - ❌ 错误写法:"SELECT * FROM " + tableName + " WHERE id = ?" → 表名拼接,完全绕过防护
- ⚠️ 常见陷阱:动态 IN 列表不能写成
WHERE id IN (?),因为 ? 只能对应单个值;需按实际个数生成多个 ?,如IN (?, ?, ?),再循环调用setLong()
参数必须全走 setXxx(),且类型严格匹配
setString() 不是“随便设字符串”,它告诉数据库这个参数该按 VARCHAR 处理;用错方法可能存错、查不到,甚至触发隐式转换导致性能问题。
- 查 INT 字段(如 id),就用
setInt()或setLong(),别用setString("123") - 时间字段是 TIMESTAMP,就用
setTimestamp(),传字符串会失败或存成 1970-01-01 - 传 null 值必须用
setNull(index, Types.INTEGER),不能对setInt()传 null,否则抛 NullPointerException
动态结构(如排序、条件)要用白名单校验
ORDER BY 后的字段、WHERE 中可选的搜索条件、GROUP BY 的列……这些不是“值”,不能用 ?,但也不能放任拼接。必须限定在可信范围内。
- 例如允许排序字段仅限
"name"、"created_at"、"status"—— 先判断输入是否在白名单里,再拼进 SQL - 多条件搜索(姓名/邮箱/手机号可选填):用 StringBuilder 拼 WHERE 子句骨架,只对确定的值用 ? 占位,避免写一堆 if-else 拼完整 SQL
- 表名、列名绝对不可来自用户输入,除非经过严格白名单比对,否则直接拒绝请求
批量操作和边界情况要特别注意
addBatch() 不是“把 SQL 拼好再塞进去”,而是多次绑定 + 一次提交。另外,驱动和数据库对参数数量、时间类型处理有差异,容易忽略。
- 批量插入 100 条记录:复用同一个 PreparedStatement,循环调用
setXxx()→addBatch()→ 最后executeBatch() - MySQL 驱动默认按 UTC 解析 java.util.Date,若应用时区和数据库时区不一致,
setTimestamp()可能偏差数小时——建议统一用Instant+setObject(i, instant, Types.TIMESTAMP) - SQL 模板中空格、换行、大小写不一致,会导致数据库无法复用执行计划,失去预编译性能优势
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











