#{} 和 ${} 的区别?
结论先行:#{} 是 PreparedStatement 的占位符,走预编译、参数绑定,能防 SQL 注入;${} 是字符串直接拼接,在 SQL 解析阶段就把值原样塞进语句,只该用于表名、列名、排序字段这类不能参数化的位置,且必须做白名单校验。
1. 编译期发生了什么
#{} : SELECT * FROM t_user WHERE name = ?
└─ 预编译 SQL 骨架,值通过 setString 绑定,数据库按「值」处理
${} : SELECT * FROM t_user WHERE name = 'tom'
└─ 先做字符串替换,再把整条 SQL 交给数据库解析,值变成了「语法的一部分」
差别不在性能,而在语义:参数绑定后,值永远不会被解析成 SQL 关键字;字符串拼接则会把恶意输入当成 SQL 执行。
2. 注入是怎么发生的
<!-- 危险写法 -->
<select id="login" resultType="User">
SELECT * FROM t_user WHERE name = '${name}' AND pwd = '${pwd}'
</select>
传入 name = ' OR '1'='1,最终 SQL 变成:
SELECT * FROM t_user WHERE name = '' OR '1'='1' AND pwd = ''
条件恒真,任意账号都能登录。改成 #{} 后,' OR '1'='1 只会被当作一个普通字符串去匹配 name,查不到任何数据。
3. 对比表
| 维度 | #{} | ${} |
|---|---|---|
| 底层实现 | PreparedStatement + ? 占位 | 字符串替换后拼进 SQL |
| 是否预编译 | 是,SQL 骨架可复用执行计划 | 否,每次都是新 SQL |
| SQL 注入 | 免疫 | 有风险,取决于输入 |
| 类型处理 | 由 TypeHandler 处理,自动加引号/转义 | 原样输出,需自己加引号 |
| 可用于表名/列名 | 不可以 | 可以(这也是它存在的理由) |
| 常见用途 | 所有查询条件、插入值 | 动态表名、动态排序、IN 之外的动态列 |
4. 正确使用 ${} 的方式
排序字段是最典型的场景,因为 ORDER BY ? 不合法。必须用枚举或白名单限定。
public enum SortField {
ID("id"), AMOUNT("amount"), CREATED_AT("created_at");
private final String column;
SortField(String column) { this.column = column; }
public String getColumn() { return column; }
}
<select id="page" resultType="Order">
SELECT id, amount, created_at
FROM t_order
<where>
<if test="status != null">status = #{status}</if>
</where>
ORDER BY ${sortColumn} ${sortOrder}
LIMIT #{offset}, #{size}
</select>
// Service 层:只放行白名单内的值,绝不直接透传前端参数
SortField field = SortField.valueOf(sort.toUpperCase());
String order = "asc".equalsIgnoreCase(dir) ? "ASC" : "DESC";
mapper.page(status, field.getColumn(), order, offset, size);
如果非要动态表名(例如按月分表 t_order_202611),表名同样要来自服务端生成的枚举,而不是请求参数。
5. 常见的模糊查询写法
<!-- 正确:值用 #{},通配符在 Java 侧或 SQL 侧拼接 -->
<select id="search" resultType="User">
SELECT id, name FROM t_user
WHERE name LIKE CONCAT('%', #{keyword}, '%')
</select>
注意不要写成 LIKE '%${keyword}%',那样 ${} 原样插入,既可能注入,也会因引号处理出错。
6. 常见追问
追问一:#{} 一定安全吗?
对「值」安全。但如果把它用在不能参数化的位置(表名、列名、ORDER BY),SQL 直接报语法错误,所以那些位置只能靠 ${} 加白名单,安全性由校验保证,不是由 #{} 保证。
追问二:${} 会影响预编译缓存吗?
会。每条不同的拼接结果都是一条全新的 SQL,数据库要重新硬解析、生成执行计划,高并发下既浪费 CPU 又容易把共享池打满。
追问三:#{} 里的 javaType、jdbcType 什么时候要写?
当参数为 null 且数据库驱动无法推断类型时会报错(典型是 Oracle 的 ORA-00932、MyBatis 的 JdbcType OTHER 异常)。此时写 #{amount, jdbcType=DECIMAL} 显式指定。MySQL 一般不需要。
追问四:注解方式怎么写?
@Select("SELECT * FROM t_order ORDER BY ${column}") 同样适用,规则不变:${} 只接白名单值。
小结
记住一句话:值用 #{},标识符用 ${} 且必须白名单。${} 不是「性能更好的 #{}」,它是另一种语义——拼接代码而不是传递数据,用错位置就是注入漏洞。