【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 注入漏洞。

相关推荐
步行cgn5 小时前
Spring c 命名空间注入详解
java·后端·spring
明月_清风6 小时前
Maven 到底是什么?一篇文章搞懂 Java 项目构建与依赖管理
java·后端·maven
aramae6 小时前
MySQL复合查询(8)
java·c语言·开发语言·后端·算法
Rain的Java大神之路7 小时前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog7 小时前
Java 接入Redis: 通用命令与键管理
java·服务器·redis
Wang's Blog7 小时前
Java 接入Redis: 列表集合与有序集合操作命令
java·服务器·redis
Ivanqhz8 小时前
SVD++算法
java·服务器·网络·深度学习·神经网络
qq_2518364578 小时前
springboot vue3 开发实现 拼豆管理系统
java·开发语言·ai编程
caoerzhong9 小时前
JeeWMS 开源仓库管理系统全景解读:一套 Java WMS 如何把 WMS/OMS/BMS/TMS 装进同一个系统
java·开源
曹牧9 小时前
Java集合框架:List、ArrayList、Map 和 HashMap
java