一、核心结论(先记死,不踩坑)
✅ `#{}`:安全占位符,防 SQL 注入,99% 的业务场景优先使用; ❌ `{}\`:字符串直接拼接,有 SQL 注入风险,仅3种特殊场景可用; 口诀:能 #{ 就 #{,实在不行才 {}。
二、两者核心原理(彻底搞懂"什么时候替换")
1. #{ }:预处理占位符(安全核心)
核心逻辑:Java 程序将 SQL 发送给 MySQL 之前,会先把 `#{xxx}` 全部替换成 `?`(占位符),再将参数值单独传递给 MySQL,由 MySQL 安全填充参数,全程不拼接 SQL 字符串。
完整流程示例(以查询频道名称为例):
-
Java 中写法:`@Select("select * from leadnews_wemedia.wm_channel where name = #{name}")`
-
Java 层预处理:将 `#{name}` 替换成 `?`,最终发给 MySQL 的 SQL 是:`select * from leadnews_wemedia.wm_channel where name = ?`
-
MySQL 执行:将参数值(如"Java")安全填充到 `?` 位置,执行查询。
2. ${ }:字符串直接拼接(危险核心)
核心逻辑:在 Java 程序内部,直接将参数值拼接到 SQL 语句中,拼接完成后再发送给 MySQL 执行。相当于"直接把参数写死到 SQL 里",没有任何安全处理。
完整流程示例:
-
Java 中写法:`@Select("select * from leadnews_wemedia.wm_channel where name = ${name}")`
-
Java 层拼接:若参数值为"Java",直接拼成:`select * from leadnews_wemedia.wm_channel where name = Java`(语法错误);若参数值为恶意内容,会直接被当作 SQL 逻辑执行。
-
MySQL 执行:直接执行拼接后的 SQL,风险极高。
三、为什么 #{ } 能防 SQL 注入?${ } 却不行?
1. #{ } 防注入的核心原因
因为参数值永远被当作"普通字符串"处理,不会被 MySQL 解析为 SQL 代码。即使传入恶意参数,也只会被当作字符串拼接,无法改变 SQL 的执行逻辑。
示例(恶意注入场景):
前端传入恶意参数:`name = "张三' or '1'='1"`
用 `#{name}` 最终执行的 SQL:`select * from wm_channel where name = '张三\' or \'1\'=\'1'`
解析:整个恶意参数被当作一个字符串(转义了单引号),MySQL 只会查询"张三' or '1'='1"这个字符串对应的频道,不会执行 `or '1'='1` 这个永真条件,避免了全表数据泄露。
2. ${ } 存在注入风险的原因
因为参数值直接拼接进 SQL,若传入恶意参数,会被 MySQL 当作 SQL 逻辑执行,篡改原有的查询意图。
同样的恶意参数,用 `${name}` 拼接后执行的 SQL:
`select * from wm_channel where name = '张三' or '1'='1'`
解析:`or '1'='1` 是永真条件,会查询出表中所有数据,造成数据泄露,这就是典型的 SQL 注入攻击。
四、关键区别对比表(一目了然)
| 对比维度 | #{ } | ${ } |
|---|---|---|
| 核心原理 | 预处理占位符,替换为 ? 后填充参数 | 字符串直接拼接,参数直接嵌入 SQL |
| 替换时机 | Java 层预处理时替换为 ?,MySQL 层填充值 | Java 层直接拼接,拼接完成后发给 MySQL |
| 安全性 | 极高,自动防 SQL 注入 | 极低,无任何安全处理,易注入 |
| 参数处理 | 自动添加引号(字符串类型),无需手动处理 | 不添加引号,需手动处理格式 |
| 适用场景 | 普通参数传递(查询、新增、修改、删除的条件参数) | 表名、排序字段、SQL 关键字 |
五、${ } 的 3 种必用场景(别无选择)
虽然 { } 有风险,但以下 3 种场景,只能用 { },无法用 #{ }(因为 #{ } 会自动加引号,导致 SQL 失效)。
1. 传入表名(动态切换表)
示例:动态查询不同表的数据(如按日期分表)
java
// 正确:用 ${} 传入表名,表名不能加引号
@Select("select * from ${tableName} where id = #{id}")
User selectByTable(@Param("tableName") String tableName, @Param("id") Integer id);
// 错误:用 #{tableName} 会变成 select * from 'user_2024' where id = ?,SQL 语法错误
2. 传入排序字段和排序方式(order by 后)
示例:动态排序(按创建时间/ID 升序/降序)
java
// 正确:排序字段和 desc/asc 关键字用 ${}
@Select("select * from wm_channel order by ${sortColumn} ${sortType}")
List<AdChannel> listBySort(@Param("sortColumn") String sortColumn, @Param("sortType") String sortType);
// 错误:用 #{sortColumn} 会变成 order by 'created_time' desc,排序失效
3. 传入 SQL 关键字(如 limit、in 等特殊场景)
示例:动态拼接 limit 分页参数(若分页参数是动态传入,且需拼接)
java
3. 传入 SQL 关键字(如 limit、in 等特殊场景)
示例:动态拼接 limit 分页参数(若分页参数是动态传入,且需拼接)
六、面试高频提问(提前准备)
-
MyBatis 中 #{ } 和 ${ } 的区别?答:核心是预处理 vs 字符串拼接,前者防注入,后者有风险,适用场景不同。
-
为什么 #{ } 能防止 SQL 注入?答:参数先替换为 ?,MySQL 安全填充,参数始终作为字符串,不被解析为 SQL 代码。
-
什么时候必须用 ${ }?答:传入表名、排序字段、SQL 关键字时。