正确使用 PreparedStatement 防 SQL 注入的关键是所有用户输入必须通过 setXxx() 绑定,确保 SQL 结构与数据分离;表名、列名等结构化内容须白名单校验,禁止字符串拼接,且需复用实例以发挥性能优势。

用对 PreparedStatement 才能真正防住 SQL 注入,也才能发挥它的性能优势。关键不在“用了没”,而在“怎么用”。
所有用户输入必须走 setXxx() 绑定
PreparedStatement 防注入的原理是:SQL 结构和数据彻底分离。数据库先解析固定结构的语句,再把参数值通过二进制通道传入——值永远被当数据处理,不会参与语法解析。
- 正确写法:conn.prepareStatement("SELECT * FROM user WHERE name = ? AND status = ?"),再调用 pstmt.setString(1, userInputName) 和 pstmt.setInt(2, status)
- 错误写法:"SELECT * FROM " + tableName + " WHERE id = ?" —— 表名不能用 ? 占位,拼接即失效
- 数字、时间、布尔等类型也不能例外,必须用 setInt()、setTimestamp()、setBoolean(),避免隐式转换漏洞
禁止在 SQL 字符串中拼接任何用户可控内容
哪怕只加一个单引号、一个空格、一个字段名,都会让预编译形同虚设。
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- 动态表名、列名、ORDER BY 字段、IN 子句占位符数量——这些都属于 SQL 结构,不能由用户决定;必须用白名单校验或枚举控制(如 if ("name".equals(sortField)) orderSql = "ORDER BY name")
- 不要用 String.format()、+ 连接、replaceAll() 等方式构造 SQL 字符串后再传给 prepareStatement()
- 批量 IN 查询别靠字符串拼出 "? , ? , ?",应限制最大长度(如最多 100 个),再用循环 addBatch()
复用 PreparedStatement 实例才能提升效率
预编译的优势分两层:数据库端缓存执行计划 + 客户端复用对象。如果每次查询都 new 一个 PreparedStatement,等于只用了“半截功能”。
立即学习“Java免费学习笔记(深入)”;
- 在循环插入/更新时,创建一次 PreparedStatement,反复调用 setXxx() → addBatch() → executeBatch()
- 避免在方法内临时创建、用完就丢;可考虑将常用语句封装为工具类成员变量(注意线程安全)
- 连接池环境下,频繁新建 PreparedStatement 可能导致 Statement 泄漏,增加 GC 压力
配合其他措施形成完整防护
PreparedStatement 是核心防线,但不是唯一防线。
- 对非参数位置(如表名、排序字段、分页参数)做严格白名单校验
- 密码等敏感字段不走明文比对,应使用 BCrypt 加盐哈希验证
- 开启数据库的预编译开关(如 MySQL 的 useServerPrepStmts=true 和 cachePrepStmts=true)
- ORM 框架(如 MyBatis、JPA)也要确认其生成 SQL 是否参数化,警惕 ${} 与 #{} 混用

















