JDBC(Java Database Connectivity)定義了一組統一的 Java API,具體數據庫廠商提供實現(驅動 JAR)。核心類:
java.sql.DriverManager:服務提供者管理器,維護已註冊驅動列表,根據 URL 選擇驅動獲取連接。java.sql.Driver:驅動接口,廠商實現(如 com.mysql.cj.jdbc.Driver)。java.sql.Connection:數據庫會話上下文,輕量級對象,實際持有物理連接(TCP Socket)。Class.forName() 與 SPI 自動註冊// 1. 載入驅動類到 JVM,觸發類初始化
Class.forName("com.mysql.cj.jdbc.Driver"); // MySQL 8.0+ 驅動類名
// 2. 驅動類靜態代碼塊中向 DriverManager 註冊自己
// Driver 實現類典型結構:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver()); // 關鍵:註冊實例
} catch (SQLException e) {
throw new RuntimeException("Can't register driver!", e);
}
}
// ...
}
META-INF/services/java.sql.Driver SPI)META-INF/services/java.sql.Driver 文件,內容為驅動實現類全限定名。DriverManager 初始化時通過 ServiceLoader 自動發現並註冊,**無需手動 Class.forName()**// 完整示例:查詢用戶表
public void queryUsers() {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
// 1. 加載驅動(JDBC 4.0+ 可省略)
// Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 定義連接 URL
String url = "jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
// 3. 獲取連接
conn = DriverManager.getConnection(url, "root", "password");
// 4. 創建 Statement(預編譯防注入)
String sql = "SELECT id, username, email FROM users WHERE status = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 1); // 綁定參數
// 5. 執行查詢
rs = pstmt.executeQuery();
// 6. 處理結果集
while (rs.next()) {
Long id = rs.getLong("id");
String username = rs.getString("username");
String email = rs.getString("email");
System.out.printf("User: %d - %s - %s%n", id, username, email);
}
} catch (SQLException e) {
e.printStackTrace(); // 生產環境用日誌框架
} finally {
// 7. 關閉資源(逆序:ResultSet → Statement → Connection)
closeQuietly(rs);
closeQuietly(pstmt);
closeQuietly(conn);
}
}
// 靜默關閉工具方法
private void closeQuietly(AutoCloseable closeable) {
if (closeable != null) {
try { closeable.close(); } catch (Exception ignored) {}
}
}
jdbc:: // 協議:子協議:數據源標識 // MySQL 示例 jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai // 協議 子協議 主機:端口/數據庫?參數 // 關鍵參數 useUnicode=true // 使用 Unicode 字符集 characterEncoding=utf8 // 字符編碼(配合 useUnicode) useSSL=false // 關閉 SSL(本地開發加速) serverTimezone=Asia/Shanghai // 時區(避免時區差導致時間錯誤) allowPublicKeyRetrieval=true // MySQL 8.0+ 缓存 sha2_password 公鑰 rewriteBatchedStatements=true // 開啟批次優化(批量插入性能提升 10x+)
| 類型 | 創建方式 | 特點 | 適用場景 | 防注入 |
|---|---|---|---|---|
Statement | conn.createStatement() | 靜態 SQL,每次執行編譯 | DDL(建表、建索引)、極簡單無參查詢 | 無,拼接 SQL 極易注入 |
PreparedStatement | conn.prepareStatement(sql) | 預編譯,參數化佔位符 ?,執行計劃可復用 | **95%+ 業務 SQL**(增刪改查帶參數) | 有效防注入,參數不參與 SQL 解析 |
CallableStatement | conn.prepareCall("{call proc(?,?)}") | 調用存儲過程/函數,支援 OUT/INOUT 參數 | 複雜業務邏輯下推數據庫、報表統計 | 同 PreparedStatement |
// 參數綁定(索引從 1 開始)
pstmt.setLong(1, 100L);
pstmt.setString(2, "張三");
pstmt.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
// 執行
boolean hasResult = pstmt.execute(); // 通用執行(多結果集/更新計數)
ResultSet rs = pstmt.executeQuery(); // SELECT 返回 ResultSet
int rows = pstmt.executeUpdate(); // INSERT/UPDATE/DELETE/DDL 返回影響行數
// 批量執行(配合 rewriteBatchedStatements=true 效果最佳)
for (User u : users) {
pstmt.setLong(1, u.getId());
pstmt.setString(2, u.getName());
pstmt.addBatch();
}
int[] counts = pstmt.executeBatch(); // 返回每條影響行數
| 游標類型 | 常量 | 特點 |
|---|---|---|
| 只前向 | TYPE_FORWARD_ONLY | 默認,高效,僅 next() 向後移動 |
| 可滾動不敏感 | TYPE_SCROLL_INSENSITIVE | 可 previous()/absolute(),不見他人提交 |
| 可滾動敏感 | TYPE_SCROLL_SENSITIVE | 可滾動,可見他人提交 |
// 創建可滾動 ResultSet
Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
rs.last(); // 移動到最後一行
int total = rs.getRow(); // 獲取行數(需可滾動)
rs.beforeFirst(); // 重置游標
while (rs.next()) { ... }
rs.getLong(1))而非列名(rs.getLong("id")),避免列名查找開銷。getLong/getString/getTimestamp),避免 getObject 額外轉換。getBinaryStream/getCharacterStream),避免一次性載入內存。// 正確順序
ResultSet rs = ...; // 1. 最後創建
Statement stmt = ...; // 2. 中間創建
Connection conn = ...; // 3. 最先創建
// 關閉順序:rs → stmt → conn
try { if (rs != null) rs.close(); } catch (SQLException e) { log.warn("", e); }
try { if (stmt != null) stmt.close(); } catch (SQLException e) { log.warn("", e); }
try { if (conn != null) conn.close(); } catch (SQLException e) { log.warn("", e); }
原因:ResultSet 依賴 Statement,Statement 依賴 Connection。先關閉 Connection 會導致 Statement/ResultSet 失效,後續關閉拋異常或洩漏底層 Socket。
八、連接池:生產環境必備(HikariCP / Druid)
頻繁
DriverManager.getConnection()開銷極大(TCP 三次握手、認證、會話建立)。生產環境**必須**使用連接池:
# Spring Boot application.yml (HikariCP)
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?...
username: root
password: pwd
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1
mysql:mysql-connector-java)。PreparedStatement + 參數綁定(防注入、計劃復用)。addBatch/executeBatch + rewriteBatchedStatements=true。try-with-resources,順序 RS → Stmt → Conn。cause,統一上層處理。掌握 JDBC 七步驟、三類 Statement、URL 參數、資源關閉鐵律與連接池配置,是 Java 後端開發者直連數據庫、排查連接洩漏、優化 SQL 執行效能的硬核基本功。建議將本文作為《JDBC 開發規範》附件,代碼評審時逐項核對。