java面试必问12:MyBatis #{} 和 ${} 区别:从预编译到防注入,一篇讲透
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 注入坑,欢迎继续讨论。
如果觉得我的内容对您有帮助,欢迎了解我的更多干货输出。我的个人简介最后有一段内容,感兴趣的朋友可以去找找看。那里有我日常分享的技术深度解析和职场避坑指南,期待与您继续交流。
更多推荐



所有评论(0)