#{} 和 ${} 的区别?

#Java#面试八股

#{} 和 ${} 的区别?

结论先行:#{} 是 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}") 同样适用,规则不变:${} 只接白名单值。

小结

记住一句话:值用 #{},标识符用 ${} 且必须白名单。${} 不是「性能更好的 #{}」,它是另一种语义——拼接代码而不是传递数据,用错位置就是注入漏洞。

笔记加载中…