MyBatis #{} 和 ${} 区别:从预编译到防注入,一篇讲透

面试官:“MyBatis 中 #{} 和 KaTeX parse error: Expected 'EOF', got '#' at position 17: …} 有什么区别?” 你:“#̲{} 是预编译占位符,生成 P…{} 是纯字符串拼接,直接替换到 SQL 中,不安全。传参值、列值必须用 #{},只有表名、排序字段等动态结构才考虑 ${}。原则是能用 #{} 绝不用 ${}。”
面试官:“那 ${} 就一定不能用吗?什么场景下不得不使用?”
你:“……”

很多人知道 #{} 安全,但不知道为什么安全,也不清楚 ${} 的合理使用场景。本文从底层原理、注入攻击示例、最佳实践等方面彻底讲透这两个占位符的区别。


一、核心区别速览

特性 #{} ${}
处理方式 预编译,生成 ? 占位符 直接字符串拼接
SQL 预编译 是,使用 PreparedStatement 否,使用 Statement
防 SQL 注入 ✅ 安全 ❌ 不安全,需手动过滤
适用场景 传入参数值(where 条件、insert 值、update 值) 动态表名、列名、排序字段、limit 后跟变量(部分数据库)
示例 where id = #{id}where id = ? order by ${column}order by name

二、底层原理:为什么 #{} 能防注入?

1. #{} 预编译机制

MyBatis 会将 #{xxx} 替换成 ?,然后使用 java.sql.PreparedStatement 设置参数。PreparedStatement 会在数据库端预编译 SQL 语句,参数值在编译后作为数据传递,不会改变 SQL 结构。

// MyBatis 生成的伪代码
String sql = "select * from user where name = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "张三");  // 参数值被安全转义

即使传入 "张三' or '1'='1",也只会被当作普通字符串,不会导致 SQL 注入。

2. ${} 字符串拼接

${xxx} 直接替换为传入的字符串,然后 MyBatis 使用 java.sql.Statement 执行拼接后的 SQL。这种方式不会预编译,参数值直接嵌入 SQL 语句中。

String sql = "select * from user where name = '" + name + "'";
Statement stmt = conn.createStatement();
stmt.executeQuery(sql);

如果 name 传入 "张三' or '1'='1",最终 SQL 变成:

select * from user where name = '张三' or '1'='1'

条件永远为真,返回所有用户,造成注入。


三、SQL 注入示例与危害

攻击场景:登录验证

<select id="login" resultType="User">
    select * from user where username = '${username}' and password = '${password}'
</select>

正常请求:

username=admin, password=123456
SQL: select * from user where username = 'admin' and password = '123456'

恶意请求:

username = admin' -- 
password = 任意
最终 SQL: select * from user where username = 'admin' -- ' and password = 'xxx'

-- 注释了后面的密码检查,无需密码即可登录。

更危险的注入

username = '; drop table user; --

可能导致表被删除(取决于数据库权限)。使用 ${} 且参数来自用户输入时,风险极高


四、必须使用 ${} 的场景

尽管 #{} 更安全,但有些动态结构无法用占位符替代:

1. 动态表名

<select id="selectFromTable" resultType="map">
    select * from ${tableName} where id = #{id}
</select>

表名不能参数化(PreparedStatement 不支持表名占位符),只能拼接。

2. 动态列名

<select id="selectOrderBy" resultType="User">
    select * from user order by ${column} ${direction}
</select>

排序字段和排序方向(ASC/DESC)无法用 ? 传递。

3. 部分数据库的特殊语法

例如 MySQL 的 limit 后跟变量:limit ${offset}, ${limit},某些版本不支持占位符(高版本已支持,但为兼容性可能用拼接)。

4. 动态 in 条件(数量不固定)

<select id="selectByIds" resultType="User">
    select * from user where id in (${ids})
</select>

如果 ids"1,2,3",可以用 ${} 拼接。但更推荐使用 MyBatis 的 <foreach> 配合 #{} 来安全处理。


五、使用 ${} 的安全措施

当不得不使用 ${} 时,必须对输入进行严格校验,例如:

  • 表名/列名白名单
public String checkTableName(String tableName) {
    Set<String> allowed = Set.of("user", "order", "product");
    if (!allowed.contains(tableName)) {
        throw new IllegalArgumentException("Invalid table name");
    }
    return tableName;
}
  • 排序方向枚举
public String checkDirection(String direction) {
    if (!"ASC".equalsIgnoreCase(direction) && !"DESC".equalsIgnoreCase(direction)) {
        throw new IllegalArgumentException("Invalid direction");
    }
    return direction;
}
  • 避免直接拼接用户输入:所有 ${} 的值应来自可信代码(如枚举、常量),而非用户请求。

六、常见误区与最佳实践

误区1:${} 用于模糊查询

错误写法:

<select id="findByName">
    select * from user where name like '%${name}%'
</select>

正确写法:

<select id="findByName">
    select * from user where name like concat('%', #{name}, '%')
</select>

或使用数据库函数。

误区2:${} 用于 limit

部分数据库支持占位符,应优先使用:

<select id="page">
    select * from user limit #{offset}, #{limit}
</select>

MySQL 从 5.5 开始支持占位符。

误区3:认为 #{} 一定比 ${}

#{} 预编译一次后可多次执行,实际性能通常优于 ${}(避免重复解析 SQL)。不存在性能劣势。

最佳实践总结

  • 默认使用 #{},除非明确需要动态结构。
  • 动态表名/列名 必须使用 ${} 时,添加白名单校验。
  • 避免 ${} 拼接用户输入,尤其是直接来自 HTTP 参数的。
  • 使用 <foreach> 处理 in 条件
<select id="selectByIds" resultType="User">
    select * from user where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
  • 开启 MyBatis 日志,观察生成的 SQL 是否有注入风险。

七、总结对比表

对比项 #{} ${}
SQL 结构 预编译,参数占位符 纯字符串替换
防注入 ✅ 安全 ❌ 风险高
适用位置 值(where、insert、update) 表名、列名、order by、limit(部分)
性能 预编译可重用,通常更好 每次拼接,无法预编译
类型处理 自动类型转换(如日期转字符串) 直接拼接,需手动处理
空值处理 会设置 null 类型 拼接成 null 字符串

一句口诀参数值用井号,防注入;结构动态用美元,需谨慎。

希望这篇文章能让你彻底分清 #{} 和 ${},面试时对答如流,写代码时避开所有 SQL 注入坑,欢迎继续讨论。

如果觉得我的内容对您有帮助,欢迎了解我的更多干货输出。我的个人简介最后有一段内容,感兴趣的朋友可以去找找看。那里有我日常分享的技术深度解析和职场避坑指南,期待与您继续交流。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐