MyBatis 中 #{} 和 ${} 的区别详解
一、概述
在 MyBatis 中,#{} 和 ${} 都用于在 SQL 语句中引用参数,但它们在底层实现、安全性和使用场景上存在本质区别。
| 对比维度 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译参数(PreparedStatement) | 直接字符串替换 |
| SQL 注入 | 可防止 SQL 注入 | 无法防止 SQL 注入 |
| 参数类型 | 自动加引号(字符串类型加单引号) | 直接拼接,不额外加引号 |
| 适用场景 | 普通参数传递(值) | 动态表名、字段名、ORDER BY 等 |
| 性能 | 较高(可复用预编译 SQL) | 较低(每次重新编译) |
二、核心区别详解
1. #{} — 预编译参数
#{} 使用 PreparedStatement 进行预编译处理。MyBatis 会将 SQL 中的 #{} 部分替换为占位符 ?,然后通过 PreparedStatement.setXxx() 方法设置参数值。
示例:
-- Mapper 中的 SQL
SELECT * FROM user WHERE name = #{name};
-- 预编译后的 SQL
SELECT * FROM user WHERE name = ?;
-- 实际执行(假设 name = "zhangsan")
SELECT * FROM user WHERE name = 'zhangsan';
特点:
- 参数被自动转义,传入的值会被当作一个字符串处理,自动加上单引号。
- 可以有效防止 SQL 注入攻击。
- 相同的 SQL 可以缓存预编译结果,提高性能。
2. ${} — 字符串直接替换
${} 是简单的字符串替换,MyBatis 在解析 SQL 阶段直接将 ${} 替换为参数值,不经过预编译处理。
示例:
-- Mapper 中的 SQL
SELECT * FROM ${tableName} WHERE name = #{name};
-- 解析后(假设 tableName = "user")
SELECT * FROM user WHERE name = ?;
特点:
- 直接将参数值拼接到 SQL 语句中,不加任何转义。
- 存在 SQL 注入风险,不建议用户输入的内容使用
${}。 - 适合动态传入数据库对象(表名、字段名、ORDER BY 排序字段等)。
三、SQL 注入问题分析
使用 #{} 安全的情况
-- 假设 name = "zhangsan' OR '1'='1"
SELECT * FROM user WHERE name = #{name};
-- 预编译后:SELECT * FROM user WHERE name = 'zhangsan'' OR ''1''=''1';
-- 安全,不会导致注入
使用 ${} 危险的情况
-- 假设 tableName = "user; DROP TABLE user; --"
SELECT * FROM ${tableName} WHERE name = #{name};
-- 解析后:SELECT * FROM user; DROP TABLE user; -- WHERE name = ?;
-- 危险!数据库表被删除
结论: 只要允许用户输入的内容作为参数,就必须使用 #{} 来防止 SQL 注入。只有在传入数据库对象名(表名、字段名等)时才考虑使用 ${},且必须严格校验输入内容。
四、什么场景必须用 ${}
虽然推荐优先使用 #{},但在以下场景中必须使用 ${}:
-
动态表名
SELECT * FROM ${tableName} WHERE id = #{id}; -
动态字段名
SELECT ${columnName} FROM user WHERE id = #{id}; -
ORDER BY 子句
SELECT * FROM user ORDER BY ${orderField} ${orderDirection};注意:ORDER BY 后面不支持占位符
?,只能用${}。 -
LIKE 模糊查询(某些写法)
-- #{} 方式(推荐,安全) SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%'); -- ${} 方式(不推荐,有注入风险) SELECT * FROM user WHERE name LIKE '%${name}%';
五、源码层面理解
MyBatis 在解析 Mapper XML 时:
-
#{}→ 创建ParameterMapping对象,生成?占位符 → 执行时通过PreparedStatement.setXxx()设置参数 → 由 数据库驱动 完成转义。 -
${}→ 在DynamicSqlSource.getBoundSql()阶段直接替换字符串 → 生成的 SQL 中已经包含实际值 → 后续解析为普通 SQL 执行。
六、总结
| 要点 | 说明 |
|---|---|
| 优先使用 #{} | 安全、高效,是传递参数值的首选方式 |
| 谨慎使用 ${} | 仅限于动态SQL中的表名、字段名、排序等场景 |
| ${} 必做校验 | 使用 ${} 时,必须对参数进行白名单校验或严格过滤 |
| 混合使用 | 可在同一 SQL 中混合使用:SELECT * FROM ${tableName} WHERE name = #{name} |
| 性能差异 | JDK 1.8+ 的 PreparedStatement 有预编译缓存,#{} 性能优于 ${} |
最佳实践:能用 #{} 的绝不用 ${},不得不用 ${} 时一定要做好输入校验。