Java面试——MyBatis中#{}与${}的区别

{}能防止SQL注入而${}不能,因#{}走PreparedStatement预编译并安全绑定参数,${}是纯字符串替换、易被注入;${}仅限动态SQL结构(如表名、排序字段),须白名单校验。

为什么#{}能防止SQL注入而${}不能

根本原因在于 MyBatis 对两者的处理时机和方式不同:#{} 触发预编译(PreparedStatement),参数被当作占位符传入,数据库驱动自动做类型转换和转义;${} 是纯字符串替换,MyBatis 在 SQL 解析阶段就直接把变量值拼进 SQL 字符串里,不经过 JDBC 预编译流程。

比如 WHERE name = #{name} 最终生成的是 WHERE name = ?,再由 PreparedStatement.setString(1, "admin' --") 安全绑定;而 WHERE name = '${name}' 会直接变成 WHERE name = 'admin' --',注释掉后续条件,造成注入漏洞。

哪些场景必须用${},又该怎么控风险

${} 唯一合理用途是动态拼接 **SQL 结构部分**,比如表名、列名、ORDER BY 子句、LIMIT 参数(非数值上下文)——这些都不能用 ? 占位。

  • 表名动态:用 FROM ${tableName},但必须白名单校验 tableName 值,例如只允许 "user""order" 等枚举值
  • 排序字段:用 ORDER BY ${sortColumn} ${sortOrder}sortColumn 必须从固定字段列表中取(如 "id", "create_time"),sortOrder 限制为 "ASC""DESC"
  • 避免在 ${}

    中拼接用户输入的任意字符串,尤其是带单引号、分号、注释符的内容

#{id} 和 ${id} 在参数类型为数字时表现一样?

表面上看都“能运行”,但行为完全不同:

  • WHERE id = #{id} → JDBC 绑定为 setLong(1, 123),类型安全,支持 null 处理(setNull
  • WHERE id = ${id} → 直接替换为 WHERE id = 123,如果 id 是 null,会拼出 WHERE id = null,语法错误或逻辑异常
  • id 是字符串类型(如 "123' OR '1'='1"),${id} 会直接触发注入,#{id} 则作为字符串字面量安全绑定

MyBatis Plus 的 Wrapper 是否也遵循这套规则

是的,QueryWrapperUpdateWrapper 底层仍走 MyBatis 的 SQL 构建逻辑。它默认所有条件方法(如 eq("name", value))都使用 #{} 语义;但当你调用 apply("age > {0}", 18)last("LIMIT 1") 时,就进入了 ${} 类似的行为域——apply 中的 {0} 是模板占位,实际仍可能拼接原始字符串,务必确保传入的参数已过滤或来自可信源。

更稳妥的做法是:优先用 gt("age", 18) 这类类型安全方法;非要用 apply 拼 SQL 片段时,对变量值做正则校验(如 ^\d+$ 匹配纯数字)。

SELECT * FROM ${tableName} WHERE status = #{status} ORDER BY ${sortBy} ${sortOrder}

真正危险的不是语法本身,是把本该由数据库驱动处理的参数,交给人肉字符串拼接来承担。只要涉及用户输入,${} 就得过白名单或格式校验这一关——漏掉一次,就可能让整张表裸奔。