數據庫表 `master` (寵物主人):
name | password
---------|---------
liubei | 123
String sql = "SELECT * FROM `master` WHERE `name` = '" + name + "' AND `password` = '" + password + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
liubei123' OR '1'='1SELECT * FROM master WHERE name = 'liubei' AND password = '123' OR '1'='1'根因:用戶輸入被直接拼接進 SQL 字符串,改變了語義結構。這就是 SQL 注入。
| 特性 | Statement | PreparedStatement |
|---|---|---|
| SQL 處理 | 字符串拼接,每次發送完整 SQL | 預編譯模板,參數佔位符 ? |
| 編譯次數 | 每次執行都編譯 | 預編譯一次,多次執行復用計劃 |
| 參數傳遞 | 字符串拼接 (不安全) | setXxx(index, value) 類型安全綁定 |
| 防注入 | 無 | 徹底防禦 (參數不參與語法解析) |
| 性能 | 低 (重複編譯、無計劃緩存) | 高 (預編譯、計劃緩存、批量優化) |
String sql = "SELECT * FROM `master` WHERE `name` = ? AND `password` = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name); // 索引從 1 開始
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
// 即使 password = "123' OR '1'='1",也會作為字面量字符串匹配,不會改變 SQL 結構
| Java 類型 | Setter 方法 | 數據庫類型示例 |
|---|---|---|
| String | setString(int, String) | VARCHAR, CHAR, TEXT |
| int / Integer | setInt(int, int) | INT, SMALLINT |
| long / Long | setLong(int, long) | BIGINT |
| double / Double | setDouble(int, double) | DOUBLE, DECIMAL |
| boolean / Boolean | setBoolean(int, boolean) | BIT, BOOLEAN |
| Date / LocalDate | setDate(int, Date) / setObject(int, LocalDate) | DATE |
| Timestamp / LocalDateTime | setTimestamp(int, Timestamp) / setObject(int, LocalDateTime) | DATETIME, TIMESTAMP |
| byte[] | setBytes(int, byte[]) | BLOB, VARBINARY |
| InputStream | setBlob(int, InputStream) / setBinaryStream | BLOB |
| Reader | setClob(int, Reader) / setCharacterStream | CLOB, TEXT |
| null (任意類型) | setNull(int, sqlType) | 所有類型 |
// 1. 查詢 (SELECT)
ResultSet rs = pstmt.executeQuery();
// 2. 更新 (INSERT/UPDATE/DELETE/DDL)
int affectedRows = pstmt.executeUpdate(); // 返回影響行數
// 3. 通用 (可執行任意 SQL,返回 boolean)
boolean hasResultSet = pstmt.execute(); // true: 有 ResultSet (SELECT); false: 更新計數或 DDL
String sql = "INSERT INTO `order` (user_id, amount, create_time) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
conn.setAutoCommit(false); // 關閉自動提交,手動控制事務
for (Order order : orderList) {
pstmt.setLong(1, order.getUserId());
pstmt.setBigDecimal(2, order.getAmount());
pstmt.setTimestamp(3, new Timestamp(order.getCreateTime().getTime()));
pstmt.addBatch(); // 累加到批次
// 可選:分批提交,防止內存溢出
if (++count % 1000 == 0) {
pstmt.executeBatch();
conn.commit();
pstmt.clearBatch();
}
}
int[] results = pstmt.executeBatch(); // 執行剩餘批次
conn.commit();
批量優化關鍵:關閉自動提交 + `addBatch()` + `executeBatch()` + 定期提交。MySQL 需連接串添加 `rewriteBatchedStatements=true` 開啟驅動端重寫優化。
// 1. Client 發送 prepareStatement(sql)
// 2. Server 解析、編譯、優化 → 生成執行計劃 → 分配 Statement ID (handle)
// 3. Server 返回 handle 給 Client
// 4. Client 發送 execute(handle, parameters)
// 5. Server 綁定參數、執行計劃、返回結果
// 6. 重複 4-5,無需重新解析編譯
PreparedStatement / MyBatis `#{}` / JPA 參數綁定#{param}` → PreparedStatement;`${param}` → 字符串替換 (危險,僅用於表名/列名/排序字段且需白名單校驗)?` 佔位符自動綁定| 誤區 | 正確認知 |
|---|---|
| 以為 `PreparedStatement` 只是防注入 | 核心優勢是預編譯復用執行計劃,性能提升往往超過安全收益 |
| 在 `IN` 子句中直接拼接列表 | `WHERE id IN (?)` 無法綁定列表。需動態生成 `?,?,?` 或使用 MyBatis `` |
| 表名/列名/排序字段用參數綁定 | 標識符不支持參數化。必須白名單校驗後拼接 |
| 批量插入不關閉自動提交 | 每條 `executeUpdate` 都提交事務,性能極差。必須 `setAutoCommit(false)` + `executeBatch()` |
| 長連接不關閉 PreparedStatement | 佔用服務端緩存槽位,導致 `PreparedStatement cache` 失效。用完即關或使用連接池 `preparedStatementCacheSize` |
PreparedStatement 以預編譯 + 參數化綁定雙重機制,同時解決了 SQL 注入安全性 與 重複編譯性能 兩大核心痛點。掌握其正確用法 (參數索引從 1 開始、批量處理關閉自動提交、標識符白名單校驗),並理解驅動端緩存與數據庫端計劃緩存的交互原理,是 Java 開發者從「能跑 SQL」進化到「寫好 SQL」的必修課。