{}通过PreparedStatement预编译防SQL注入,参数作为数据绑定;${}是字符串拼接,直接参与SQL解析,易导致注入。仅ORDER BY、表名等语法位置必须用${},但需白名单校验。

因为 #{} 走的是 JDBC PreparedStatement 预编译流程,参数值永远作为「数据」传入,不参与 SQL 语法解析;而 ${} 是纯字符串替换,用户输入直接拼进 SQL 文本,数据库会把它当语法执行。
预编译机制让恶意输入变成普通字符串
MyBatis 解析 #{id} 时,会把整条 SQL 改写成类似 SELECT * FROM user WHERE id = ? 的模板,交给数据库预编译。真实参数值通过 setLong(1, id) 这类方法单独绑定——哪怕传入 "1; DROP TABLE user; --",最终执行的也只是:
SELECT * FROM user WHERE id = '1; DROP TABLE user; --'
数据库只查一个带分号和破折号的字符串值,不会触发任何额外语句。
用 ${id} 则完全不同:MyBatis 直接把变量内容原样塞进 SQL,比如传入 "1' OR '1'='1",生成的就是:
SELECT * FROM user WHERE id = '1' OR '1'='1'
这已经是一条完整、可执行的注入语句。
类型安全与自动转义是#{}的默认行为
#{} 不仅防注入,还自带类型处理能力:
- 字符串参数自动加单引号,
#{name}传"O'Reilly"→WHERE name = 'O''Reilly'(内部已转义) - 数值参数不加引号,
#{age}传"123abc"→ 直接抛NumberFormatException,根本不会走到 SQL 执行阶段 - 日期、布尔等类型也按 JDBC 类型映射,避免隐式转换带来的歧义
而 ${} 完全跳过这些校验,传什么就拼什么,连空格、换行、注释符都照单全收。
ORDER BY / 表名等场景必须用${},但风险完全在你手上
这些地方用 #{} 会直接报错,不是 MyBatis 不想支持,而是 JDBC 规范禁止:
-
ORDER BY #{column}→ 实际生成ORDER BY 'name',语法错误 -
FROM #{tableName}→ 变成FROM 'user',数据库找不到带引号的表名
所以你不得不用 ${column},但这就意味着:所有传入值必须经过白名单硬编码校验,比如
if (!Set.of("id", "name", "created_at").contains(column)) { throw new IllegalArgumentException(); }漏掉一次校验,或允许前端传任意字段名,${column} 和裸写 Statement 就没区别。
真正危险的从来不是 ${} 本身,而是把它当成“和 #{} 差不多,只是写法不同”的误解。只要动态拼接出现在 SQL 关键字位置,就必须由你亲手守住白名单、元数据校验、输入来源控制这三道关——没有自动防护,只有手动兜底。

















