【MyBatis】${}与 #{}的区别

本质差异

对比维度 #{} ${}
处理方式 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"

⚠️ 安全措施 :必须在业务层白名单校验 orderColumnorderDirection,禁止直接传入用户输入。

动态表名

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 不同的处理阶段

  1. ${} 替换 :发生在 SqlSource 解析阶段(DynamicSqlSource / TextSqlNode),此时 SQL 还是字符串,直接做 String.replace()
  2. #{} 替换 :发生在 SqlSourceBoundSql 转换阶段(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 注入漏洞。

相关推荐
karry_k12 小时前
MyBatis批量insert-select踩坑:useGeneratedKeys=true 可能让PostgreSQL返回大量插入结果
java·后端
karry_k12 小时前
PostgreSQL 在 MyBatis 中执行正常 SQL 失效:一次 DELETE USING 踩坑记录
java·后端
SamDeepThinking15 小时前
从源码到代码:MyBatis-Flex 与 MyBatis-Plus 的逐项对比
java·后端·程序员
她的男孩18 小时前
Spring Boot 接 Flowable 工作流:用 3 个注解搭一个请假审批流程
java·后端·架构
荣码20 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
plainGeekDev21 小时前
Gson → kotlinx.serialization
android·java·kotlin
小bo波1 天前
Java Swing 图形用户界面实验 —— 从算术练习到游戏开发的完整实践
java·课程设计·gui·游戏开发·扫雷·swing
咖啡八杯1 天前
GoF设计模式——备忘录模式
java·后端·spring·设计模式