什么是占位符?
在 Java 中,当我们需要执行一条带有动态数据的 SQL 语句时(根据用户输入的用户名查询信息),我们不能直接将变量拼接在 SQL 字符串里,这样做不仅代码难看,而且极其危险(SQL 注入)。

占位符就是 SQL 语句中的一个特殊标记,它代表一个“占位”,等待后续用实际的变量值来填充,它像一个模具,我们先用它定义好 SQL 的结构,然后再“注入”具体的数据。
在 Java 中,最常用的占位符有两种,它们分别对应不同的数据库 API:
- (问号):用于 JDBC (Java Database Connectivity),这是 Java 访问数据库的官方标准 API。
- (井号加花括号):用于 MyBatis 等持久层框架,它是对 JDBC 的一种高级封装。
JDBC 中的占位符
这是最基础、最核心的用法,当你使用原生 JDBC 时, 是你唯一的选择。
工作原理
- 编写 SQL 模板:在 SQL 字符串中,用 来表示需要动态替换的部分。
- 创建
PreparedStatement:使用Connection对象的prepareStatement()方法,传入 SQL 模板,这会创建一个PreparedStatement对象,数据库会预先编译这个 SQL 模板。 - 设置参数:通过
PreparedStatement的setXxx()方法(如setString(),setInt()等),按照 的顺序(从 1 开始)将实际的值绑定到占位符上。 - 执行 SQL:调用
executeQuery()或executeUpdate()方法来执行 SQL,数据库会使用你提供的值来执行预编译好的语句。
代码示例
假设我们有一个 users 表,结构如下:
id (INT), username (VARCHAR), password (VARCHAR)

目标:根据用户名 username 查询用户信息。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcPlaceholderExample {
// 数据库连接信息 (请替换为你自己的)
private static final String DB_URL = "jdbc:mysql://localhost:3306/your_database";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
String usernameToFind = "john_doe"; // 这是用户输入的动态值
// 1. 编写 SQL 模板,使用 ? 作为占位符
String sql = "SELECT id, username, password FROM users WHERE username = ?";
// try-with-resources 语句,确保资源自动关闭
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 2. 设置参数,将 usernameToFind 的值绑定到第一个 ? 上
// 参数索引从 1 开始
pstmt.setString(1, usernameToFind);
// 3. 执行查询
ResultSet rs = pstmt.executeQuery();
// 处理结果集
if (rs.next()) {
int id = rs.getInt("id");
String username = rs.getString("username");
String password = rs.getString("password");
System.out.println("找到用户: ID=" + id + ", 用户名=" + username + ", 密码=" + password);
} else {
System.out.println("未找到用户名为 '" + usernameToFind + "' 的用户。");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
为什么必须使用 而不是字符串拼接?
危险示例:字符串拼接
// !!! 危险的代码,绝对不要这样做 !!! String userInput = "admin' OR '1'='1"; // 恶意的用户输入 String sql = "SELECT * FROM users WHERE username = '" + userInput + "'"; // 最终的 SQL 变成了: SELECT * FROM users WHERE username = 'admin' OR '1'='1' // 这个条件永远为真,导致查询出所有用户!这就是 SQL 注入攻击。
安全示例:使用
String userInput = "admin' OR '1'='1"; // 恶意的用户输入 String sql = "SELECT * FROM users WHERE username = ?"; // ... pstmt.setString(1, userInput); // 最终发送给数据库的 SQL 始终是: SELECT * FROM users WHERE username = 'admin\' OR \'1\'=\'1' // 数据库会将整个字符串 "admin' OR '1'='1" 当作一个普通的用户名去精确匹配,而不是作为 SQL 代码执行。 // 因为 SQL 语句的结构已经被预编译和固定了。
的优点:

- 防止 SQL 注入:这是最重要的优点。
PreparedStatement会将参数值作为字面量处理,而不是 SQL 代码的一部分。 - 性能提升:对于需要多次执行的 SQL(只是参数不同),数据库可以缓存预编译的 SQL 语句,提高执行效率。
- 代码清晰:将 SQL 逻辑和数据绑定逻辑分离,使代码更易读和维护。
MyBatis 中的占位符 和
MyBatis 是一个流行的持久层框架,它简化了 JDBC 操作,MyBatis 提供了两种占位符,它们的作用有本质区别。
(参数占位符)
这是 MyBatis 中推荐使用的默认占位符。
- 工作原理: 会被 MyBatis 预编译成一个 ,然后通过
PreparedStatement设置参数,它完全具备 的所有优点,可以 100% 防止 SQL 注入。 - 场景:适用于所有需要传入变量值的地方,如
WHERE条件、INSERT的值、UPDATE的值等。
MyBatis XML 示例 (使用 )
<!-- UserMapper.xml -->
<select id="findUserByUsername" resultType="User">
SELECT id, username, password
FROM users
WHERE username = #{username}
</select>
当调用 mapper.findUserByUsername("john_doe") 时,MyBatis 会将其转换为 JDBC 的 PreparedStatement,并调用 pstmt.setString(1, "john_doe")。
(字符串替换/拼接)
这是一个非常强大但极其危险的占位符,必须谨慎使用。
- 工作原理: 不会进行预编译,而是将变量直接替换成字符串,拼接到最终的 SQL 语句中,它相当于 Java 里的字符串拼接。
- 场景:
- 表名或列名是动态的:根据配置选择不同的表进行查询。
SELECT * FROM ${tableName}。 - SQL 片段的动态拼接:
ORDER BY后面跟动态的列名。ORDER BY ${columnName}。
- 表名或列名是动态的:根据配置选择不同的表进行查询。
- 警告:因为 是直接拼接字符串,所以无法防止 SQL 注入,如果输入的数据来自用户,必须进行严格的白名单校验。
MyBatis XML 示例 (使用 ,危险!)
<!-- 假设 orderByColumn 是用户输入的值 -->
<select id="findUsersOrdered" resultType="User">
SELECT id, username, password
FROM users
ORDER BY ${orderByColumn} <!-- 危险!orderByColumn 是 "id; DROP TABLE users;--" -->
</select>
orderByColumn 的值是 "id; DROP TABLE users;--",最终生成的 SQL 会是:
SELECT id, username, password FROM users ORDER BY id; DROP TABLE users;--
这将导致表被删除!
总结与对比
| 特性 | JDBC (问号) | MyBatis (井号) | MyBatis (美元符) |
|---|---|---|---|
| 核心机制 | 预编译,参数化查询 | 预编译,参数化查询 | 字符串拼接,直接替换 |
| 安全性 | 高,防止 SQL 注入 | 高,防止 SQL 注入 | 低,存在 SQL 注入风险 |
| 性能 | 高(预编译缓存) | 高(底层是 ) | 低(每次都要重新解析 SQL) |
| 主要用途 | 所有动态数据 | 所有动态数据(首选) | 动态表名、动态列名、SQL 片段 |
| 最佳实践 | 必须使用 | 必须使用 | 仅在无法使用 时使用,并严格校验输入 |
最终建议:
- 如果你使用原生 JDBC:永远、永远、永远使用 。
- 如果你使用 MyBatis 或类似框架:
- 默认且优先使用 ,它能解决 99% 的动态数据问题,并且是安全的。
- 仅在万不得已的情况下(如动态表名)使用 ,并且要对输入的数据进行极其严格的验证(只允许预定义的、固定的几个值)。
