MyBatis 中 #{} 与 ${} 终极指南:区别、场景与安全实践

一、引言:为什么会有两种参数传递方式?

MyBatis 作为 Java 生态最流行的持久层框架,提供了 #{} 和 ${} 两种参数注入方式。很多开发者在实际开发中会混淆二者的用法:

  • 为什么 ORDER BY 必须用 ${} 而不能用 #{} ?
  • 为什么用 #{} 能防 SQL 注入,而 ${} 不行?
  • 哪些场景必须用 ${},哪些场景坚决不能用?

本文将从底层原理到实际场景,一次性讲透二者的核心差异与使用准则。

二、核心原理:#{} 与 ${} 的本质区别

二者的核心差异在于 参数处理方式—— 是否进行 SQL 预编译,这直接决定了它们的安全性、语法兼容性和适用场景。

对比维度

#{} (参数占位符)

${} (字符串替换)

底层实现

生成 ? 占位符,通过 PreparedStatement 预编译

直接将参数值作为字符串 “替换” 到 SQL 语句中

最终 SQL 形式

SELECT * FROM user WHERE id = ?(执行时填充参数)

SELECT * FROM user WHERE id = 10(编译前已拼接)

数据类型处理

自动进行类型转换(如字符串加单引号、日期格式化)

不做任何转换,直接拼接原始字符串

SQL 注入风险

无(参数预编译,自动转义特殊字符)

有(直接拼接用户输入,易被注入恶意 SQL)

语法兼容性

仅支持 “参数值” 注入(如 WHERE 条件、INSERT 值)

支持 “语法片段” 注入(如列名、表名、关键字)

执行计划缓存

支持(预编译 SQL 可复用执行计划)

不支持(每次参数不同则 SQL 不同,需重新解析)

关键结论:

#{} 是 “参数级注入”,适合传递查询条件的值;${} 是 “语法级注入”,适合传递 SQL 语句的语法片段。

三、使用场景:该用 #{} 还是 ${}?

1. 优先用 #{} 的场景(90% 以上的业务场景)

只要是传递「参数值」,一律用 #{} —— 这是最安全、最通用的选择。

典型场景

  • WHERE 条件中的等值查询、范围查询
  • INSERT 语句的 values 赋值
  • UPDATE 语句的 set 赋值

示例代码

<!-- 1. WHERE 条件查询(正确用法) -->
<select id="getUserById" resultType="User">
  SELECT id, name, age FROM user WHERE id = #{id}
</select>
<!-- 2. INSERT 赋值(正确用法) -->
<insert id="addUser">
  INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert>
<!-- 3. UPDATE 赋值(正确用法) -->
<update id="updateUser">
  UPDATE user SET age = #{newAge} WHERE id = #{id}
</update>

执行效果

MyBatis 会将 #{} 替换为 ?,预编译后通过 PreparedStatement 填充参数,即使参数包含特殊字符(如 '、;)也会自动转义,避免 SQL 注入。例如:

  • 参数 name = "张三' OR 1=1" 会被转义为 '张三'' OR 1=1',最终 SQL 为:
INSERT INTO user (name, age) VALUES ('张三'' OR 1=1', 25)

不会触发恶意注入。

2. 必须用 ${} 的场景(仅当需要动态拼接 SQL 语法时)

${} 是直接字符串替换,适合传递「SQL 语法的一部分」—— 这些部分无法用 ? 占位符表示(语法不允许)。

典型场景

(1)动态列名(ORDER BY、GROUP BY)

ORDER BY 和 GROUP BY 后面跟的是「列名」或「排序方向」,属于 SQL 语法片段,不能用 #{} (会被加单引号导致语法错误)。

错误示例(用 #{}

<!-- 错误:ORDER BY 后用 #{} 会导致语法错误 -->
<select id="getUserList" resultType="User">
  SELECT * FROM user ORDER BY #{sortColumn}  -- 编译后为 ORDER BY 'name'
</select>

最终执行的 SQL 是:

SELECT * FROM user ORDER BY 'name'  -- 按字符串常量排序,所有行排序结果相同

排序逻辑完全失效。

正确示例(用 ${}

<select id="getUserList" resultType="User">
  SELECT * FROM user ORDER BY ${sortColumn} ${sortOrder}
</select>

当参数 sortColumn = "name"、sortOrder = "DESC" 时,拼接后的 SQL 为:

SELECT * FROM user ORDER BY name DESC  -- 正确按 name 列降序排序
(2)动态表名(分表场景)

按日期、用户 ID 分表的场景,表名需要动态拼接(如 user_202511、user_10086),此时必须用 ${}。

示例

<!-- 按日期分表查询 -->
<select id="getOrderByDate" resultType="Order">
  SELECT * FROM order_${dateSuffix} WHERE user_id = #{userId}
</select>

参数 dateSuffix = "202511" 时,最终 SQL 为:

SELECT * FROM order_202511 WHERE user_id = ?
(3)动态 SQL 关键字 / 函数

需要动态拼接 SQL 关键字(如 ASC/DESC)、函数(如 COUNT()、SUM())时,用 ${}。

示例

<!-- 动态统计字段 -->
<select id="statUser" resultType="Integer">
  SELECT ${aggFunc}(#{field}) FROM user WHERE dept_id = #{deptId}
</select>

参数 aggFunc = "COUNT"、field = "id" 时,SQL 为:

SELECT COUNT(?) FROM user WHERE dept_id = ?  -- 预编译后填充参数

四、致命风险:${} 的 SQL 注入防护

${} 直接拼接字符串,若参数来自用户输入且未做校验,会导致严重的 SQL 注入攻击。

攻击示例:

假设存在以下查询(动态排序):

<select id="getUserList" resultType="User">
  SELECT * FROM user ORDER BY ${sortColumn}
</select>

恶意用户传入参数 sortColumn = "name; DROP TABLE user;",最终拼接的 SQL 为:

SELECT * FROM user ORDER BY name; DROP TABLE user;  -- 执行后删除 user 表!
安全防护措施(必须遵守)

使用 ${} 时,必须通过以下方式杜绝 SQL 注入:

1. 白名单校验(最核心)

对传入的参数进行 “合法值校验”,仅允许预设的白名单值通过。

示例(Java 代码校验)

// 合法的排序列名白名单
private static final List<String> VALID_SORT_COLUMNS = Arrays.asList("id", "name", "age", "create_time");
// 合法的排序方向白名单
private static final List<String> VALID_SORT_ORDER = Arrays.asList("ASC", "DESC");
public List<User> getUserList(String sortColumn, String sortOrder) {
  // 校验排序列名
  if (!VALID_SORT_COLUMNS.contains(sortColumn)) {
    throw new IllegalArgumentException("非法排序列名:" + sortColumn);
  }
  // 校验排序方向(忽略大小写)
  if (!VALID_SORT_ORDER.contains(sortOrder.toUpperCase())) {
    sortOrder = "ASC";  // 默认升序
  }
  return userMapper.getUserList(sortColumn, sortOrder);
}
2. 避免直接使用用户输入

尽量将 ${} 的参数来源限制为 “系统内部参数”(如枚举、配置文件),而非用户直接输入。例如:

// 用枚举限制排序列
public enum SortColumn {
  ID("id"), NAME("name"), AGE("age");
  private String column;
  // 构造器、getter 省略
}
// 调用时直接用枚举值
userMapper.getUserList(SortColumn.NAME.getColumn(), "DESC");
3. 特殊场景的参数过滤

若必须使用用户输入,需对参数进行字符过滤(如只允许字母、数字、下划线),禁止特殊字符(如 ;、'、OR、DROP 等)。

五、最佳实践总结

  1. 核心原则:能⽤ #{} ,坚决不用 ${}

90% 以上的业务场景(传递参数值)用 #{} ,仅在需要动态拼接 SQL 语法时用 ${}。

  1. ${} 使用三要素
    • 参数来源必须可信(或经过严格校验);
    • 必须做白名单校验,杜绝非法值;
    • 避免拼接用户输入的复杂字符串。
  1. 常见误区避坑
    • ❌ 不要用 #{} 拼接列名、表名(语法错误);
    • ❌ 不要用 ${} 传递查询条件(如 WHERE name = ${name}),存在注入风险;
    • ✅ 分表、排序、动态函数等场景,用 ${} 并配合白名单校验。
  1. 工具辅助
    • 开发时开启 MyBatis 日志(logImpl=SLF4J),观察最终生成的 SQL,验证参数拼接是否正确;
    • 上线前通过代码审计工具(如 SonarQube)检查 ${} 的使用是否存在未校验的用户输入。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值