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

相关推荐
终将老去的穷苦程序员18 分钟前
基于SpringBoot的餐饮管理系统
java·spring boot·后端
心之伊始20 分钟前
Spring AI Tool Calling 实战:让 Java Agent 调用本地 Bean 工具方法
java·spring boot·agent·spring ai·tool calling
AI人工智能+电脑小能手25 分钟前
【大白话说Java面试题 第110题】【并发篇】第10题:CAS 存在哪些问题?
java·开发语言·面试
瀚高PG实验室43 分钟前
java中间件无法连接数据库
java·数据库·中间件·瀚高数据库
东南门吹雪1 小时前
JAVA TCP socket编程框架
java·高并发·socket·tcp·nio
xingyuzhisuan1 小时前
缓存命中率提升方案:从 30% 优化至 82% 全流程优化记录
java·开发语言·缓存·ai
一条泥憨鱼1 小时前
Java开发效率神器:Lombok从入门到精通!
java·后端·学习·开发·lombok
Jinkxs1 小时前
Python基础 - 初识内置函数 Python自带的便捷工具
android·java·python
熠熠仔1 小时前
Spring Boot 与 MyBatis-Plus 空间几何数据集成指南
spring boot·后端·mybatis
奥利奥夹心脆芙1 小时前
零基础调试 Java 代码:Gemini 报错排查完整实操指南
java