MyBatis 中 #{} 和 ${} 有什么区别?
简化版
#{} 会生成 JDBC ? 占位符,由 PreparedStatement 和 TypeHandler 安全绑定值;${} 是把文本直接拼进 SQL,不做参数化或转义。普通业务值一律优先 #{},表名、列名、排序方向等必须动态拼接时只能使用受控白名单,绝不能直接接收用户输入。
详细版
WHERE id = #{id} 生成 WHERE id = ?,SQL 结构与参数值分离,可复用执行计划并显著降低注入风险。ORDER BY ${column} 会先把 column 文本替换进 SQL,攻击者可注入额外表达式。
占位符不能替代表名、列名和 SQL 关键字,因为数据库不会把 ORDER BY ? 的参数值当列标识符解析。正确做法是在 Java 或 <choose> 中把外部枚举映射为固定 SQL 片段;模糊查询用参数值拼好 % 或数据库拼接函数,IN 列表用 <foreach>。
完整版教学
一、两者生成 SQL 的时机不同
<select id="find" resultType="User">
SELECT * FROM users WHERE name = #{name}
</select>
最终发送给 JDBC 的 SQL 类似 name = ?,参数单独绑定。若改成 ${name},值会直接进入 SQL 文本,字符串引号也要自己处理,并暴露注入风险。
| 写法 | 生成方式 | 是否参数化 | 适合内容 |
|---|---|---|---|
#{name} | 生成 ?,通过 PreparedStatement 绑定 | 是 | 普通值:字符串、数字、时间、枚举值 |
${column} | 文本替换进 SQL | 否 | 受控白名单中的表名、列名、排序方向 |
<foreach> #{id} | 多个 ? + 多个参数映射 | 是 | IN 列表、批量 values |
记忆钩子:
#{}是“值通道”,${}是“结构通道”。值可以参数化,表名列名这种 SQL 结构只能白名单拼接。
二、为什么参数化更安全
PreparedStatement 把 SQL 结构和数据分开,输入中的引号、注释符等只作为值处理。MyBatis 再根据 Java 类型和 jdbcType 选择 TypeHandler 调用合适的 set 方法。
#{} 不是对字符串做简单转义,而是使用 JDBC 参数绑定;这也是它比手工拼接可靠的根本原因。
输入 name = "' OR 1=1 --"
使用 #{}:
SQL: SELECT * FROM users WHERE name = ?
参数: "' OR 1=1 --"
含义: 查 name 等于这段字符串的用户
使用 ${}:
SQL: SELECT * FROM users WHERE name = '' OR 1=1 --'
含义: SQL 结构被改变,可能变成全表条件
这个例子说明,安全差异不在于 MyBatis 帮你“转义得更好”,而在于数据库从一开始就知道哪部分是 SQL 结构、哪部分只是参数值。
三、动态排序应该怎么写
错误写法:
ORDER BY ${sortBy} ${direction}
安全写法是把外部值转换成内部枚举,再用固定分支:
<choose>
<when test="sortBy == 'createdAt'">ORDER BY created_at</when>
<when test="sortBy == 'name'">ORDER BY name</when>
<otherwise>ORDER BY id</otherwise>
</choose>
排序方向也只允许 ASC/DESC 两种固定片段。白名单校验必须发生在拼接前,不能靠过滤几个危险字符。
四、LIKE 与 IN 的正确处理
LIKE 可在 Java 中把参数构造成 %keyword% 后用 #{keyword},或使用数据库的字符串连接函数。不要写 LIKE '%${keyword}%'。
IN 查询应使用 foreach 为每个元素生成一个参数占位符,而不是把逗号字符串整体塞给一个 #{ids},也不要用 ${ids} 直接拼接。
五、#{} 的常用属性
可以声明 jdbcType、javaType、typeHandler 等参数映射选项。尤其给可能为 null 的值绑定到某些数据库时,显式 jdbcType 可避免驱动无法判断类型。
这些配置只影响值如何绑定,不会让占位符变成动态标识符。
六、常见误区与追问
- 误区:
${}也会像#{}一样做安全转义。${}是文本替换,不做 JDBC 参数化,用户输入直接进入 SQL 结构就有注入风险。 - 误区:所有动态内容都能用
#{}。 表名、列名、排序方向属于 SQL 结构,数据库不会把?当标识符解析,必须从白名单选择固定片段。 - 追问:模糊查询应该怎么写? 在 Java 里拼好
%keyword%后用#{keyword},或用数据库字符串连接函数,避免LIKE '%${keyword}%'。 - 追问:IN 查询为什么不用一个
#{ids}? 一个占位符只代表一个值,多个 id 应用<foreach>生成多个?和参数映射。 - 误区:过滤危险字符就能安全使用
${}。 黑名单过滤容易漏掉编码、注释、函数等绕过方式,正确做法是固定白名单。 - 追问:
jdbcType的作用是什么? 它告诉 JDBC 空值或特殊类型如何绑定,只影响参数值绑定,不改变 SQL 结构。
七、加强记忆
#{} 是“SQL 留 ?、值单独绑定”,${} 是“文本原样拼进去”。值永远优先 #{};只有数据库必须解析成语法结构的表名、列名、排序方向才考虑固定白名单片段,任何用户原文都不能直接进入 ${}。