MyBatis Mapper变量引用方式#{}与${}差别
前言
在 Java 开发中,SQL 注入是一个永远不能忽视的安全问题。每一个有经验的开发者都知道,用户输入的数据永远不能被信任——它可能包含恶意代码,试图绕过你的安全检查,直接操作你的数据库。
MyBatis 作为一款优秀的 ORM 框架,在设计之初就充分考虑了 SQL 注入的防护。默认情况下,MyBatis 会使用预编译的方式处理参数,并设置 PreparedStatement 参数,同时进行必要的安全检查和字符转义。
但在实际开发中,我们经常会在 Mapper 文件中看到两种变量引用方式:#{} 和 ${}。它们看起来很像,但背后的运行机制却截然不同。理解它们的区别,是每一个 MyBatis 使用者的必修课。

PreparedStatement 预编译(#{} 方式)
1. Mapper 配置
在 MyBatis 中,默认使用预编译方式。以下是一个典型的 Mapper 配置:
1 | <select id="getList" resultType="Map"> |
注意:这里 ORDER BY #{age} 的写法是有问题的。预编译参数不能用于 SQL 的结构部分(如 ORDER BY、GROUP BY、表名、列名等),只能用于值的部分。下面会详细解释。
2. SQL 执行过程
1 | select * from A where age = #{age} |
当参数 age 传入值为 5 时:
- MyBatis 首先将 SQL 解析为预编译语句:
select * from A where age = ? - 然后通过
PreparedStatement的setXxx()方法将值5绑定到占位符?上 - 最后在 SQL 的执行中进行值的替换
3. 为什么预编译能防止 SQL 注入?
预编译的核心原理是:SQL 语句的结构和执行计划的编译,与参数的绑定是两个完全独立的阶段。
1 | 阶段一:SQL 编译 |
即使用户传入的值为 "5; DROP TABLE A; --",数据库也会把它当作一个完整的字符串值来处理,而不是将其中的 DROP TABLE 识别为 SQL 命令。这就是预编译防止 SQL 注入的根本原因。
4. 预编译的优势
- 安全性:从根本上杜绝了 SQL 注入攻击。
- 性能:预编译语句的执行计划可以被数据库缓存,重复执行时可以直接复用,减少编译开销。
- 类型安全:
PreparedStatement会根据参数的 Java 类型自动选择合适的setXxx()方法,避免类型转换错误。
Statement 直接拼接(${} 方式)
1. Mapper 配置
如果需要在 Mapper 中使用 ${} 方式,通常需要配合 statementType="STATEMENT" 使用:
1 | <select id="getList" resultType="Map" statementType="STATEMENT"> |
2. SQL 执行过程
1 | select * from B where age = ${age} |
当参数 age 传入值为 5 时:
- MyBatis 直接将值拼接到 SQL 字符串中
- 解析后执行的 SQL:
select * from B where age = 5
3. ${} 的危险性
${} 方式的本质是字符串拼接,它不会进行任何参数化处理。这意味着:
- 如果用户传入
"5; DROP TABLE A; --",最终执行的 SQL 就会变成:1
select * from B where age = 5; DROP TABLE A; --
- 你的数据库可能瞬间被清空。

4. ${} 的合理使用场景
虽然 ${} 存在安全风险,但它并非一无是处。以下场景中,${} 是合理甚至必要的选择:
场景一:动态排序字段
1 | <select id="getList" resultType="Map"> |
这里的 orderByColumn 和 sortDirection 是 SQL 的结构部分,不能用 #{} 替代。但必须在代码层面对这两个参数进行严格的白名单校验。
场景二:动态表名
1 | <select id="getData" resultType="Map"> |
场景三:LIKE 模糊查询(部分场景)
1 | <select id="searchUsers" resultType="Map"> |
注意:LIKE 查询其实有更好的写法:
LIKE CONCAT('%', #{keyword}, '%'),推荐使用这种预编译的方式。
#{} 与 ${} 的详细对比
| 对比维度 | #{} | ${} |
|---|---|---|
| 处理方式 | PreparedStatement 预编译 | Statement 直接拼接 |
| SQL 注入防护 | 是(参数与 SQL 分离) | 否(直接拼接) |
| 执行性能 | 更好(执行计划可缓存) | 较差(每次重新编译) |
| 适用场景 | 参数值(WHERE 条件中的值) | SQL 结构部分(表名、列名、ORDER BY) |
| 字符串处理 | 自动加引号 | 原样拼接 |
| 类型安全 | 是(自动类型转换) | 否 |
一个生动的例子
假设我们有一个查询用户的方法:
1 | <!-- 使用 #{} - 安全 --> |
当 name 传入 "zhangsan" 时,执行的 SQL 是:
1 | select * from users where name = 'zhangsan' |
1 | <!-- 使用 ${} - 危险 --> |
当 name 传入 "zhangsan" 时,执行的 SQL 是:
1 | select * from users where name = zhangsan |
注意这里没有引号!如果 zhangsan 不是一个合法的列名或变量名,SQL 就会报错。如果用户恶意传入 "1=1 OR 1=1",执行的 SQL 就变成了:
1 | select * from users where name = 1=1 OR 1=1 |
这会返回所有用户的数据——这就是经典的 SQL 注入攻击。
最佳实践与安全建议
经过大量的实战经验,我总结了以下最佳实践:
1. 默认使用 #{}
能使用 #{} 的情况下就坚决不要使用 ${}。 这是铁律,没有例外。
2. 必须使用 ${} 时做好白名单校验
如果确实需要使用 ${}(比如动态排序),必须在代码层面做好严格的参数校验:
1 | public List<User> getUsers(String orderBy, String direction) { |
3. 使用 MyBatis 的 和 标签
对于需要动态拼接 SQL 的场景,优先使用 MyBatis 的动态 SQL 标签,而不是 ${}:
1 | <select id="searchUsers" resultType="User"> |
4. 开启 SQL 日志进行审计
在开发和测试环境中,开启 MyBatis 的 SQL 日志,方便排查问题和进行安全审计:
1 | <setting name="logImpl" value="STDOUT_LOGGING"/> |
总结
回到最初的那句话:SQL 存在注入的风险,MyBatis 默认使用预编译的方式并进行了必要的安全检查和字符转义。
${}方式会引发 SQL 注入的问题,同时会影响 SQL 语句的预编译,所以从安全性和性能的角度出发,能使用#{}的情况下就不要使用${}。- 可能需要直接插入一个不做任何修改的字符串到 SQL 语句中时(如动态表名、动态排序),使用
${}语法更加灵活方便,但是不能有外部 SQL 的进入,防止 SQL 注入。
一句话记住核心区别:
#{}是”值替换”,${}是”字符串拼接”。前者安全,后者灵活但有风险。在安全面前,灵活性永远要让步。





