JDBC 連線 MySQL 完整實戰:從驅動載入到連線池最佳實踐
JDBC (Java Database Connectivity) 是 Java 應用存取關聯式資料庫的標準 API。本文涵蓋驅動版本選擇、連線參數優化、資源管理模式、預處理防注入、以及生產環境必備的連線池整合。
Maven 依賴(MySQL 8.0+ 驅動)
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId> <!-- 8.0+ 套件名變更 -->
<version>8.3.0</version>
</dependency>
<!-- 5.7 專案請用舊驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
基礎連線範例(JDBC 4.0+ 自動載入驅動,無需 Class.forName)
import java.sql.*;
public class JdbcDemo {
public static void main(String[] args) {
// 連線 URL 參數詳解見下表
String url = "jdbc:mysql://127.0.0.1:3306/mydb?" +
"useUnicode=true&characterEncoding=utf8mb4&" +
"useSSL=false&serverTimezone=Asia/Taipei&" +
"allowPublicKeyRetrieval=true&" +
"rewriteBatchedStatements=true"; // 批次優化關鍵
String user = "app_user";
String password = "strongpass";
// try-with-resources 自動關閉連線/Statement/ResultSet (JDK 7+)
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(
"SELECT id, name, email FROM user WHERE status = ? AND created_at > ?");
) {
ps.setInt(1, 1); // 防 SQL 注入
ps.setTimestamp(2, Timestamp.valueOf("2024-01-01 00:00:00"));
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// 依欄位名取值(推薦),不依索引,避免 SELECT * 順序變更出錯
System.out.printf("ID: %d, Name: %s, Email: %s%n",
rs.getInt("id"), rs.getString("name"), rs.getString("email"));
}
}
} catch (SQLException e) {
e.printStackTrace();
// 實際專案應拋出自訂異常、記錄錯誤碼 SQLState
}
}
}
關鍵連線 URL 參數對照表
| 參數 | 推薦值 | 說明 |
useUnicode | true | 使用 Unicode 字元集 |
characterEncoding | utf8mb4 | 支援 Emoji,MySQL 端 CHARSET=utf8mb4 對應 |
useSSL | false | 內網環境關閉 SSL 降低握手延遲;外網必開 true |
serverTimezone | Asia/Taipei | 避免時區不一致導致時間偏移(8.0+ 必填) |
allowPublicKeyRetrieval | true | 配合 caching_sha2_password 認證插件(8.0 預設) |
rewriteBatchedStatements | true | 批次 INSERT/UPDATE 重寫為多值語句,效能提升 10~100 倍 |
useServerPrepStmts | true | 啟用伺服器端預處理,減少解析開銷 |
cachePrepStmts | true | 客戶端快取 PreparedStatement |
prepStmtCacheSize | 250 | 快取大小 |
prepStmtCacheSqlLimit | 2048 | 快取 SQL 長度上限 |
connectTimeout | 10000 | 建立連線超時 ms |
socketTimeout | 30000 | 讀寫超時 ms,防止網路異常卡死 |
tcpKeepAlive | true | 啟用 TCP KeepAlive 偵測死連線 |
批次操作最佳實踐
// 批次插入 10,000 筆
String sql = "INSERT INTO user (name, email, status) VALUES (?, ?, ?)";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false); // 手動提交
int batchSize = 1000;
for (int i = 0; i < 10000; i++) {
ps.setString(1, "User" + i);
ps.setString(2, "user" + i + "@example.com");
ps.setInt(3, 1);
ps.addBatch();
if (i % batchSize == 0) {
ps.executeBatch(); // 執行批次
conn.commit(); // 提交事務
}
}
ps.executeBatch(); // 剩餘
conn.commit();
} catch (BatchUpdateException e) {
// 處理部分失敗:e.getUpdateCounts() 取得各條結果
}
生產環境:HikariCP 連線池整合
// Spring Boot application.yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Taipei&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
username: app_user
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20 # CPU核心數×2+1
minimum-idle: 20 # 固定大小
idle-timeout: 60000 # 60s < wait_timeout
max-lifetime: 1800000 # 30min
connection-timeout: 30000 # 30s
validation-timeout: 5000
leak-detection-threshold: 60000 # 洩漏偵測 60s
pool-name: MyApp-HikariCP
data-source-properties:
cachePrepStmts: true
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
useServerPrepStmts: true
常見坑與解決
| 現象 | 原因 | 解決 |
| Communications link failure | 連線池拿到 MySQL 已斷開的閒置連線 | 縮短 idleTimeout;開啟 testWhileIdle / validationQuery |
| Public Key Retrieval is not allowed | 8.0 預設 caching_sha2_password 需傳遞公鑰 | URL 加 allowPublicKeyRetrieval=true 或改用 mysql_native_password |
| 批次 INSERT 極慢 | 未開啟 rewriteBatchedStatements | URL 加 rewriteBatchedStatements=true |
| 時區錯誤 8 小時 | JVM/MySQL 時區不一致 | URL 明確指定 serverTimezone=Asia/Taipei |
| Emoji 存成 ???? | 字元集非 utf8mb4 | URL characterEncoding=utf8mb4、表 CHARSET=utf8mb4、連線池 connectionInitSql=SET NAMES utf8mb4 |
資源管理鐵律
- 永遠使用 try-with-resources 自動關閉
Connection、Statement、ResultSet,順序相反。
- 絕不在循環中建立連線,必須從連線池獲取。
- PreparedStatement 防注入,嚴禁字串拼接 SQL。
- 大結果集分頁 使用
LIMIT offset, size 或游標 FETCH FIRST n ROWS ONLY,避免一次拉取 OOM。
- 事務邊界清晰:
conn.setAutoCommit(false) → 業務邏輯 → commit()/rollback() → setAutoCommit(true)。