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/

面试考察点

面试官提出这个问题,主要希望考察候选人以下几个方面:

  1. 基础语法掌握: 能否清晰地陈述 #{}${} 在 MyBatis 中的基本语法作用和写法区别。
  2. 安全性与SQL注入风险: 这是最核心的考察点。面试官不仅仅想知道“谁安全、谁不安全”,更是想知道你是否深刻理解其背后的原理(预编译 vs. 字符串拼接),以及在实际开发中对安全性的重视程度。
  3. 性能与底层实现原理: 对 MyBatis 及 JDBC 底层工作机制的理解。了解 #{} 如何利用 PreparedStatement 实现预编译,以及这带来的性能优势(如数据库缓存执行计划)。
  4. 适用场景与最佳实践: 能否根据具体场景做出正确选择。面试官希望听到“绝大部分情况用 #{},但在某些特殊动态场景下(如表名、列名动态指定)不得不用 ${},且必须严格过滤”这样的回答,这体现了你的实践经验。
  5. 对“动态SQL”的理解边界: 是否混淆了“参数传递”与“SQL片段动态生成”这两个概念。${} 更像是简单的文本替换,用于 SQL 结构本身的变化;而 #{} 是安全的参数占位符。

核心答案

  • #{} 是 MyBatis 的参数占位符,会对传入的参数进行预编译处理,能有效防止 SQL 注入,适用于所有需要传入参数值的场景
  • ${} 是 MyBatis 的字符串替换符,会直接将传入的参数值(字符串)拼接进 SQL 语句中,存在 SQL 注入风险,通常仅用于动态指定表名、列名等 SQL 关键字或结构部分

深度解析

原理/机制

  • #{} (预编译占位符):
    • MyBatis 在解析 SQL 时,会将 #{} 替换为 ?,即 JDBC PreparedStatement 的占位符。
    • 执行时,PreparedStatement 会安全地将参数值设置进去,数据库驱动会对参数进行正确的类型处理和转义(例如,字符串会自动加引号)。
    • 核心优点: 防 SQL 注入(因为 SQL 结构在预编译时已确定,后续传入的值只会被当作数据,而不会改变 SQL 语义)。性能更高(数据库可以对预编译的 SQL 模板缓存执行计划)。
  • ${} (字符串拼接):
    • MyBatis 在解析 SQL 时,会直接将 ${} 中的内容(字符串)替换到 SQL 语句中,是纯粹的文本拼接。
    • 等同于在 Java 代码中做 "SELECT * FROM " + tableName
    • 核心风险: SQL 注入。如果替换内容是用户输入的,且未经验证过滤,攻击者可拼接恶意 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 关键字部分

最佳实践:

  1. 原则: 能用 #{} 的地方绝不用 ${}。这是 MyBatis 使用的铁律。
  2. ${} 的使用场景: 仅在 SQL 的结构部分需要动态变化时(如动态表名 <select id="findByTable" resultType="map"> SELECT * FROM ${tableName} </select>)才考虑使用。
  3. 使用 ${} 时的强制要求: 必须对传入的 ${} 参数值进行严格的过滤与校验(例如,只允许传入白名单内的值),或确保其来源完全可信(如内部系统生成,非用户输入)。

常见误区

  • 误区一: 认为 ${} 只是 #{} 的一个功能子集或旧写法。它们设计目的完全不同。
  • 误区二: 为了拼接 LIKE 查询而使用 ${},如 LIKE ‘%${name}%’。这是非常危险的做法。正确做法是使用 #{} 并在 Java 代码或 SQL 中拼接通配符:LIKE CONCAT(‘%’, #{name}, ‘%’) (MySQL)或 LIKE ‘%’ || #{name} || ‘%’ (Oracle),或使用 MyBatis 的 <bind> 标签。
  • 误区三:ORDER BY 后使用 #{}。这会导致排序失效,因为 #{} 会给参数值加上引号,最终 SQL 类似 ORDER BY ‘user_name’,这在语法上是合法的,但意思是按一个常量字符串排序,而非按列排序。此时必须使用 ${} 并做好校验。

总结

一句话概括:#{} 是安全的参数值传递方式,而 ${} 是危险的 SQL 片段替换方式;永远优先使用 #{},仅在动态 SQL 结构部分且确保安全的前提下谨慎使用 ${}。理解这个区别,是写出安全、高效 MyBatis 代码的基础。