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

相关推荐
维天说4 分钟前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane199441 分钟前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
噢,我明白了2 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品2 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘2 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记2 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
糖果店的幽灵2 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph
极光代码工作室3 小时前
基于SpringBoot的在线博客系统
java·springboot·web开发·后端开发
我是唐青枫4 小时前
Java Spring Security 实战详解:从登录认证到 JWT 权限控制
java·spring
ihuyigui4 小时前
海外签收通知短信接口
android·java·开发语言·前端·数据库·后端