MyBatis 中 #{} 和 ${} 的区别是什么?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
基础掌握度:面试官不仅仅是想知道你 "用过" 这两个符号,更是想考察你是否理解它们底层的处理机制——预编译 vs 字符串拼接。
-
安全意识:这是这道题的核心。考察你是否具备 SQL 注入防范意识,以及能否在开发中做出正确选择。
-
实战经验:看你能否说出
${}的合理使用场景(动态表名、动态排序等),而不是一刀切地说 "永远别用${}"。
核心答案
#{} 和 ${} 的核心区别在于 SQL 参数的替换方式不同:
| 对比维度 | #{} | ${} |
|---|---|---|
| 替换方式 | 预编译处理,使用 ? 占位符 | 字符串拼接,直接替换到 SQL 中 |
| SQL 注入 | 安全,自动转义 | 危险,存在注入风险 |
| 性能 | 预编译 SQL 可缓存复用 | 每次生成不同的 SQL,无法复用 |
| 适用场景 | 传参数值(WHERE 条件等) | 动态表名、列名、ORDER BY 等 |
| 类型处理 | 自动处理类型(加引号等) | 原样拼接,不处理类型 |
一句话结论:能用 #{} 就用 #{},只有动态表名、动态列名、ORDER BY 等场景才用 ${}。
深度解析
一、底层机制对比
上图中可以看到两种方式处理流程的关键差异:
-
#{}的处理流程:MyBatis 在解析 SQL 时,会将#{id}替换为?占位符,然后通过PreparedStatement.setInt()等方法设置参数值。数据库先编译 SQL 结构,再绑定参数,参数值不会参与 SQL 编译,因此不可能改变 SQL 的语法结构。 -
${}的处理流程:MyBatis 在解析 SQL 时,直接将${id}替换为参数的字符串值,拼接到 SQL 中。参数值参与了 SQL 编译,恶意输入可以改变 SQL 的语法结构,导致 SQL 注入。
二、SQL 注入实战演示
来看一个典型的 SQL 注入场景:
<!-- 危险!使用 ${} 拼接用户名 -->
<select id="findByUsername" resultType="User">
SELECT * FROM t_user WHERE username = '${username}'
</select>
假设恶意用户输入:username = "admin' OR '1'='1"
最终执行的 SQL 变成:
-- 原本只想查一个用户,结果变成了查全部用户!
SELECT * FROM t_user WHERE username = 'admin' OR '1'='1'
同样的场景,使用 #{}:
<!-- 安全!使用 #{} 预编译 -->
<select id="findByUsername" resultType="User">
SELECT * FROM t_user WHERE username = #{username}
</select>
不管用户输入什么,最终 SQL 结构始终是:
-- 参数值只是 ? 的绑定值,无法改变 SQL 结构
SELECT * FROM t_user WHERE username = ?
-- 参数绑定值:admin' OR '1'='1(整个字符串作为一个值)
三、${} 的合理使用场景
${} 并非一无是处,以下场景必须用 ${},因为 ? 占位符不能用在 SQL 的结构部分:
场景一:动态表名
<!-- 根据月份动态选择分表 -->
<select id="findByMonth" resultType="Order">
SELECT * FROM t_order_${month} WHERE status = #{status}
</select>
<!-- 调用:month = "202601" → 查询 t_order_202601 表 -->
场景二:动态列名
<!-- 根据用户选择动态排序字段 -->
<select id="findList" resultType="User">
SELECT id, username, email FROM t_user
ORDER BY ${orderColumn} ${orderDir}
</select>
<!-- 调用:orderColumn = "created_time", orderDir = "DESC" -->
场景三:LIKE 查询
<!-- LIKE 查询不能直接用 #{} 包裹 %,需要用 CONCAT 函数 -->
<select id="searchByUsername" resultType="User">
SELECT * FROM t_user
WHERE username LIKE CONCAT('%', #{keyword}, '%')
</select>
注意:
LIKE查询推荐用CONCAT+#{}的方式,而不是'%' + ${keyword} + '%'。
四、${} 的安全防范
当必须使用 ${} 时,一定要在 Java 代码层做白名单校验:
// 动态排序:白名单校验列名和方向
private static final Set<String> ALLOWED_COLUMNS = Set.of(
"id", "username", "created_time", "status"
);
private static final Set<String> ALLOWED_DIRS = Set.of("ASC", "DESC");
public List<User> findList(String orderColumn, String orderDir) {
// 白名单校验,防止注入
if (!ALLOWED_COLUMNS.contains(orderColumn)) {
throw new IllegalArgumentException("非法排序字段");
}
if (!ALLOWED_DIRS.contains(orderDir.toUpperCase())) {
throw new IllegalArgumentException("非法排序方向");
}
return userMapper.findList(orderColumn, orderDir);
}
面试高频追问
-
追问一:为什么
#{}可以防止 SQL 注入?- 因为
#{}使用PreparedStatement的参数绑定机制,参数值不参与 SQL 编译,数据库将参数值作为纯数据处理,无法改变 SQL 的语法结构。
- 因为
-
追问二:
LIKE查询怎么用#{}?- 使用
LIKE CONCAT('%', #{keyword}, '%'),而不是'%' + ${keyword} + '%'。
- 使用
-
追问三:MyBatis 的
#{}底层是怎么实现的?- MyBatis 在解析 Mapper XML 时,将
#{}替换为?,并记录参数映射信息。执行时通过PreparedStatement.setXxx()方法设置参数值,底层依赖 JDBC 的PreparedStatement机制。
- MyBatis 在解析 Mapper XML 时,将
常见面试变体
- 变体一:"MyBatis 如何防止 SQL 注入?"
- 变体二:"
#{}和${}分别在什么场景下使用?" - 变体三:"为什么
#{}不能用在ORDER BY后面?"
记忆口诀
# 号安全占位问,$ 号直接拼字符串——#{} 是 ? 占位符(安全),${} 是字符串拼接(危险),能用 # 就别用 $。
总结
#{} 使用 PreparedStatement 预编译处理,参数通过 ? 占位符绑定,安全防注入;${} 直接字符串拼接到 SQL 中,存在 SQL 注入风险。日常开发中优先使用 #{},只有在动态表名、动态列名、ORDER BY 等无法使用占位符的场景才用 ${},并且必须在代码层做白名单校验。