一、核心概念
MyBatis 中的 #{} 和 ${} 都是参数占位符,但处理方式完全不同:
#{}:预编译占位符,使用PreparedStatement,参数会经过预编译处理${}:字符串替换占位符,直接进行文本替换,存在 SQL 注入风险
二、原理与区别
1. #{} 预编译占位符
原理:使用 JDBC 的 PreparedStatement 进行预编译:
<!-- MyBatis SQL -->
<select id="findUserById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
底层执行流程:
// 1. MyBatis 生成预编译 SQL
String sql = "SELECT * FROM user WHERE id = ?";
// 2. 创建 PreparedStatement
PreparedStatement ps = connection.prepareStatement(sql);
// 3. 设置参数(自动类型转换)
ps.setInt(1, 123); // 或 setString/setLong 等
// 4. 执行查询
ResultSet rs = ps.executeQuery();
特点:
- 参数类型安全:自动进行类型转换和校验
- 防止 SQL 注入:参数被视为纯数据,不会被解析为 SQL 语句
- 性能优化:数据库可缓存执行计划,多次执行同一 SQL 时性能更好
- 自动处理特殊字符:如单引号、双引号等会自动转义
2. ${} 字符串替换占位符
原理:直接进行字符串拼接:
<!-- MyBatis SQL -->
<select id="findUserByColumn" resultType="User">
SELECT * FROM user ORDER BY ${columnName}
</select>
底层执行流程:
// 假设传入参数 columnName = "name"
// 1. MyBatis 直接替换(字符串拼接)
String sql = "SELECT * FROM user ORDER BY " + columnName;
// 结果:SELECT * FROM user ORDER BY name
// 2. 创建 Statement(非预编译)
Statement stmt = connection.createStatement();
// 3. 执行 SQL
ResultSet rs = stmt.executeQuery(sql);
特点:
- 纯文本替换:类似于字符串拼接,参数直接嵌入 SQL
- 存在 SQL 注入风险:恶意参数可能改变 SQL 语义
- 无类型检查:不会进行类型转换
- 无法利用预编译缓存:每次生成的 SQL 可能不同
三、SQL 注入风险演示
危险示例(使用 ${})
<!-- ❌ 危险:使用 ${} 接收用户输入 -->
<select id="findUserByName" resultType="User">
SELECT * FROM user WHERE name = '${name}'
</select>
攻击场景:
// 正常输入
userMapper.findUserByName("张三");
// 生成 SQL: SELECT * FROM user WHERE name = '张三'
// 恶意输入(SQL 注入)
userMapper.findUserByName("' OR '1'='1");
// 生成 SQL: SELECT * FROM user WHERE name = '' OR '1'='1'
// 结果:返回所有用户数据!
// 更危险的攻击
userMapper.findUserByName("'; DROP TABLE user; --");
// 生成 SQL: SELECT * FROM user WHERE name = ''; DROP TABLE user; --'
// 结果:删除整个 user 表!
安全示例(使用 #{})
<!-- ✅ 安全:使用 #{} -->
<select id="findUserByName" resultType="User">
SELECT * FROM user WHERE name = #{name}
</select>
防注入效果:
// 恶意输入被当作普通字符串
userMapper.findUserByName("' OR '1'='1");
// 预编译 SQL: SELECT * FROM user WHERE name = ?
// 参数值: "' OR '1'='1"(整体作为字符串)
// 实际执行: SELECT * FROM user WHERE name = '\' OR \'1\'=\'1\''
// 结果:查不到数据,不会返回所有用户
四、使用场景
1. 必须使用 #{} 的场景(绝大多数)
<!-- ✅ WHERE 条件参数 -->
<select id="findUser" resultType="User">
SELECT * FROM user WHERE id = #{id} AND status = #{status}
</select>
<!-- ✅ INSERT 参数 -->
<insert id="insertUser">
INSERT INTO user (name, age, email)
VALUES (#{name}, #{age}, #{email})
</insert>
<!-- ✅ UPDATE 参数 -->
<update id="updateUser">
UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
</update>
2. 适合使用 ${} 的场景(需严格控制)
仅在以下场景使用,且参数必须来自可信来源(如代码常量、白名单):
<!-- ✅ 动态表名(参数来自代码常量) -->
<select id="selectFromTable" resultType="Map">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
<!-- ✅ 动态列名(参数来自预定义枚举) -->
<select id="selectByColumn" resultType="User">
SELECT * FROM user ORDER BY ${orderColumn} ${orderDirection}
</select>
<!-- ✅ IN 子句(MyBatis 3.5+ 推荐用 foreach) -->
<select id="findUserByIds" resultType="User">
SELECT * FROM user WHERE id IN (${ids})
</select>
安全使用示例:
// ❌ 危险:直接接收用户输入
String tableName = request.getParameter("tableName"); // 用户可输入任意值
mapper.selectFromTable(tableName);
// ✅ 安全:使用白名单验证
enum AllowedTables {
USER("user"), ORDER("order"), PRODUCT("product");
private final String tableName;
}
String userInput = request.getParameter("tableName");
AllowedTables table = AllowedTables.valueOf(userInput.toUpperCase());
mapper.selectFromTable(table.getTableName()); // 只允许预定义的表名
3. MyBatis 3.5+ 推荐的替代方案
对于动态 IN 子句,推荐使用 foreach:
<!-- ✅ 推荐:使用 foreach 替代 ${} -->
<select id="findUserByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
五、对比总结
| 对比项 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译(PreparedStatement) | 字符串替换(Statement) |
| SQL 注入 | ✅ 安全(自动转义) | ❌ 不安全(可被注入) |
| 类型处理 | ✅ 自动类型转换 | ❌ 无类型检查 |
| 性能 | ✅ 可利用预编译缓存 | ❌ 每次都需解析 SQL |
| 特殊字符 | ✅ 自动转义(如单引号) | ❌ 不转义 |
| 使用场景 | 所有参数值(WHERE/INSERT 等) | 动态 SQL 结构(表名/列名/关键字) |
| 推荐度 | ⭐⭐⭐⭐⭐(默认首选) | ⚠️ 谨慎使用(仅在必要时) |
六、面试总结
推荐回答思路:
- 先说核心区别:
#{}是预编译占位符,${}是字符串替换 - 强调 SQL 注入风险:
${}直接拼接,可能被注入;#{}参数化查询,安全 - 说明使用场景:
#{}:所有参数值(WHERE 条件、INSERT/UPDATE 的值)${}:动态 SQL 结构(表名、列名、ORDER BY),且需严格验证
- 补充原理:
#{}使用 PreparedStatement 预编译,${}使用 Statement 拼接
加分项:
- 能举例说明 SQL 注入攻击(如
' OR '1'='1) - 提到白名单验证、枚举限制等安全措施
- 了解
foreach等 MyBatis 动态 SQL 标签 - 知道
#{}底层使用PreparedStatement.setXxx()方法
核心记忆点:
能用
#{}就用#{},必须用${}时务必验证参数来源(白名单/枚举/常量)!