MyBatis 中 #{} 与 ${} 有什么区别?会引发 SQL 注入吗?
结论先行:一句话区别——#{} 是预编译占位符,MyBatis 会把它渲染成 PreparedStatement 的 ?,参数值由 JDBC 驱动转义,能有效防止 SQL 注入;${} 是字符串直接拼接,把值原样拼进 SQL,灵活但危险。能用 #{} 的场合绝不用 ${};只有表名、列名、ORDER BY 字段这类“SQL 结构”必须动态化时才用 ${},并且必须做白名单校验。
对比表
| 维度 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译 ? 占位 | 字符串直接拼接 |
| 安全性 | 防 SQL 注入 | 存在注入风险 |
| 使用场景 | 条件值、批量参数 | 表名/列名/排序字段/动态片段 |
| 引号处理 | 自动按类型转义 | 原样输出、不补引号 |
| 执行计划 | 同语句可复用 | 每句都是新 SQL |
正反示例
<!-- 安全写法:值用 #{},走预编译占位 -->
<select id="queryByName" resultType="User">
SELECT * FROM user
WHERE name LIKE CONCAT('%', #{kw}, '%')
</select>
<!-- 危险写法:用户输入直接拼入 SQL -->
<!-- 若 kw = x' OR '1'='1,条件恒真 → 全表数据被带出 -->
<select id="badQuery" resultType="User">
SELECT * FROM user WHERE name LIKE '%${kw}%'
</select>
何时必须用 ${} 以及怎么防护
- 动态表名(如按月分表)、动态排序字段无法用占位符表达,只能拼接;
- 防护方案:把允许值收进白名单/枚举校验后再拼,绝不直接信任外部入参。
常见追问 / 记忆点
- 追问:#{} 既然安全,为什么 MyBatis 日志里看起来是完整 SQL?答:MyBatis 先渲染出带 ? 的 SQL,参数由 PreparedStatement 单独传入,用户的“值”不会成为 SQL 文本的一部分。
- 追问:ORDER BY 的字段名能用 #{} 吗?答:不能,#{} 只会作为值传入;排序字段属于 SQL 结构,只能用 ${} 配合白名单。
- 记忆点:#{} 管“值”,${} 管“结构字面量”;默认 #{},非用 ${} 必校验。