PreparedStatement 防 SQL 注入的关键在于正确使用 prepareStatement() 和 ? 占位符绑定参数,而非连接池本身;Druid 默认客户端预编译,HikariCP 不干预,MySQL 服务端预编译非默认且易致性能瓶颈。

PreparedStatement 本身不依赖连接池,但连接池必须支持预编译语义
连接池(如 Druid、HikariCP)只是管理 Connection 对象的生命周期,它不改变 SQL 执行机制。真正防 SQL 注入的是你用 Connection.prepareStatement() 创建的 PreparedStatement,而不是连接池本身。但连接池若默认关闭预编译或强制走字符串拼接(极少数旧版池),就会让 PreparedStatement 退化为普通语句。
验证方式很简单:抓包或开启 JDBC 日志,看实际发给 MySQL 的是否是 COM_STMT_PREPARE 协议指令。如果只看到 COM_QUERY + 拼接后的 SQL 字符串,说明预编译没生效。
- Druid 默认启用客户端预编译(即 JDBC 驱动层模拟),无需额外配置
- HikariCP 本身不干预预编译行为,完全由底层驱动决定
- 若使用 MySQL Connector/J,必须确认未设置
useServerPrepStmts=true且服务端未禁用prepared_statement功能(MySQL 8.0+ 默认开启)
MySQL 服务端预编译不是默认开启,且多数场景不推荐
MySQL 的服务端预编译(PREPARE / EXECUTE)需要显式开启,并受服务端参数控制。但阿里云 AnalyticDB、部分云 MySQL 实例甚至明确建议关闭服务端预编译,原因很实际:
- 服务端预编译句柄占用内存,高并发下易触发
max_prepared_stmt_count限制(默认 16382),报错SQLState: HY000, Error Code: 1461, Message: Can't create more than max_prepared_stmt_count statements - MySQL 5.7/8.0 中,服务端预编译对简单查询反而比客户端预编译慢,因多一次网络 round-trip 和服务端状态维护开销
- 服务端预编译无法复用跨连接的语句,而连接池中每个
Connection是独立会话,预编译缓存不共享
所以生产环境更常见的是:保持 useServerPrepStmts=false(MySQL Connector/J 默认值),让驱动在客户端做参数插值(即“客户端预编译仿真”),既防注入又避免服务端瓶颈。
真正起作用的是 prepareStatement() 调用时机和参数绑定方式
哪怕连接池配置再“安全”,如果你写的是:
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'"; statement.executeQuery(sql);
——那连接池和预编译开关都救不了你。防御的关键动作只有两个:
- SQL 字符串里不能拼接任何用户输入,必须用
?占位 - 必须调用
pstmt.setString(1, userInput)这类类型化 setter,而不是pstmt.setObject(1, userInput)(虽不报错,但某些驱动可能绕过类型检查)
注意:MyBatis 的 ${} 是字符串替换,等同于拼接;只有 #{} 才触发 PreparedStatement 绑定。这点常被忽略。
连接池配置中唯一影响预编译的参数是 useServerPrepStmts
这个参数只在 MySQL JDBC 驱动中有效,它控制驱动是否向 MySQL 服务端发起 COM_STMT_PREPARE 请求。但它不是“开关”,而是一个协商信号:
-
useServerPrepStmts=true:驱动尝试服务端预编译;失败则自动降级为客户端插值(不报错) -
useServerPrepStmts=false(默认):跳过服务端协商,直接在驱动内完成参数填充,生成完整 SQL 后走普通查询协议 - 不要同时设
useCursorFetch=true,它会强制覆盖useServerPrepStmts为 false
真正要检查的,是你代码里有没有漏掉 prepareStatement() 调用、有没有混用 createStatement()、有没有在日志里看到 Preparing: SELECT * FROM ... 后跟带问号的原始 SQL —— 这才是预编译生效的铁证。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











