MyBatis 中 #{} 和 ${} 的区别是什么?
- 标签:后端、MyBatis
🏛 业务架构师
#{} 是预编译参数占位:SQL 模板先发给数据库编译,参数用 ? 传入由驱动安全转义——值被当数据,杜绝 SQL 注入。${} 是字符串直接拼接:值被当代码拼进 SQL,注入风险敞开。
${} 的合法场景非常少:动态表名、动态列名、order by 的列——这些结构部分无法用 ? 占位。即便如此,也必须白名单校验:
// 允许的排序列白名单
Set<String> allowed = Set.of("created_at", "price", "sales");
if (!allowed.contains(sortField)) throw new BizException("非法排序字段");业务视角我想把这件事拔高:SQL 注入不是技术 bug,是资损与合规事件。注入成功可以拖库(用户数据泄露 → 监管处罚 + 公关危机)、可以篡改(改余额、改权限)。等保、PCI-DSS 都把它列为红线项,安全扫描里出现 ${} 拼接外部输入,我按最高优先级打回。
治理动作三条:
- 代码规约:一切"值"用 #{},一切"结构"(表名/列名/排序)走白名单映射,禁止 ${} 直接触碰任何外部输入;
- 门禁:CI 静态扫描(Sonar 规则)拦截 mapper 里的 ${},白名单场景显式注解豁免并 CR 重点审;
- 纵深防御:数据库账号最小权限(应用账号不给 DDL、不给跨库)、敏感字段加密——就算注入发生,爆炸半径也被限定。
一句话:#{} vs ${} 是五分钟的知识点,"外部输入永不信任"是贯穿整个系统的纪律。
🏗 系统架构师
两者都是 MyBatis 的参数占位符,本质差异在预编译与字符串拼接:
- #{}:生成 PreparedStatement 的
?占位符,参数通过 setXxx 传入。预编译带来三重收益:防 SQL 注入(参数不参与 SQL 文法解析)、语句可被数据库执行计划缓存、类型安全映射。业务参数一律用它; - ${}:字符串直接替换进 SQL,参数成为 SQL 语法的一部分。有注入风险(用户可控输入拼 order by 就是典型注入点),但少数场景必须用它:动态表名(分表
${tableName})、动态列、order by 字段名——这些无法作为预编译参数,因为 SQL 结构本身要变。
我的工程规范:
- 默认强制 #{},Code Review 扫描 ${} 出现点;
- 不得不用的 ${}:白名单校验(枚举值比对)、不信任任何前端传入直接拼接;
- ORM 绕不开的规律:值用参数绑定,结构用白名单——这条原则同样适用于 JPA、JdbcTemplate 和自研 SQL 拼装层,注入事故几乎都出在"把值当结构拼"这一处。
💻 全栈开发专家
#{} 预编译占位,${} 字符串拼接——前者防 SQL 注入,后者不防。
java
// #{}
SELECT * FROM user WHERE name = #{name}
// → SELECT * FROM user WHERE name = ? (PreparedStatement 参数绑定)
// ${}
SELECT * FROM user WHERE name = '${name}'
// name = "a' OR '1'='1" → 全表数据被拖走执行差异:#{} 走 PreparedStatement,一次 SQL 模板可复用执行计划缓存;${} 每次拼出不同 SQL,还有注入风险。默认永远用 #{}。
${} 的合法场景(SQL 语法不允许参数占位的地方):
xml
ORDER BY ${orderColumn} ${orderDir} -- 排序字段
FROM ${tableName} -- 动态表名(分表路由)
GROUP BY ${dimension}我的安全规范:${} 的值绝不允许来自用户原始输入,必须白名单校验:
java
private static final Set<String> ALLOWED = Set.of("create_time", "amount", "user_id");
Preconditions.checkArgument(ALLOWED.contains(column));踩过的真实案例:排序字段直接从前端透传进 ${},安全扫描打出一串注入 payload,其中 updatexml(1,concat(0x7e,database()),1) 报错注入直接能探库。修复就是白名单 + 默认排序兜底。