本质差异
| 对比维度 | #{} |
${} |
|---|---|---|
| 处理方式 | JDBC 预编译参数占位符 ? |
SQL 字符串直接拼接 |
| SQL 注入 | ✅ 安全,自动转义 | ❌ 不安全,不做任何处理 |
| 类型处理 | MyBatis 自动推断 JDBC 类型 | 无类型概念,纯字符串替换 |
| 适用场景 | 列值(WHERE/VALUES/SET) | 动态表名/列名/ORDER BY |
| 编译阶段 | 先编译 SQL → 再设参数 | 拼接完才编译 SQL |
#{}------预编译占位符
MyBatis 将 #{} 替换为 JDBC 的 ?,再通过 PreparedStatement.setXXX() 设置参数值。整个过程由 JDBC 驱动负责转义:
java
// Mapper 接口
@Select("SELECT * FROM user_info WHERE username = #{username}")
List<UserInfo> selectByUsername(String username);
实际执行的 SQL(MyBatis 日志输出):
sql
-- 预编译阶段:? 占位
SELECT * FROM user_info WHERE username = ?
-- 执行阶段:传入参数 "zhangsan"
SELECT * FROM user_info WHERE username = 'zhangsan'
即使传入恶意字符串,也会被完整转义为字面值,不会破坏 SQL 结构:
java
// 假设用户输入:' OR 1=1 --
selectByUsername("' OR 1=1 --");
sql
-- 实际执行的 SQL(? 机制保证了安全)
SELECT * FROM user_info WHERE username = '\' OR 1=1 --'
-- ↑ 整个字符串被当作一个字面值,单引号被转义
-- 结果:查不到任何数据,注入失败
${}------字符串直接拼接
MyBatis 在构建 SQL 字符串阶段 就将 ${} 替换为参数的字面值,然后再交给 JDBC 编译执行。期间不做任何转义:
java
// ❌ 危险写法:用 ${} 拼接 WHERE 条件值
@Select("SELECT * FROM user_info WHERE username = '${username}'")
List<UserInfo> selectByUsername(String username);
实际执行的 SQL(MyBatis 日志输出):
sql
-- 传入 "zhangsan" → 正常
SELECT * FROM user_info WHERE username = 'zhangsan'
-- 传入 "' OR 1=1 --" → SQL 注入成功
SELECT * FROM user_info WHERE username = '' OR 1=1 --'
-- ↑ 闭合了前面的引号,注入 OR 1=1
-- 结果:返回全表数据!
${} 的正确使用场景
虽然 ${} 有注入风险,但在以下场景中必须使用 ,因为 JDBC 的 ? 占位符不能替代 SQL 语法结构:
动态排序字段
java
// ORDER BY 不能用 ? 占位符,只能用 ${}
@Select("SELECT * FROM user_info ORDER BY ${orderColumn} ${orderDirection}")
List<UserInfo> selectAllOrdered(
@Param("orderColumn") String orderColumn, // 如 "create_time"
@Param("orderDirection") String orderDirection); // 如 "DESC"
⚠️ 安全措施 :必须在业务层白名单校验
orderColumn和orderDirection,禁止直接传入用户输入。
动态表名
java
// 表名不能用 ? 占位符,只能用 ${}
@Select("SELECT * FROM ${tableName} WHERE id = #{id}")
UserInfo selectByIdFromTable(
@Param("tableName") String tableName,
@Param("id") Integer id);
LIKE 模糊查询
当使用 ${} 拼接 LIKE 模式时需要注意注入风险,推荐用 #{} + CONCAT:
java
// ✅ 推荐:用 #{} + CONCAT,安全
@Select("SELECT * FROM user_info WHERE username LIKE CONCAT('%', #{keyword}, '%')")
List<UserInfo> selectByKeyword(@Param("keyword") String keyword);
// ❌ 不推荐:用 ${} 直接拼接,有注入风险
@Select("SELECT * FROM user_info WHERE username LIKE '%${keyword}%'")
List<UserInfo> selectByKeyword(@Param("keyword") String keyword);
底层实现对比
#{} 的处理流程:
解析 XML/注解 → 将 #{name} 替换为 ? → 生成 BoundSql(含 ? )→
PreparedStatement.setString(1, "zhangsan") → 执行
${} 的处理流程:
解析 XML/注解 → 将 ${name} 替换为字面值 "zhangsan" →
生成 BoundSql(字符串已拼入)→ PreparedStatement → 执行
#{} 和 ${} 的替换发生在 MyBatis 不同的处理阶段:
${}替换 :发生在SqlSource解析阶段(DynamicSqlSource/TextSqlNode),此时 SQL 还是字符串,直接做String.replace()。#{}替换 :发生在SqlSource→BoundSql转换阶段(GenericTokenParser),此时生成?占位符并把参数信息存入ParameterMapping列表。
速查总结
| 场景 | 使用 | 示例 |
|---|---|---|
| WHERE 条件值 | #{} |
WHERE id = #{id} |
| INSERT / UPDATE 值 | #{} |
VALUES (#{username}, #{age}) |
| LIKE 模糊匹配 | #{} + CONCAT |
LIKE CONCAT('%', #{kw}, '%') |
| 动态排序字段 | ${} + 白名单 |
ORDER BY ${col} |
| 动态表名 | ${} + 白名单 |
FROM ${table} |
| 动态列名 | ${} + 白名单 |
SELECT ${column} |
| IN 查询 | #{} + foreach |
WHERE id IN #{ids} |
黄金法则 :凡是传给
${}的值来自用户输入,必须做白名单校验,否则就是 SQL 注入漏洞。