java面试必问12:MyBatis #{} 和 ${} 区别:从预编译到防注入,一篇讲透

张开发
2026/4/18 5:07:15 15 分钟阅读

分享文章

java面试必问12:MyBatis #{} 和 ${} 区别:从预编译到防注入,一篇讲透
MyBatis #{} 和 ${} 区别从预编译到防注入一篇讲透面试官“MyBatis 中 #{} 和KaTeX parse error: Expected EOF, got # at position 17: …} 有什么区别” 你“#̲{} 是预编译占位符生成 P…{} 是纯字符串拼接直接替换到 SQL 中不安全。传参值、列值必须用 #{}只有表名、排序字段等动态结构才考虑 ${}。原则是能用 #{} 绝不用 ${}。”面试官“那 ${} 就一定不能用吗什么场景下不得不使用”你“……”很多人知道 #{} 安全但不知道为什么安全也不清楚 ${} 的合理使用场景。本文从底层原理、注入攻击示例、最佳实践等方面彻底讲透这两个占位符的区别。一、核心区别速览特性#{}${}处理方式预编译生成?占位符直接字符串拼接SQL 预编译是使用PreparedStatement否使用Statement防 SQL 注入✅ 安全❌ 不安全需手动过滤适用场景传入参数值where 条件、insert 值、update 值动态表名、列名、排序字段、limit后跟变量部分数据库示例where id #{id}→where id ?order by ${column}→order by name二、底层原理为什么 #{} 能防注入1. #{} 预编译机制MyBatis 会将#{xxx}替换成?然后使用java.sql.PreparedStatement设置参数。PreparedStatement 会在数据库端预编译 SQL 语句参数值在编译后作为数据传递不会改变 SQL 结构。// MyBatis 生成的伪代码Stringsqlselect * from user where name ?;PreparedStatementpsconn.prepareStatement(sql);ps.setString(1,张三);// 参数值被安全转义即使传入张三 or 11也只会被当作普通字符串不会导致 SQL 注入。2. ${} 字符串拼接${xxx}直接替换为传入的字符串然后 MyBatis 使用java.sql.Statement执行拼接后的 SQL。这种方式不会预编译参数值直接嵌入 SQL 语句中。Stringsqlselect * from user where name name;Statementstmtconn.createStatement();stmt.executeQuery(sql);如果name传入张三 or 11最终 SQL 变成select*fromuserwherename张三or11条件永远为真返回所有用户造成注入。三、SQL 注入示例与危害攻击场景登录验证selectidloginresultTypeUserselect * from user where username ${username} and password ${password}/select正常请求usernameadmin, password123456 SQL: select * from user where username admin and password 123456恶意请求username admin -- password 任意 最终 SQL: select * from user where username admin -- and password xxx--注释了后面的密码检查无需密码即可登录。更危险的注入username ; drop table user; --可能导致表被删除取决于数据库权限。使用 ${} 且参数来自用户输入时风险极高。四、必须使用 ${} 的场景尽管 #{} 更安全但有些动态结构无法用占位符替代1. 动态表名selectidselectFromTableresultTypemapselect * from ${tableName} where id #{id}/select表名不能参数化PreparedStatement 不支持表名占位符只能拼接。2. 动态列名selectidselectOrderByresultTypeUserselect * from user order by ${column} ${direction}/select排序字段和排序方向ASC/DESC无法用?传递。3. 部分数据库的特殊语法例如 MySQL 的limit后跟变量limit ${offset}, ${limit}某些版本不支持占位符高版本已支持但为兼容性可能用拼接。4. 动态in条件数量不固定selectidselectByIdsresultTypeUserselect * from user where id in (${ids})/select如果ids是1,2,3可以用${}拼接。但更推荐使用 MyBatis 的foreach配合#{}来安全处理。五、使用 ${} 的安全措施当不得不使用${}时必须对输入进行严格校验例如表名/列名白名单publicStringcheckTableName(StringtableName){SetStringallowedSet.of(user,order,product);if(!allowed.contains(tableName)){thrownewIllegalArgumentException(Invalid table name);}returntableName;}排序方向枚举publicStringcheckDirection(Stringdirection){if(!ASC.equalsIgnoreCase(direction)!DESC.equalsIgnoreCase(direction)){thrownewIllegalArgumentException(Invalid direction);}returndirection;}避免直接拼接用户输入所有${}的值应来自可信代码如枚举、常量而非用户请求。六、常见误区与最佳实践误区1${}用于模糊查询错误写法selectidfindByNameselect * from user where name like %${name}%/select正确写法selectidfindByNameselect * from user where name like concat(%, #{name}, %)/select或使用数据库函数。误区2${}用于limit后部分数据库支持占位符应优先使用selectidpageselect * from user limit #{offset}, #{limit}/selectMySQL 从 5.5 开始支持占位符。误区3认为#{}一定比${}慢#{}预编译一次后可多次执行实际性能通常优于${}避免重复解析 SQL。不存在性能劣势。最佳实践总结默认使用#{}除非明确需要动态结构。动态表名/列名必须使用${}时添加白名单校验。避免${}拼接用户输入尤其是直接来自 HTTP 参数的。使用foreach处理in条件selectidselectByIdsresultTypeUserselect * from user where id inforeachcollectionidsitemidopen(separator,close)#{id}/foreach/select开启 MyBatis 日志观察生成的 SQL 是否有注入风险。七、总结对比表对比项#{}${}SQL 结构预编译参数占位符纯字符串替换防注入✅ 安全❌ 风险高适用位置值where、insert、update表名、列名、order by、limit部分性能预编译可重用通常更好每次拼接无法预编译类型处理自动类型转换如日期转字符串直接拼接需手动处理空值处理会设置null类型拼接成null字符串一句口诀参数值用井号防注入结构动态用美元需谨慎。希望这篇文章能让你彻底分清 #{} 和 ${}面试时对答如流写代码时避开所有 SQL 注入坑欢迎继续讨论。如果觉得我的内容对您有帮助欢迎了解我的更多干货输出。我的个人简介最后有一段内容感兴趣的朋友可以去找找看。那里有我日常分享的技术深度解析和职场避坑指南期待与您继续交流。

更多文章