必须用 ${} 时是 JDBC 不支持参数化的场景,如动态表名、列名、ORDER BY、GROUP BY、LIMIT;因 PreparedStatement 无法处理这些语法片段,而 #{} 通过预编译防注入,${} 需白名单校验且影响执行计划缓存。

什么时候必须用 ${}?
只有在需要把参数当 SQL 语法片段用时才用 ${},比如动态表名、列名、ORDER BY 子句、GROUP BY 字段或 LIMIT 偏移量——这些地方根本不能塞进 PreparedStatement 的 ? 占位符里。
-
ORDER BY ${sortField} ${sortOrder}:字段名和asc/desc是关键字,不是值,#{}会加引号变成ORDER BY 'name' 'desc',直接报错 -
SELECT * FROM ${tableName}:表名无法参数化,JDBC 不允许SELECT * FROM ? -
LIMIT ${offset}, ${limit}:数值位置固定但需拼接,#{offset}会被当字符串处理,导致语法错误
用 ${} 不是图省事,是 JDBC 层面的硬性限制。
#{} 为什么能防 SQL 注入?
因为 MyBatis 把它交给 PreparedStatement 处理:SQL 模板先编译,参数后绑定,数据库根本不把参数当 SQL 解析。
- 输入
username = "admin' -- ",#{username}最终执行的是:SELECT * FROM users WHERE username = ?,然后ps.setString(1, "admin' -- ") - 而
${username}是直接字符串替换:SELECT * FROM users WHERE username = 'admin' -- ',注释掉后续条件 - 哪怕传入
' OR 1=1 OR ',#{}也只会查一个带单引号的用户名,不会改变语句结构
安全不是靠 MyBatis “过滤”,而是靠 PreparedStatement 的协议隔离。
用 ${} 时怎么避免翻车?
只要用户能控制 ${} 的值,就等于把 SQL 构造权交出去了。必须做白名单校验,不能只靠前端限制或简单 trim。
- 排序方向只允许
asc/desc:用枚举或if判断,非法值抛异常,不 fallback - 表名/列名来自配置或枚举常量,绝不用
request.getParameter("table")直接拼 - 避免组合式拼接:
WHERE ${col} = ${val}是双重风险,应拆成WHERE ${col} = #{val}(列名白名单 + 值参数化) - MyBatis 动态 SQL 中慎用
${}:比如<if test="sortField != null">ORDER BY ${sortField}</if>,必须确保sortField已经被服务端校验过
没校验的 ${} 就是开着门的数据库。
性能和缓存差异真有那么大?
有,而且影响真实线上的执行计划复用率。
-
#{}生成的 SQL 固定(如SELECT * FROM user WHERE id = ?),数据库可缓存执行计划,1000 次查询走同一计划 -
${}每次都生成新 SQL(SELECT * FROM user WHERE id = 1、SELECT * FROM user WHERE id = 2…),MySQL 要反复解析、优化、生成新计划,高并发下容易触发Query Cache失效或plan cache bloat - 某些分库分表中间件(如 ShardingSphere)依赖 SQL 结构做路由,
${}导致路由规则失效
不是“慢一点”,是可能让数据库在高峰期多花 30% CPU 在 SQL 解析上。
最常被忽略的一点:${} 看似灵活,但一旦混入未校验的业务参数,修复成本远高于提前用 #{} + 动态 SQL 重构。别等审计报告出来再补白名单。










