⭐ 掌握占位符 | 💡 理解SQL注入 | 🔥 面试必备知识
📖 前言
💭 「#{}和${}的区别,是MyBatis面试中最高频的问题之一」
在MyBatis开发中,我们经常会看到两种参数占位符:#{}和${}。它们看起来很相似,但实际上有着本质的区别。使用不当可能导致严重的SQL注入漏洞!
想象一下:
#{}就像一个安全门,会对参数进行严格检查和处理${}就像一个普通门,直接让参数通过,可能会有安全隐患
本文将从原理到实战,带你彻底理解这两种占位符的区别和使用场景。
🎯 学习目标:
- ✅ 深入理解#{}和${}的本质区别
- ✅ 掌握预编译和字符串替换的原理
- ✅ 了解SQL注入的危害和防范
- ✅ 学会正确选择使用场景
- ✅ 理解底层实现机制
- ✅ 应对面试高频考点
一、🎯 快速对比:#{}和${}的区别
1.1 核心区别一览表
| 对比项 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译(PreparedStatement) | 字符串替换(Statement) |
| SQL注入 | ✅ 安全(防止SQL注入) | ❌ 不安全(可能SQL注入) |
| 参数类型 | 作为参数传入 | 直接拼接到SQL |
| 引号 | 自动添加单引号 | 不添加引号 |
| 性能 | ✅ 高(可重用执行计划) | ❌ 低(每次重新编译) |
| 使用场景 | 传递参数值(推荐) | 动态表名、字段名 |
1.2 直观示例
java
/**
* 直观对比:#{}和${}的使用
*/
public class QuickComparison {
/**
* 使用#{}:安全的参数传递
*
* MyBatis配置:
* SELECT * FROM user WHERE name = #{name}
*
* 传入参数:name = "张三"
*
* 生成的SQL:
* SELECT * FROM user WHERE name = ?
*
* 执行时:
* PreparedStatement设置参数:setString(1, "张三")
* 最终SQL:SELECT * FROM user WHERE name = '张三'
*/
void exampleWithHashTag() {
// ✅ 安全:参数通过预编译传入
User user = userMapper.selectByName("张三");
}
/**
* 使用${}:字符串直接替换
*
* MyBatis配置:
* SELECT * FROM user WHERE name = '${name}'
*
* 传入参数:name = "张三"
*
* 生成的SQL:
* SELECT * FROM user WHERE name = '张三'
*
* 执行时:
* 直接执行SQL,没有参数绑定
*/
void exampleWithDollar() {
// ⚠️ 不安全:参数直接拼接到SQL
User user = userMapper.selectByName("张三");
}
}
二、🔐 深入理解:预编译vs字符串替换
2.1 什么是预编译?
💡 通俗解释:预编译就像提前准备好一个模板,需要时只填充数据
预编译的工作流程:
SQL模板
SELECT * FROM user WHERE id = ?
数据库预编译
生成执行计划
传入参数
id = 123
执行SQL
使用缓存的执行计划
详细代码示例:
java
/**
* 预编译原理演示
* 模拟MyBatis使用#{}的底层实现
*/
public class PreparedStatementDemo {
/**
* 使用PreparedStatement(预编译)
* 这就是#{}的底层实现方式
*/
public User selectByIdWithPrepared(Long id) throws SQLException {
// 1. 准备SQL模板(使用?作为占位符)
String sql = "SELECT * FROM user WHERE id = ?";
// 2. 创建PreparedStatement(预编译)
PreparedStatement pstmt = connection.prepareStatement(sql);
System.out.println("========== 预编译流程 ==========");
System.out.println("SQL模板: " + sql);
System.out.println("数据库会预编译这个SQL,生成执行计划");
// 3. 设置参数(参数绑定)
pstmt.setLong(1, id); // 第1个?的值设置为id
System.out.println("设置参数: id = " + id);
// 4. 执行查询(使用预编译的执行计划)
ResultSet rs = pstmt.executeQuery();
System.out.println("执行SQL(使用缓存的执行计划)");
// 5. 处理结果集
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setAge(rs.getInt("age"));
}
// 6. 关闭资源
rs.close();
pstmt.close();
return user;
}
/**
* 预编译的优点演示
*/
public void preparedStatementAdvantages() throws SQLException {
// 准备SQL模板(只需要一次)
String sql = "SELECT * FROM user WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
System.out.println("========== 预编译的优点 ==========");
// ===== 优点1:可以重复使用 =====
System.out.println("\n1. 可以重复使用同一个PreparedStatement:");
// 查询用户1
pstmt.setLong(1, 1L);
ResultSet rs1 = pstmt.executeQuery();
System.out.println("查询用户1(使用预编译的执行计划)");
rs1.close();
// 查询用户2(重用同一个PreparedStatement)
pstmt.setLong(1, 2L);
ResultSet rs2 = pstmt.executeQuery();
System.out.println("查询用户2(重用预编译的执行计划,性能更好)");
rs2.close();
// ===== 优点2:防止SQL注入 =====
System.out.println("\n2. 防止SQL注入:");
// 尝试注入恶意SQL
String maliciousInput = "1 OR 1=1";
pstmt.setString(1, maliciousInput);
System.out.println("恶意输入: " + maliciousInput);
System.out.println("PreparedStatement会将其作为普通字符串处理");
System.out.println("实际查询: WHERE id = '1 OR 1=1'");
System.out.println("不会执行OR 1=1,查询失败(安全)");
// ===== 优点3:自动处理特殊字符 =====
System.out.println("\n3. 自动处理特殊字符:");
String nameWithQuote = "O'Reilly";
PreparedStatement pstmt2 = connection.prepareStatement(
"SELECT * FROM user WHERE name = ?"
);
pstmt2.setString(1, nameWithQuote);
System.out.println("包含单引号的名字: " + nameWithQuote);
System.out.println("PreparedStatement会自动转义单引号");
System.out.println("实际查询: WHERE name = 'O''Reilly'");
pstmt.close();
pstmt2.close();
}
}
2.2 什么是字符串替换?
💡 通俗解释:字符串替换就像直接复制粘贴,没有任何检查
字符串替换的工作流程:
SQL字符串
SELECT * FROM user WHERE id = $
替换占位符
SELECT * FROM user WHERE id = 123
直接执行SQL
每次都重新编译
详细代码示例:
java
/**
* 字符串替换原理演示
* 模拟MyBatis使用${}的底层实现
*/
public class StatementDemo {
/**
* 使用Statement(字符串拼接)
* 这就是${}的底层实现方式
*/
public User selectByIdWithStatement(Long id) throws SQLException {
// 1. 直接拼接SQL字符串(危险!)
String sql = "SELECT * FROM user WHERE id = " + id;
System.out.println("========== 字符串替换流程 ==========");
System.out.println("拼接后的SQL: " + sql);
// 2. 创建Statement(不预编译)
Statement stmt = connection.createStatement();
// 3. 直接执行SQL
ResultSet rs = stmt.executeQuery(sql);
System.out.println("直接执行SQL(每次都重新编译)");
// 4. 处理结果集
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setAge(rs.getInt("age"));
}
// 5. 关闭资源
rs.close();
stmt.close();
return user;
}
/**
* 字符串替换的问题演示
*/
public void statementProblems() throws SQLException {
Statement stmt = connection.createStatement();
System.out.println("========== 字符串替换的问题 ==========");
// ===== 问题1:SQL注入风险 =====
System.out.println("\n1. SQL注入风险:");
// 正常输入
String normalInput = "1";
String sql1 = "SELECT * FROM user WHERE id = " + normalInput;
System.out.println("正常输入: " + normalInput);
System.out.println("生成SQL: " + sql1);
System.out.println("结果: 查询ID为1的用户");
// 恶意输入(SQL注入)
String maliciousInput = "1 OR 1=1";
String sql2 = "SELECT * FROM user WHERE id = " + maliciousInput;
System.out.println("\n恶意输入: " + maliciousInput);
System.out.println("生成SQL: " + sql2);
System.out.println("结果: 返回所有用户(SQL注入成功!)");
// ===== 问题2:特殊字符问题 =====
System.out.println("\n2. 特殊字符问题:");
String nameWithQuote = "O'Reilly";
String sql3 = "SELECT * FROM user WHERE name = '" + nameWithQuote + "'";
System.out.println("包含单引号的名字: " + nameWithQuote);
System.out.println("生成SQL: " + sql3);
System.out.println("结果: SQL语法错误!");
System.out.println("错误原因: WHERE name = 'O'Reilly'(单引号没有转义)");
// ===== 问题3:性能问题 =====
System.out.println("\n3. 性能问题:");
System.out.println("每次查询都需要重新编译SQL");
System.out.println("无法利用数据库的执行计划缓存");
System.out.println("性能较差,尤其是频繁查询时");
stmt.close();
}
}
2.3 两者对比实验
java
/**
* 预编译vs字符串替换:完整对比实验
*/
public class ComparisonExperiment {
/**
* 实验1:安全性对比
*/
public void securityComparison() {
System.out.println("========== 安全性对比实验 ==========\n");
// 测试输入(恶意SQL注入)
String maliciousId = "1 OR 1=1 --";
// ===== 使用#{}(预编译)=====
System.out.println("【使用#{}(预编译)】");
System.out.println("SQL模板: SELECT * FROM user WHERE id = ?");
System.out.println("参数: id = '" + maliciousId + "'");
System.out.println("实际执行: PreparedStatement.setString(1, '1 OR 1=1 --')");
System.out.println("结果: 查询失败,没有ID为'1 OR 1=1 --'的用户");
System.out.println("安全性: ✅ 安全(SQL注入被阻止)\n");
// ===== 使用${}(字符串替换)=====
System.out.println("【使用${}(字符串替换)】");
System.out.println("SQL模板: SELECT * FROM user WHERE id = ${id}");
System.out.println("参数: id = " + maliciousId);
System.out.println("字符串替换后: SELECT * FROM user WHERE id = 1 OR 1=1 --");
System.out.println("结果: 返回所有用户!");
System.out.println("安全性: ❌ 不安全(SQL注入成功)\n");
}
/**
* 实验2:性能对比
*/
public void performanceComparison() throws SQLException {
System.out.println("========== 性能对比实验 ==========\n");
int testCount = 1000; // 测试次数
// ===== 测试PreparedStatement =====
System.out.println("【测试PreparedStatement(#{})】");
long startTime1 = System.currentTimeMillis();
// 预编译一次,重复使用
String sql1 = "SELECT * FROM user WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql1);
for (int i = 1; i <= testCount; i++) {
pstmt.setLong(1, i);
ResultSet rs = pstmt.executeQuery();
rs.close();
}
pstmt.close();
long endTime1 = System.currentTimeMillis();
long time1 = endTime1 - startTime1;
System.out.println("执行" + testCount + "次查询");
System.out.println("SQL预编译: 1次");
System.out.println("耗时: " + time1 + "ms\n");
// ===== 测试Statement =====
System.out.println("【测试Statement(${})】");
long startTime2 = System.currentTimeMillis();
Statement stmt = connection.createStatement();
for (int i = 1; i <= testCount; i++) {
String sql2 = "SELECT * FROM user WHERE id = " + i;
ResultSet rs = stmt.executeQuery(sql2);
rs.close();
}
stmt.close();
long endTime2 = System.currentTimeMillis();
long time2 = endTime2 - startTime2;
System.out.println("执行" + testCount + "次查询");
System.out.println("SQL预编译: " + testCount + "次(每次都重新编译)");
System.out.println("耗时: " + time2 + "ms\n");
// ===== 对比结果 =====
System.out.println("【性能对比结果】");
System.out.println("PreparedStatement: " + time1 + "ms");
System.out.println("Statement: " + time2 + "ms");
System.out.println("性能提升: " + String.format("%.2f", (time2 - time1) * 100.0 / time2) + "%");
System.out.println("结论: PreparedStatement性能更好!");
}
/**
* 实验3:特殊字符处理
*/
public void specialCharacterHandling() {
System.out.println("========== 特殊字符处理实验 ==========\n");
// 测试数据(包含特殊字符)
String[] testNames = {
"O'Reilly", // 单引号
"Zhang\"San\"", // 双引号
"Li\\Si", // 反斜杠
"Wang%Wu", // 百分号
"Zhao_Liu" // 下划线
};
for (String name : testNames) {
System.out.println("测试名字: " + name);
// ===== 使用#{}(预编译)=====
System.out.println(" #{}方式:");
System.out.println(" SQL: SELECT * FROM user WHERE name = ?");
System.out.println(" 参数: name = '" + name + "'");
System.out.println(" 处理: 自动转义特殊字符");
System.out.println(" 结果: ✅ 正常执行");
// ===== 使用${}(字符串替换)=====
System.out.println(" ${}方式:");
System.out.println(" SQL: SELECT * FROM user WHERE name = '${name}'");
System.out.println(" 替换后: SELECT * FROM user WHERE name = '" + name + "'");
if (name.contains("'") || name.contains("\\")) {
System.out.println(" 结果: ❌ SQL语法错误(特殊字符未转义)");
} else {
System.out.println(" 结果: ⚠️ 可能正常,但不安全");
}
System.out.println();
}
}
}
三、⚠️ SQL注入详解
3.1 什么是SQL注入?
💡 通俗解释:SQL注入就像小偷在门锁上做手脚,绕过正常的验证机制
SQL注入的原理:
java
/**
* SQL注入原理演示
*/
public class SqlInjectionDemo {
/**
* 案例1:绕过登录验证
*/
public void loginBypassExample() {
System.out.println("========== SQL注入案例:绕过登录 ==========\n");
// 正常的登录SQL
String username = "admin";
String password = "123456";
System.out.println("【正常登录】");
System.out.println("用户名: " + username);
System.out.println("密码: " + password);
// 使用${}(不安全)
String sql1 = "SELECT * FROM user " +
"WHERE username = '" + username + "' " +
"AND password = '" + password + "'";
System.out.println("SQL: " + sql1);
System.out.println("结果: 验证成功\n");
// SQL注入攻击
String hackerUsername = "admin' OR '1'='1";
String hackerPassword = "anything";
System.out.println("【SQL注入攻击】");
System.out.println("用户名: " + hackerUsername);
System.out.println("密码: " + hackerPassword);
// 拼接后的SQL
String sql2 = "SELECT * FROM user " +
"WHERE username = '" + hackerUsername + "' " +
"AND password = '" + hackerPassword + "'";
System.out.println("SQL: " + sql2);
System.out.println("实际执行: ");
System.out.println(" SELECT * FROM user");
System.out.println(" WHERE username = 'admin' OR '1'='1'");
System.out.println(" AND password = 'anything'");
System.out.println("结果: 绕过密码验证,登录成功!(危险)\n");
// 使用#{}(安全)
System.out.println("【使用#{}防御】");
System.out.println("SQL模板: SELECT * FROM user WHERE username = ? AND password = ?");
System.out.println("参数1: username = 'admin' OR '1'='1'");
System.out.println("参数2: password = 'anything'");
System.out.println("结果: 查询失败(没有这样的用户名)");
System.out.println("安全性: ✅ SQL注入被阻止");
}
/**
* 案例2:删除所有数据
*/
public void deleteAllDataExample() {
System.out.println("\n========== SQL注入案例:删除所有数据 ==========\n");
// 根据ID删除用户
System.out.println("【正常删除】");
String normalId = "123";
String sql1 = "DELETE FROM user WHERE id = " + normalId;
System.out.println("参数: id = " + normalId);
System.out.println("SQL: " + sql1);
System.out.println("结果: 删除ID为123的用户\n");
// SQL注入攻击
System.out.println("【SQL注入攻击】");
String maliciousId = "123 OR 1=1";
String sql2 = "DELETE FROM user WHERE id = " + maliciousId;
System.out.println("参数: id = " + maliciousId);
System.out.println("SQL: " + sql2);
System.out.println("实际执行: DELETE FROM user WHERE id = 123 OR 1=1");
System.out.println("结果: 删除所有用户!(灾难)\n");
// 使用#{}(安全)
System.out.println("【使用#{}防御】");
System.out.println("SQL模板: DELETE FROM user WHERE id = ?");
System.out.println("参数: id = '123 OR 1=1'");
System.out.println("结果: 删除失败(没有ID为'123 OR 1=1'的用户)");
System.out.println("安全性: ✅ 数据被保护");
}
/**
* 案例3:窃取敏感数据
*/
public void dataleakExample() {
System.out.println("\n========== SQL注入案例:窃取敏感数据 ==========\n");
// 根据用户名查询用户信息
System.out.println("【正常查询】");
String normalName = "张三";
String sql1 = "SELECT * FROM user WHERE name = '" + normalName + "'";
System.out.println("参数: name = " + normalName);
System.out.println("SQL: " + sql1);
System.out.println("结果: 返回张三的信息\n");
// SQL注入攻击(UNION注入)
System.out.println("【SQL注入攻击(UNION注入)】");
String maliciousName = "张三' UNION SELECT username, password, NULL, NULL FROM admin --";
String sql2 = "SELECT * FROM user WHERE name = '" + maliciousName + "'";
System.out.println("参数: name = " + maliciousName);
System.out.println("SQL: " + sql2);
System.out.println("实际执行: ");
System.out.println(" SELECT * FROM user WHERE name = '张三'");
System.out.println(" UNION");
System.out.println(" SELECT username, password, NULL, NULL FROM admin --'");
System.out.println("结果: 同时返回用户信息和管理员账号密码!(严重泄露)\n");
// 使用#{}(安全)
System.out.println("【使用#{}防御】");
System.out.println("SQL模板: SELECT * FROM user WHERE name = ?");
System.out.println("参数: name = \"张三' UNION SELECT ...\"");
System.out.println("结果: 查询失败(没有这样的用户名)");
System.out.println("安全性: ✅ UNION注入被阻止");
}
}
3.2 常见的SQL注入攻击类型
java
/**
* 常见SQL注入攻击类型
*/
public class SqlInjectionTypes {
/**
* 类型1:基于布尔的盲注
*/
public void booleanBasedBlindInjection() {
System.out.println("========== 类型1:基于布尔的盲注 ==========\n");
// 攻击者通过观察页面响应判断SQL执行结果
String[] payloads = {
"1' AND 1=1 --", // 返回正常(真)
"1' AND 1=2 --", // 返回异常(假)
"1' AND (SELECT COUNT(*) FROM admin)>0 --" // 判断admin表是否存在
};
for (String payload : payloads) {
System.out.println("攻击载荷: " + payload);
String sql = "SELECT * FROM user WHERE id = '" + payload + "'";
System.out.println("SQL: " + sql);
System.out.println("攻击者根据页面响应推断信息\n");
}
}
/**
* 类型2:基于时间的盲注
*/
public void timeBasedBlindInjection() {
System.out.println("========== 类型2:基于时间的盲注 ==========\n");
// 攻击者通过SQL执行时间判断
String payload = "1' AND IF(1=1, SLEEP(5), 0) --";
System.out.println("攻击载荷: " + payload);
String sql = "SELECT * FROM user WHERE id = '" + payload + "'";
System.out.println("SQL: " + sql);
System.out.println("如果条件为真,SQL会执行5秒");
System.out.println("攻击者通过响应时间推断数据\n");
}
/**
* 类型3:堆叠查询注入
*/
public void stackedQueriesInjection() {
System.out.println("========== 类型3:堆叠查询注入 ==========\n");
// 执行多条SQL语句
String payload = "1'; DROP TABLE user; --";
System.out.println("攻击载荷: " + payload);
String sql = "SELECT * FROM user WHERE id = '" + payload + "'";
System.out.println("SQL: " + sql);
System.out.println("实际执行: ");
System.out.println(" SELECT * FROM user WHERE id = '1';");
System.out.println(" DROP TABLE user;");
System.out.println(" --'");
System.out.println("结果: 删除整个user表!(极其危险)\n");
}
}
3.3 如何防止SQL注入?
java
/**
* SQL注入防御措施
*/
public class SqlInjectionPrevention {
/**
* 方法1:使用预编译(推荐)
*/
public void usePrep aredStatement() {
System.out.println("========== 防御方法1:使用预编译 ==========\n");
System.out.println("【MyBatis配置】");
System.out.println("<select id=\"selectByName\" resultType=\"User\">");
System.out.println(" SELECT * FROM user WHERE name = #{name}");
System.out.println("</select>\n");
System.out.println("【原理】");
System.out.println("1. SQL模板: SELECT * FROM user WHERE name = ?");
System.out.println("2. 参数绑定: PreparedStatement.setString(1, name)");
System.out.println("3. 任何输入都作为字符串处理,不会改变SQL结构\n");
System.out.println("【效果】");
System.out.println("✅ 完全防止SQL注入");
System.out.println("✅ 自动处理特殊字符");
System.out.println("✅ 性能更好\n");
}
/**
* 方法2:输入验证
*/
public void inputValidation() {
System.out.println("========== 防御方法2:输入验证 ==========\n");
System.out.println("【验证策略】");
System.out.println("1. 白名单验证:只允许特定字符");
System.out.println("2. 类型检查:确保输入类型正确");
System.out.println("3. 长度限制:限制输入长度");
System.out.println("4. 正则表达式:验证格式\n");
System.out.println("【代码示例】");
System.out.println("```java");
System.out.println("public boolean validateInput(String input) {");
System.out.println(" // 只允许字母、数字、下划线");
System.out.println(" if (!input.matches(\"^[a-zA-Z0-9_]+$\")) {");
System.out.println(" return false;");
System.out.println(" }");
System.out.println(" // 限制长度");
System.out.println(" if (input.length() > 50) {");
System.out.println(" return false;");
System.out.println(" }");
System.out.println(" return true;");
System.out.println("}");
System.out.println("```\n");
}
/**
* 方法3:最小权限原则
*/
public void leastPrivilege() {
System.out.println("========== 防御方法3:最小权限原则 ==========\n");
System.out.println("【数据库权限设置】");
System.out.println("1. 应用程序使用专用数据库账号");
System.out.println("2. 只授予必要的权限(SELECT、INSERT、UPDATE)");
System.out.println("3. 禁止DROP、CREATE等危险权限");
System.out.println("4. 不同功能使用不同账号\n");
System.out.println("【效果】");
System.out.println("即使发生SQL注入,也无法执行危险操作");
System.out.println("例如:无法DROP TABLE、无法访问其他数据库\n");
}
/**
* 方法4:错误信息处理
*/
public void errorHandling() {
System.out.println("========== 防御方法4:错误信息处理 ==========\n");
System.out.println("【不要暴露详细错误】");
System.out.println("❌ 错误示例:");
System.out.println(" SQL Error: Table 'database.admin' doesn't exist");
System.out.println(" 暴露了数据库结构信息\n");
System.out.println("✅ 正确示例:");
System.out.println(" 系统繁忙,请稍后再试");
System.out.println(" 只给用户友好提示,不暴露内部信息\n");
System.out.println("【日志记录】");
System.out.println("1. 详细错误记录到日志文件");
System.out.println("2. 不在前端显示技术细节");
System.out.println("3. 监控异常SQL模式\n");
}
}
四、📋 使用场景详解
4.1 什么时候用#{}?
💡 原则:99%的情况应该使用#{}
xml
<!--
场景1:传递参数值(最常见)
推荐使用:#{}
-->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
<!--
场景2:WHERE条件查询
推荐使用:#{}
-->
<select id="selectByNameAndAge" resultType="User">
SELECT * FROM user
WHERE name = #{name} AND age = #{age}
</select>
<!--
场景3:INSERT插入数据
推荐使用:#{}
-->
<insert id="insertUser">
INSERT INTO user (name, age, email)
VALUES (#{name}, #{age}, #{email})
</insert>
<!--
场景4:UPDATE更新数据
推荐使用:#{}
-->
<update id="updateUser">
UPDATE user
SET name = #{name}, age = #{age}
WHERE id = #{id}
</update>
<!--
场景5:LIKE模糊查询
推荐使用:#{} + CONCAT函数
-->
<select id="selectByNameLike" resultType="User">
<!-- 方式1:CONCAT函数 -->
SELECT * FROM user WHERE name LIKE CONCAT('%', #{keyword}, '%')
<!-- 方式2:在Java代码中处理 -->
<!-- String keyword = "%" + input + "%"; -->
<!-- SELECT * FROM user WHERE name LIKE #{keyword} -->
</select>
<!--
场景6:IN查询(多个值)
推荐使用:#{} + foreach
-->
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
Java代码示例:
java
/**
* 使用#{}的场景示例
*/
@Mapper
public interface UserMapper {
/**
* 场景1:根据ID查询
* SQL: SELECT * FROM user WHERE id = ?
*/
User selectById(@Param("id") Long id);
/**
* 场景2:条件查询
* SQL: SELECT * FROM user WHERE name = ? AND age = ?
*/
User selectByNameAndAge(@Param("name") String name,
@Param("age") Integer age);
/**
* 场景3:插入数据
* SQL: INSERT INTO user (name, age) VALUES (?, ?)
*/
int insertUser(User user);
/**
* 场景4:更新数据
* SQL: UPDATE user SET name = ?, age = ? WHERE id = ?
*/
int updateUser(User user);
/**
* 场景5:模糊查询
* SQL: SELECT * FROM user WHERE name LIKE CONCAT('%', ?, '%')
*/
List<User> selectByNameLike(@Param("keyword") String keyword);
/**
* 场景6:IN查询
* SQL: SELECT * FROM user WHERE id IN (?, ?, ?)
*/
List<User> selectByIds(@Param("ids") List<Long> ids);
}
4.2 什么时候用${}?
⚠️ 警告:只在无法使用#{}的场景下使用${},并且要严格验证输入
xml
<!--
场景1:动态表名(无法用#{})
使用${},但必须验证输入
-->
<select id="selectFromTable" resultType="Map">
SELECT * FROM ${tableName}
WHERE id = #{id}
</select>
<!--
场景2:动态字段名(无法用#{})
使用${},但必须验证输入
-->
<select id="selectOrderBy" resultType="User">
SELECT * FROM user
ORDER BY ${orderColumn} ${orderDirection}
</select>
<!--
场景3:动态SQL片段(无法用#{})
使用${},但必须来自可信来源
-->
<select id="selectWithCondition" resultType="User">
SELECT * FROM user
WHERE 1=1 ${condition}
</select>
Java代码示例(包含安全验证):
java
/**
* 使用${}的场景示例
* ⚠️ 必须包含严格的输入验证
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 场景1:动态表名
* ⚠️ 必须验证表名是否合法
*/
public List<Map<String, Object>> selectFromTable(String tableName, Long id) {
// ===== 严格验证表名 =====
// 白名单验证
Set<String> allowedTables = new HashSet<>(Arrays.asList(
"user", "user_2023", "user_2024"
));
if (!allowedTables.contains(tableName)) {
throw new IllegalArgumentException("非法的表名: " + tableName);
}
// 调用Mapper
return userMapper.selectFromTable(tableName, id);
}
/**
* 场景2:动态排序
* ⚠️ 必须验证字段名和排序方向
*/
public List<User> selectOrderBy(String orderColumn, String orderDirection) {
// ===== 严格验证排序字段 =====
Set<String> allowedColumns = new HashSet<>(Arrays.asList(
"id", "name", "age", "create_time"
));
if (!allowedColumns.contains(orderColumn)) {
throw new IllegalArgumentException("非法的排序字段: " + orderColumn);
}
// ===== 严格验证排序方向 =====
if (!"ASC".equalsIgnoreCase(orderDirection) &&
!"DESC".equalsIgnoreCase(orderDirection)) {
throw new IllegalArgumentException("非法的排序方向: " + orderDirection);
}
// 调用Mapper
return userMapper.selectOrderBy(orderColumn, orderDirection);
}
/**
* 场景3:动态条件(不推荐)
* ⚠️ 尽量使用<if>、<choose>等标签代替
*/
public List<User> selectWithCondition(String condition) {
// ===== 严格验证条件 =====
// 最好的方式是不使用这种方式,改用MyBatis的动态SQL标签
// 如果必须使用,进行严格的白名单验证
Set<String> allowedConditions = new HashSet<>(Arrays.asList(
"AND status = 1",
"AND age > 18"
));
if (!allowedConditions.contains(condition)) {
throw new IllegalArgumentException("非法的查询条件");
}
return userMapper.selectWithCondition(condition);
}
}
更安全的替代方案:
xml
<!--
❌ 不推荐:使用${}动态排序
<select id="selectOrderBy" resultType="User">
SELECT * FROM user ORDER BY ${orderColumn} ${orderDirection}
</select>
-->
<!--
✅ 推荐:使用<choose>标签
-->
<select id="selectOrderBy" resultType="User">
SELECT * FROM user
ORDER BY
<choose>
<when test="orderColumn == 'name'">name</when>
<when test="orderColumn == 'age'">age</when>
<when test="orderColumn == 'createTime'">create_time</when>
<otherwise>id</otherwise>
</choose>
<choose>
<when test="orderDirection == 'DESC'">DESC</when>
<otherwise>ASC</otherwise>
</choose>
</select>
4.3 场景对比总结
| 场景 | #{}推荐度 | ${}可用性 | 原因 |
|---|---|---|---|
| 传递参数值 | ⭐⭐⭐⭐⭐ | ❌ | 安全,性能好 |
| WHERE条件 | ⭐⭐⭐⭐⭐ | ❌ | 防止SQL注入 |
| INSERT值 | ⭐⭐⭐⭐⭐ | ❌ | 安全,自动转义 |
| UPDATE值 | ⭐⭐⭐⭐⭐ | ❌ | 安全,自动转义 |
| LIKE查询 | ⭐⭐⭐⭐⭐ | ❌ | 使用CONCAT |
| IN查询 | ⭐⭐⭐⭐⭐ | ❌ | 使用foreach |
| 动态表名 | ❌ | ⚠️ | 必须白名单验证 |
| 动态字段名 | ❌ | ⚠️ | 必须白名单验证 |
| 动态排序 | ❌ | ⚠️ | 优先用 |
MyBatis占位符 - 底层原理与面试题精选
📌 说明:这是《MyBatis中#{}和${}完全指南》的补充篇,包含底层原理、实战技巧和高频面试题。
五、🔍 底层原理深入
5.1 MyBatis如何处理#{}?
java
/**
* MyBatis处理#{}的底层原理
* 基于源码分析
*/
public class HashTagProcessing {
/**
* 第1步:SQL解析
* MyBatis启动时解析XML配置
*/
public void step1_SqlParsing() {
System.out.println("========== 第1步:SQL解析 ==========\n");
// 原始SQL配置
String originalSql = "SELECT * FROM user WHERE name = #{name} AND age = #{age}";
System.out.println("原始SQL: " + originalSql);
// MyBatis解析后
String parsedSql = "SELECT * FROM user WHERE name = ? AND age = ?";
System.out.println("解析后SQL: " + parsedSql);
// 参数映射列表
System.out.println("参数映射:");
System.out.println(" ? 位置1 -> name参数");
System.out.println(" ? 位置2 -> age参数\n");
}
/**
* 第2步:创建PreparedStatement
*/
public void step2_CreatePreparedStatement() throws SQLException {
System.out.println("========== 第2步:创建PreparedStatement ==========\n");
String sql = "SELECT * FROM user WHERE name = ? AND age = ?";
// 创建PreparedStatement(预编译)
PreparedStatement pstmt = connection.prepareStatement(sql);
System.out.println("创建PreparedStatement: " + sql);
System.out.println("数据库预编译SQL,生成执行计划\n");
}
/**
* 第3步:参数绑定
*/
public void step3_ParameterBinding() throws SQLException {
System.out.println("========== 第3步:参数绑定 ==========\n");
PreparedStatement pstmt = connection.prepareStatement(
"SELECT * FROM user WHERE name = ? AND age = ?"
);
// MyBatis根据参数类型选择对应的setter方法
String name = "张三";
Integer age = 25;
System.out.println("绑定参数:");
// 位置1:name参数
pstmt.setString(1, name);
System.out.println(" 位置1: setString(1, '" + name + "')");
// 位置2:age参数
pstmt.setInt(2, age);
System.out.println(" 位置2: setInt(2, " + age + ")\n");
System.out.println("最终执行的SQL:");
System.out.println(" SELECT * FROM user WHERE name = '张三' AND age = 25");
System.out.println(" (单引号由PreparedStatement自动添加)\n");
}
/**
* 完整流程演示
*/
public void completeProcess() {
System.out.println("========== MyBatis处理#{}的完整流程 ==========\n");
System.out.println("【启动阶段】");
System.out.println("1. 扫描Mapper XML文件");
System.out.println("2. 解析SQL语句");
System.out.println(" 原始: SELECT * FROM user WHERE id = #{id}");
System.out.println(" 解析: SELECT * FROM user WHERE id = ?");
System.out.println("3. 创建MappedStatement对象");
System.out.println(" - 存储SQL模板");
System.out.println(" - 存储参数映射关系");
System.out.println(" - 存储结果映射关系\n");
System.out.println("【执行阶段】");
System.out.println("1. 根据Mapper方法找到MappedStatement");
System.out.println("2. 获取SQL模板: SELECT * FROM user WHERE id = ?");
System.out.println("3. 创建PreparedStatement(预编译)");
System.out.println("4. 根据参数映射绑定参数");
System.out.println(" - 参数类型: Long");
System.out.println(" - 调用: setLong(1, 123)");
System.out.println("5. 执行查询: executeQuery()");
System.out.println("6. 处理结果集\n");
}
}
源码级解析:
java
/**
* MyBatis核心类分析
*/
public class MyBatisSourceCodeAnalysis {
/**
* SqlSource接口:表示SQL来源
*/
interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
/**
* BoundSql类:包含最终的SQL和参数映射
*/
class BoundSql {
private String sql; // 最终SQL(?占位符)
private List<ParameterMapping> parameterMappings; // 参数映射列表
private Object parameterObject; // 参数对象
// getters and setters...
}
/**
* ParameterMapping类:参数映射信息
*/
class ParameterMapping {
private String property; // 参数名(如name、age)
private Class<?> javaType; // Java类型
private JdbcType jdbcType; // JDBC类型
private TypeHandler typeHandler; // 类型处理器
// getters and setters...
}
/**
* TypeHandler接口:类型转换器
*/
interface TypeHandler<T> {
// 设置参数
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType);
// 获取结果
T getResult(ResultSet rs, String columnName);
}
/**
* 示例:StringTypeHandler
*/
class StringTypeHandler implements TypeHandler<String> {
@Override
public void setParameter(PreparedStatement ps, int i,
String parameter, JdbcType jdbcType) {
// 调用PreparedStatement的setString方法
ps.setString(i, parameter);
}
@Override
public String getResult(ResultSet rs, String columnName) {
return rs.getString(columnName);
}
}
/**
* 执行流程模拟
*/
public void executionSimulation() {
System.out.println("========== MyBatis执行流程模拟 ==========\n");
// 1. 获取MappedStatement
System.out.println("【第1步】获取MappedStatement");
MappedStatement ms = configuration.getMappedStatement(
"com.example.mapper.UserMapper.selectById"
);
System.out.println(" SQL: " + ms.getBoundSql(null).getSql());
// 2. 获取BoundSql
System.out.println("\n【第2步】获取BoundSql");
Object params = 123L; // 参数
BoundSql boundSql = ms.getBoundSql(params);
System.out.println(" SQL: " + boundSql.getSql());
System.out.println(" 参数: " + params);
// 3. 创建PreparedStatement
System.out.println("\n【第3步】创建PreparedStatement");
PreparedStatement pstmt = connection.prepareStatement(boundSql.getSql());
System.out.println(" 预编译SQL");
// 4. 设置参数
System.out.println("\n【第4步】设置参数");
List<ParameterMapping> mappings = boundSql.getParameterMappings();
for (int i = 0; i < mappings.size(); i++) {
ParameterMapping mapping = mappings.get(i);
// 获取参数值
Object value = getParameterValue(params, mapping.getProperty());
// 获取TypeHandler
TypeHandler typeHandler = mapping.getTypeHandler();
// 设置参数
typeHandler.setParameter(pstmt, i + 1, value, mapping.getJdbcType());
System.out.println(" 位置" + (i+1) + ": " +
typeHandler.getClass().getSimpleName() +
".setParameter(" + value + ")");
}
// 5. 执行查询
System.out.println("\n【第5步】执行查询");
ResultSet rs = pstmt.executeQuery();
System.out.println(" executeQuery()");
// 6. 处理结果
System.out.println("\n【第6步】处理结果集");
System.out.println(" ResultSetHandler.handleResultSets()");
}
}
5.2 MyBatis如何处理${}?
java
/**
* MyBatis处理${}的底层原理
*/
public class DollarSignProcessing {
/**
* 第1步:SQL解析
*/
public void step1_SqlParsing() {
System.out.println("========== 第1步:SQL解析 ==========\n");
// 原始SQL配置
String originalSql = "SELECT * FROM ${tableName} WHERE id = #{id}";
System.out.println("原始SQL: " + originalSql);
// MyBatis识别${}和#{}
System.out.println("识别占位符:");
System.out.println(" ${tableName} -> 字符串替换");
System.out.println(" #{id} -> 参数绑定\n");
}
/**
* 第2步:字符串替换
*/
public void step2_StringSubstitution() {
System.out.println("========== 第2步:字符串替换 ==========\n");
// 参数
String tableName = "user";
Long id = 123L;
// 替换${}
String sql1 = "SELECT * FROM ${tableName} WHERE id = #{id}";
String sql2 = sql1.replace("${tableName}", tableName);
System.out.println("替换前: " + sql1);
System.out.println("替换后: " + sql2);
// 继续处理#{}
String sql3 = sql2.replace("#{id}", "?");
System.out.println("处理#{}后: " + sql3 + "\n");
}
/**
* 第3步:创建PreparedStatement
*/
public void step3_CreateStatement() throws SQLException {
System.out.println("========== 第3步:创建PreparedStatement ==========\n");
String sql = "SELECT * FROM user WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setLong(1, 123L);
System.out.println("最终SQL: " + sql);
System.out.println("参数绑定: setLong(1, 123)\n");
}
/**
* 完整流程演示
*/
public void completeProcess() {
System.out.println("========== MyBatis处理${}的完整流程 ==========\n");
System.out.println("【启动阶段】");
System.out.println("1. 扫描Mapper XML文件");
System.out.println("2. 解析SQL语句");
System.out.println(" 原始: SELECT * FROM ${tableName} WHERE id = #{id}");
System.out.println("3. 识别占位符类型");
System.out.println(" ${} -> 运行时字符串替换");
System.out.println(" #{} -> 预编译参数绑定\n");
System.out.println("【执行阶段】");
System.out.println("1. 获取参数: tableName='user', id=123");
System.out.println("2. 替换${}:");
System.out.println(" 替换前: SELECT * FROM ${tableName} WHERE id = #{id}");
System.out.println(" 替换后: SELECT * FROM user WHERE id = #{id}");
System.out.println("3. 处理#{}:");
System.out.println(" 处理后: SELECT * FROM user WHERE id = ?");
System.out.println("4. 创建PreparedStatement");
System.out.println("5. 绑定参数: setLong(1, 123)");
System.out.println("6. 执行查询\n");
System.out.println("【关键区别】");
System.out.println("${}: 在SQL解析时直接替换,没有预编译保护");
System.out.println("#{}: 使用?占位符,通过PreparedStatement绑定参数\n");
}
}
5.3 源码追踪
java
/**
* MyBatis关键源码位置
*/
public class SourceCodeLocation {
/**
* 1. SqlSourceBuilder类
* 负责解析SQL,将#{}转换为?
*/
public void sqlSourceBuilder() {
System.out.println("========== SqlSourceBuilder ==========\n");
System.out.println("位置: org.apache.ibatis.builder.SqlSourceBuilder");
System.out.println("核心方法: parse()");
System.out.println("\n功能:");
System.out.println("1. 解析SQL中的#{}");
System.out.println("2. 替换为?占位符");
System.out.println("3. 创建ParameterMapping");
System.out.println("\n示例代码:");
System.out.println("```java");
System.out.println("public SqlSource parse(String originalSql, ...) {");
System.out.println(" GenericTokenParser parser = new GenericTokenParser(");
System.out.println(" \"#{\", \"}\", ..."); // #{} 分隔符
System.out.println(" );");
System.out.println(" String sql = parser.parse(originalSql);");
System.out.println(" return new StaticSqlSource(configuration, sql, ...);");
System.out.println("}");
System.out.println("```\n");
}
/**
* 2. GenericTokenParser类
* 通用的占位符解析器
*/
public void genericTokenParser() {
System.out.println("========== GenericTokenParser ==========\n");
System.out.println("位置: org.apache.ibatis.parsing.GenericTokenParser");
System.out.println("核心方法: parse()");
System.out.println("\n功能:");
System.out.println("1. 解析任意形式的占位符");
System.out.println("2. #{}、${} 都由它处理");
System.out.println("3. 通过TokenHandler回调处理占位符内容");
System.out.println("\n处理逻辑:");
System.out.println("- 遇到 openToken(如#{ 或 ${)");
System.out.println("- 提取占位符内容");
System.out.println("- 调用TokenHandler.handleToken()");
System.out.println("- 返回处理结果\n");
}
/**
* 3. TextSqlNode类
* 处理${}的关键类
*/
public void textSqlNode() {
System.out.println("========== TextSqlNode ==========\n");
System.out.println("位置: org.apache.ibatis.scripting.xmltags.TextSqlNode");
System.out.println("核心方法: apply()");
System.out.println("\n功能:");
System.out.println("1. 在运行时替换${}");
System.out.println("2. 从参数对象中获取值");
System.out.println("3. 直接拼接到SQL字符串");
System.out.println("\n示例代码:");
System.out.println("```java");
System.out.println("public boolean apply(DynamicContext context) {");
System.out.println(" GenericTokenParser parser = new GenericTokenParser(");
System.out.println(" \"${\", \"}\", ..."); // ${} 分隔符
System.out.println(" );");
System.out.println(" // 直接替换${}为参数值");
System.out.println(" String sql = parser.parse(text);");
System.out.println(" context.appendSql(sql);");
System.out.println(" return true;");
System.out.println("}");
System.out.println("```\n");
}
/**
* 4. DefaultParameterHandler类
* 设置PreparedStatement参数
*/
public void defaultParameterHandler() {
System.out.println("========== DefaultParameterHandler ==========\n");
System.out.println("位置: org.apache.ibatis.scripting.defaults.DefaultParameterHandler");
System.out.println("核心方法: setParameters()");
System.out.println("\n功能:");
System.out.println("1. 遍历所有ParameterMapping");
System.out.println("2. 获取参数值");
System.out.println("3. 调用TypeHandler设置参数");
System.out.println("\n示例代码:");
System.out.println("```java");
System.out.println("public void setParameters(PreparedStatement ps) {");
System.out.println(" List<ParameterMapping> mappings = boundSql.getParameterMappings();");
System.out.println(" for (int i = 0; i < mappings.size(); i++) {");
System.out.println(" ParameterMapping mapping = mappings.get(i);");
System.out.println(" Object value = getParameterValue(mapping);");
System.out.println(" TypeHandler handler = mapping.getTypeHandler();");
System.out.println(" handler.setParameter(ps, i + 1, value, ...);");
System.out.println(" }");
System.out.println("}");
System.out.println("```\n");
}
}
六、📚 面试高频题
面试题1:#{}和${}的区别是什么?
点击查看答案
答案:
核心区别:
| 对比项 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译(PreparedStatement) | 字符串替换(拼接) |
| SQL注入 | ✅ 安全 | ❌ 不安全 |
| 参数处理 | 作为参数传入 | 直接拼接到SQL |
| 引号 | 自动添加 | 不添加 |
| 性能 | 高(可缓存执行计划) | 低(每次编译) |
详细解释:
- #{}:预编译方式
xml
<!-- MyBatis配置 -->
<select id="selectById">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 转换为 -->
<!-- SQL: SELECT * FROM user WHERE id = ? -->
<!-- 执行: PreparedStatement.setLong(1, 123) -->
- ${}:字符串替换
xml
<!-- MyBatis配置 -->
<select id="selectById">
SELECT * FROM user WHERE id = ${id}
</select>
<!-- 转换为 -->
<!-- SQL: SELECT * FROM user WHERE id = 123 -->
<!-- 执行: Statement.executeQuery() -->
为什么#{}更安全?
- 参数通过PreparedStatement绑定,不会改变SQL结构
- 自动处理特殊字符(如单引号)
- 防止SQL注入攻击
${}的风险示例:
java
// 用户输入
String id = "1 OR 1=1";
// 使用${}
// SQL: SELECT * FROM user WHERE id = 1 OR 1=1
// 结果: 返回所有用户(SQL注入)
// 使用#{}
// SQL: SELECT * FROM user WHERE id = ?
// 参数: "1 OR 1=1"(作为字符串)
// 结果: 查询失败(安全)
使用建议:
- 99%的情况用#{}
- 只在动态表名、字段名等无法用#{}的场景使用${}
- 使用${}时必须严格验证输入(白名单)
面试题2:什么是SQL注入?如何防止?
点击查看答案
答案:
什么是SQL注入?
SQL注入是一种攻击方式,攻击者通过在输入中插入恶意SQL代码,改变原本的SQL语句逻辑,从而:
- 绕过身份验证
- 获取敏感数据
- 修改或删除数据
- 执行管理员操作
典型案例:
java
/**
* 案例1:绕过登录
*/
// 正常SQL
String sql = "SELECT * FROM user WHERE username = '" + username + "' AND password = '" + password + "'";
// 正常输入
username = "admin"
password = "123456"
// SQL: SELECT * FROM user WHERE username = 'admin' AND password = '123456'
// 恶意输入(SQL注入)
username = "admin' OR '1'='1"
password = "anything"
// SQL: SELECT * FROM user WHERE username = 'admin' OR '1'='1' AND password = 'anything'
// 结果: 绕过密码验证,登录成功!
/**
* 案例2:删除数据
*/
// 正常SQL
String sql = "DELETE FROM user WHERE id = " + id;
// 恶意输入
id = "1 OR 1=1"
// SQL: DELETE FROM user WHERE id = 1 OR 1=1
// 结果: 删除所有用户!
防止SQL注入的方法:
1. 使用预编译(最重要)
java
// ✅ 正确:使用PreparedStatement
String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
// MyBatis中使用#{}
<select id="login">
SELECT * FROM user WHERE username = #{username} AND password = #{password}
</select>
2. 输入验证
java
// 白名单验证
public boolean validateInput(String input) {
// 只允许字母、数字、下划线
return input.matches("^[a-zA-Z0-9_]+$");
}
// 长度限制
if (input.length() > 50) {
throw new IllegalArgumentException("输入过长");
}
3. 最小权限原则
- 应用程序使用专用数据库账号
- 只授予必要的权限(SELECT、INSERT、UPDATE)
- 禁止DROP、CREATE等危险操作
4. 错误信息处理
java
// ❌ 不要暴露详细错误
catch (SQLException e) {
return "Error: " + e.getMessage(); // 暴露数据库信息
}
// ✅ 只返回友好提示
catch (SQLException e) {
log.error("Database error", e); // 记录到日志
return "系统繁忙,请稍后再试"; // 用户看到的
}
MyBatis防止SQL注入:
- 使用#{}而不是${}
- 动态表名、字段名使用白名单验证
- 结合Java代码进行输入验证
面试题3:什么时候必须用${}?
点击查看答案
答案:
虽然${}不安全,但有些场景确实无法使用#{}:
场景1:动态表名
xml
<!-- ❌ 不能用#{} -->
<select id="selectFromTable">
SELECT * FROM #{tableName} <!-- 错误!表名不能是参数 -->
WHERE id = #{id}
</select>
<!-- ✅ 必须用${} -->
<select id="selectFromTable">
SELECT * FROM ${tableName}
WHERE id = #{id}
</select>
java
// Java代码:严格验证表名
public List<User> selectFromTable(String tableName, Long id) {
// 白名单验证
Set<String> allowedTables = new HashSet<>(Arrays.asList(
"user", "user_2023", "user_2024"
));
if (!allowedTables.contains(tableName)) {
throw new IllegalArgumentException("非法的表名");
}
return userMapper.selectFromTable(tableName, id);
}
场景2:动态字段名
xml
<!-- 动态ORDER BY -->
<select id="selectOrderBy">
SELECT * FROM user
ORDER BY ${orderColumn} ${orderDirection}
</select>
java
// Java代码:严格验证字段名和排序方向
public List<User> selectOrderBy(String column, String direction) {
// 白名单验证字段名
Set<String> allowedColumns = new HashSet<>(Arrays.asList(
"id", "name", "age", "create_time"
));
if (!allowedColumns.contains(column)) {
throw new IllegalArgumentException("非法的排序字段");
}
// 验证排序方向
if (!"ASC".equalsIgnoreCase(direction) &&
!"DESC".equalsIgnoreCase(direction)) {
throw new IllegalArgumentException("非法的排序方向");
}
return userMapper.selectOrderBy(column, direction);
}
更好的替代方案:使用
xml
<!-- ✅ 推荐:使用MyBatis动态SQL -->
<select id="selectOrderBy">
SELECT * FROM user
ORDER BY
<choose>
<when test="orderColumn == 'name'">name</when>
<when test="orderColumn == 'age'">age</when>
<when test="orderColumn == 'createTime'">create_time</when>
<otherwise>id</otherwise>
</choose>
<choose>
<when test="orderDirection == 'DESC'">DESC</when>
<otherwise>ASC</otherwise>
</choose>
</select>
关键原则:
- 能用#{}就用#{}
- 必须用${}时,严格验证输入
- 优先考虑MyBatis动态SQL标签
- 使用白名单而不是黑名单
面试题4:MyBatis如何处理LIKE查询?
点击查看答案
答案:
LIKE查询需要特殊处理,有多种实现方式:
方式1:CONCAT函数(推荐)
xml
<select id="selectByNameLike">
SELECT * FROM user
WHERE name LIKE CONCAT('%', #{keyword}, '%')
</select>
优点:
- 安全(使用#{})
- 数据库层面处理
- 支持所有数据库(MySQL、Oracle等)
方式2:在Java代码中拼接
java
// Java代码
String keyword = "%" + input + "%";
List<User> users = userMapper.selectByNameLike(keyword);
xml
<select id="selectByNameLike">
SELECT * FROM user WHERE name LIKE #{keyword}
</select>
优点:
- 逻辑清晰
- 安全(使用#{})
缺点:
- 需要在Java代码中处理
方式3:bind标签
xml
<select id="selectByNameLike">
<bind name="pattern" value="'%' + keyword + '%'"/>
SELECT * FROM user WHERE name LIKE #{pattern}
</select>
优点:
- SQL中完成拼接
- 安全(使用#{})
❌ 错误方式:使用${}
xml
<!-- 不推荐!有SQL注入风险 -->
<select id="selectByNameLike">
SELECT * FROM user WHERE name LIKE '%${keyword}%'
</select>
风险示例:
java
// 恶意输入
String keyword = "%' OR '1'='1";
// 生成的SQL
SELECT * FROM user WHERE name LIKE '%%' OR '1'='1%'
// 结果:返回所有用户(SQL注入)
最佳实践:
- 首选CONCAT函数
- 或在Java代码中处理
- 绝不使用${}拼接LIKE
- 验证输入,限制特殊字符
面试题5:#{} 和 ${} 在性能上有什么区别?
点击查看答案
答案:
性能对比:
| 对比项 | #{} | ${} |
|---|---|---|
| SQL编译 | 只编译一次(预编译) | 每次都编译 |
| 执行计划 | 可缓存重用 | 无法缓存 |
| 网络传输 | 少(发送SQL模板+参数) | 多(发送完整SQL) |
| 内存占用 | 少 | 多(每次新SQL) |
| CPU消耗 | 少 | 多(重复编译) |
详细分析:
1. SQL编译次数
java
// 使用#{}(PreparedStatement)
PreparedStatement pstmt = connection.prepareStatement(
"SELECT * FROM user WHERE id = ?"
);
// 只编译一次
for (int i = 1; i <= 1000; i++) {
pstmt.setLong(1, i);
pstmt.executeQuery(); // 重用执行计划
}
// 使用${}(Statement)
Statement stmt = connection.createStatement();
// 每次都编译
for (int i = 1; i <= 1000; i++) {
String sql = "SELECT * FROM user WHERE id = " + i;
stmt.executeQuery(sql); // 每次重新编译
}
2. 数据库执行计划缓存
sql
-- 使用#{}
-- SQL1: SELECT * FROM user WHERE id = ?
-- SQL2: SELECT * FROM user WHERE id = ?
-- 两次SQL相同,使用同一个执行计划
-- 使用${}
-- SQL1: SELECT * FROM user WHERE id = 1
-- SQL2: SELECT * FROM user WHERE id = 2
-- 两次SQL不同,需要两个执行计划
3. 性能测试结果
java
// 测试:执行1000次查询
// 环境:MySQL 5.7,本地数据库
// 使用#{}
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 1000; i++) {
userMapper.selectByIdWithHashTag(i);
}
long time1 = System.currentTimeMillis() - startTime;
System.out.println("#{}耗时: " + time1 + "ms"); // 约500ms
// 使用${}
startTime = System.currentTimeMillis();
for (int i = 1; i <= 1000; i++) {
userMapper.selectByIdWithDollar(i);
}
long time2 = System.currentTimeMillis() - startTime;
System.out.println("${}耗时: " + time2 + "ms"); // 约800ms
// 性能提升
System.out.println("性能提升: " + ((time2 - time1) * 100 / time2) + "%"); // 约37.5%
4. 大数据量场景
java
// 场景:批量插入10000条数据
// 方式1:使用#{}(批处理)
String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
for (int i = 0; i < 10000; i++) {
pstmt.setString(1, "User" + i);
pstmt.setInt(2, 20 + i % 50);
pstmt.addBatch(); // 添加到批处理
if (i % 1000 == 0) {
pstmt.executeBatch(); // 每1000条执行一次
}
}
// 耗时:约2秒
// 方式2:使用${}(逐条执行)
Statement stmt = connection.createStatement();
for (int i = 0; i < 10000; i++) {
String sql = "INSERT INTO user (name, age) VALUES ('User" + i + "', " + (20 + i % 50) + ")";
stmt.executeUpdate(sql); // 每次都执行
}
// 耗时:约15秒
// 性能差距:7.5倍
结论:
- #{}性能更好(通常快30%-50%)
- 大数据量场景差距更明显
- ${}不仅不安全,性能也差
面试题6:MyBatis中如何处理IN查询?
点击查看答案
答案:
IN查询在MyBatis中有多种实现方式:
方式1:foreach标签(推荐)
xml
<select id="selectByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
生成的SQL:
sql
-- 输入:ids = [1, 2, 3]
SELECT * FROM user WHERE id IN (?, ?, ?)
-- 参数:1, 2, 3
Java代码:
java
List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<User> users = userMapper.selectByIds(ids);
foreach属性说明:
collection:集合参数名item:循环变量名open:开始字符close:结束字符separator:分隔符
方式2:使用@Param注解
java
// Mapper接口
List<User> selectByIds(@Param("ids") List<Long> ids);
xml
<select id="selectByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</select>
方式3:动态SQL(处理空集合)
xml
<select id="selectByIds" resultType="User">
SELECT * FROM user
<where>
<if test="ids != null and ids.size() > 0">
id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
处理逻辑:
- 如果ids为空,不添加WHERE条件
- 避免生成错误的SQL:
WHERE id IN ()
❌ 错误方式:使用${}
xml
<!-- 不推荐!有SQL注入风险 -->
<select id="selectByIds" resultType="User">
SELECT * FROM user WHERE id IN (${ids})
</select>
java
// Java代码
String ids = "1, 2, 3"; // 手动拼接
List<User> users = userMapper.selectByIds(ids);
// 恶意输入
String maliciousIds = "1, 2, 3) OR 1=1 --";
// 生成的SQL:SELECT * FROM user WHERE id IN (1, 2, 3) OR 1=1 --)
// 结果:返回所有用户(SQL注入)
大数据量处理:
java
/**
* IN查询数量过多的处理
* 例如:1000个ID
*/
public List<User> selectByManyIds(List<Long> allIds) {
// 分批查询(每批500个)
int batchSize = 500;
List<User> result = new ArrayList<>();
for (int i = 0; i < allIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, allIds.size());
List<Long> batchIds = allIds.subList(i, end);
List<User> batchResult = userMapper.selectByIds(batchIds);
result.addAll(batchResult);
}
return result;
}
最佳实践:
- 使用foreach + #{}
- 处理空集合情况
- 大数据量时分批查询
- 绝不使用${}拼接
七、📝 总结
核心知识点回顾
#{}和${}的本质区别:
| 特性 | #{} | ${} |
|---|---|---|
| 实现方式 | PreparedStatement(预编译) | String拼接 |
| SQL注入 | ✅ 安全 | ❌ 危险 |
| 性能 | ✅ 高 | ❌ 低 |
| 使用场景 | 参数值(99%场景) | 动态表名/字段名(1%场景) |
使用原则:
- 默认使用#{}
- 必须用${}时严格验证
- 永远不要直接拼接用户输入
- 使用白名单而不是黑名单
安全建议:
- 参数传递:用#{}
- LIKE查询:用CONCAT + #{}
- IN查询:用foreach + #{}
- 动态表名:用白名单验证 + ${}
- 动态排序:优先用
学习建议
- 理解原理:掌握预编译vs字符串替换
- 实践验证:自己编写测试代码
- 安全意识:时刻警惕SQL注入
- 源码学习:阅读MyBatis源码加深理解
🎉 恭喜你完成学习!
现在你已经完全掌握了MyBatis中#{}和${}的使用!
💪 记住:安全第一,性能第二!
📧 有问题欢迎交流 | ⭐ 觉得有用请点赞分享