MyBatis 中 #{} 和 ${} 的区别是什么?
2026年01月06日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/
面试考察点
面试官提出这个问题,主要希望考察候选人以下几个方面:
- 基础语法掌握: 能否清晰地陈述
#{}和${}在 MyBatis 中的基本语法作用和写法区别。 - 安全性与SQL注入风险: 这是最核心的考察点。面试官不仅仅想知道“谁安全、谁不安全”,更是想知道你是否深刻理解其背后的原理(预编译 vs. 字符串拼接),以及在实际开发中对安全性的重视程度。
- 性能与底层实现原理: 对 MyBatis 及 JDBC 底层工作机制的理解。了解
#{}如何利用PreparedStatement实现预编译,以及这带来的性能优势(如数据库缓存执行计划)。 - 适用场景与最佳实践: 能否根据具体场景做出正确选择。面试官希望听到“绝大部分情况用
#{},但在某些特殊动态场景下(如表名、列名动态指定)不得不用${},且必须严格过滤”这样的回答,这体现了你的实践经验。 - 对“动态SQL”的理解边界: 是否混淆了“参数传递”与“SQL片段动态生成”这两个概念。
${}更像是简单的文本替换,用于 SQL 结构本身的变化;而#{}是安全的参数占位符。
核心答案
#{}是 MyBatis 的参数占位符,会对传入的参数进行预编译处理,能有效防止 SQL 注入,适用于所有需要传入参数值的场景。${}是 MyBatis 的字符串替换符,会直接将传入的参数值(字符串)拼接进 SQL 语句中,存在 SQL 注入风险,通常仅用于动态指定表名、列名等 SQL 关键字或结构部分。
深度解析
原理/机制
#{}(预编译占位符):- MyBatis 在解析 SQL 时,会将
#{}替换为?,即 JDBCPreparedStatement的占位符。 - 执行时,
PreparedStatement会安全地将参数值设置进去,数据库驱动会对参数进行正确的类型处理和转义(例如,字符串会自动加引号)。 - 核心优点: 防 SQL 注入(因为 SQL 结构在预编译时已确定,后续传入的值只会被当作数据,而不会改变 SQL 语义)。性能更高(数据库可以对预编译的 SQL 模板缓存执行计划)。
- MyBatis 在解析 SQL 时,会将
${}(字符串拼接):- MyBatis 在解析 SQL 时,会直接将
${}中的内容(字符串)替换到 SQL 语句中,是纯粹的文本拼接。 - 等同于在 Java 代码中做
"SELECT * FROM " + tableName。 - 核心风险: SQL 注入。如果替换内容是用户输入的,且未经验证过滤,攻击者可拼接恶意 SQL 片段,导致数据泄露或破坏。
- MyBatis 在解析 SQL 时,会直接将
代码示例:
<!-- 假设传入的参数为: userId = 1, orderByColumn = “user_name” -->
<!-- 使用 #{},安全 -->
<select id="selectBySafe" resultType="User">
SELECT * FROM user WHERE id = #{userId}
</select>
<!-- 解析后的SQL: SELECT * FROM user WHERE id = ? -->
<!-- 执行时,参数 1 被安全地设置进去 -->
<!-- 使用 ${} 进行动态排序(无可变值参数时,属于可接受风险场景) -->
<select id="selectByOrder" resultType="User">
SELECT * FROM user ORDER BY ${orderByColumn}
</select>
<!-- 解析后的SQL: SELECT * FROM user ORDER BY user_name -->
<!-- 此处 ${} 用于替换SQL关键字部分,若orderByColumn来自用户输入且未校验,可传入“user_name; DROP TABLE user--”进行注入 -->
<!-- 【危险示例】错误地在值参数位置使用 ${} -->
<select id="selectByDanger" resultType="User">
SELECT * FROM user WHERE id = ${userId}
</select>
<!-- 若 userId 传入 “1 OR 1=1”,解析后的 SQL 变为:SELECT * FROM user WHERE id = 1 OR 1=1 -->
<!-- 这将导致查询出所有用户数据,严重安全漏洞! -->
对比分析与最佳实践
| 特性 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译(PreparedStatement) | 字符串拼接(Statement) |
| 安全性 | 高,防止 SQL 注入 | 低,存在注入风险 |
| 性能 | 高(可利用预编译缓存) | 相对较低 |
| 参数类型 | 自动添加引号(字符串等) | 原样替换,不添加引号 |
| 主要用途 | 传递 WHERE/INSERT/UPDATE 等语句中的参数值 | 动态指定表名、列名、ORDER BY 子句等 SQL 关键字部分 |
最佳实践:
- 原则: 能用
#{}的地方绝不用${}。这是 MyBatis 使用的铁律。 ${}的使用场景: 仅在 SQL 的结构部分需要动态变化时(如动态表名<select id="findByTable" resultType="map"> SELECT * FROM ${tableName} </select>)才考虑使用。- 使用
${}时的强制要求: 必须对传入的${}参数值进行严格的过滤与校验(例如,只允许传入白名单内的值),或确保其来源完全可信(如内部系统生成,非用户输入)。
常见误区
- 误区一: 认为
${}只是#{}的一个功能子集或旧写法。它们设计目的完全不同。 - 误区二: 为了拼接
LIKE查询而使用${},如LIKE ‘%${name}%’。这是非常危险的做法。正确做法是使用#{}并在 Java 代码或 SQL 中拼接通配符:LIKE CONCAT(‘%’, #{name}, ‘%’)(MySQL)或LIKE ‘%’ || #{name} || ‘%’(Oracle),或使用 MyBatis 的<bind>标签。 - 误区三: 在
ORDER BY后使用#{}。这会导致排序失效,因为#{}会给参数值加上引号,最终 SQL 类似ORDER BY ‘user_name’,这在语法上是合法的,但意思是按一个常量字符串排序,而非按列排序。此时必须使用${}并做好校验。
总结
一句话概括:#{} 是安全的参数值传递方式,而 ${} 是危险的 SQL 片段替换方式;永远优先使用 #{},仅在动态 SQL 结构部分且确保安全的前提下谨慎使用 ${}。理解这个区别,是写出安全、高效 MyBatis 代码的基础。