一、引言:为什么会有两种参数传递方式?
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 等)。
五、最佳实践总结
- 核心原则:能⽤ #{} ,坚决不用 ${}
90% 以上的业务场景(传递参数值)用 #{} ,仅在需要动态拼接 SQL 语法时用 ${}。
- ${} 使用三要素:
-
- 参数来源必须可信(或经过严格校验);
- 必须做白名单校验,杜绝非法值;
- 避免拼接用户输入的复杂字符串。
- 常见误区避坑:
-
- ❌ 不要用 #{} 拼接列名、表名(语法错误);
- ❌ 不要用 ${} 传递查询条件(如 WHERE name = ${name}),存在注入风险;
- ✅ 分表、排序、动态函数等场景,用 ${} 并配合白名单校验。
- 工具辅助:
-
- 开发时开启 MyBatis 日志(logImpl=SLF4J),观察最终生成的 SQL,验证参数拼接是否正确;
- 上线前通过代码审计工具(如 SonarQube)检查 ${} 的使用是否存在未校验的用户输入。
980

被折叠的 条评论
为什么被折叠?



