一、JDBC架构与驱动详解
- JDBC架构组成
JDBC API分为两层:
· Application层:开发者编写的Java程序,调用JDBC接口。
· Driver Manager层:DriverManager负责加载并选择合适的数据库驱动。
· Driver层:数据库厂商实现的驱动,负责与数据库通信(通常基于TCP/IP)。
- 驱动类型深入
类型 名称 工作方式 典型场景 现状
1 JDBC-ODBC桥 通过ODBC驱动访问数据库,需配置ODBC数据源 老旧系统 已废弃(Java 8移除)
2 本地API驱动 使用厂商客户端库(如OCI),转为本地方法调用 依赖特定平台 较少使用
3 网络纯Java驱动 中间服务器转换为数据库协议 防火墙穿透 很少见
4 纯Java直连驱动 直接使用数据库自有协议(如MySQL的Binlog协议) 几乎所有生产环境 最主流
常见Type 4驱动JAR坐标(Maven):
· MySQL:mysql:mysql-connector-java:8.0.33
· PostgreSQL:org.postgresql:postgresql:42.6.0
· Oracle:com.oracle.database.jdbc:ojdbc8:21.9.0.0
二、详细操作步骤(附原理说明)
步骤0:添加驱动依赖
将驱动JAR放入classpath(传统Web应用放在WEB-INF/lib;Maven项目添加依赖)。
步骤1:注册驱动
· JDBC 3.0以前:必须用Class.forName("驱动类名"),触发静态代码块中的DriverManager.registerDriver()。
· JDBC 4.0+(Java 6+):驱动JAR的META-INF/services/java.sql.Driver文件指定了驱动类名,DriverManager在初始化时会通过ServiceLoader自动加载。因此大多数情况可省略。
注意:某些老旧驱动或类加载器隔离环境(如OSGi)仍需显式注册。
步骤2:获取连接
```java
String url = "jdbc:mysql://host:port/database?参数1=值1&参数2=值2";
Connection conn = DriverManager.getConnection(url, user, password);
```
JDBC URL格式解析(以MySQL为例)
· 协议:jdbc:mysql:
· 主机/端口://localhost:3306/
· 数据库名:mydb
· 常用参数:
· useSSL=false(开发环境) / true(生产应配置证书)
· serverTimezone=UTC(避免时间区错误)
· rewriteBatchedStatements=true(优化批处理性能)
· allowMultiQueries=true(允许一次发送多条SQL,分号分隔)
· connectTimeout=30000(连接超时毫秒)
· socketTimeout=60000(读写超时)
DriverManager 工作原理
· 内部维护一个CopyOnWriteArrayList<DriverInfo>,所有已注册驱动。
· getConnection遍历驱动列表,调用driver.connect(url, info),第一个返回非空Connection即成功。
步骤3:创建Statement对象
接口 创建方式 适用场景 风险/优势
Statement conn.createStatement() 静态SQL,无参数 有SQL注入风险,效率低
PreparedStatement conn.prepareStatement(sql) 带参数的SQL,重复执行 防注入,预编译,推荐
CallableStatement conn.prepareCall("{call proc(?,?)}") 调用存储过程 可获取输出参数
PreparedStatement 预编译真相
· 当调用prepareStatement时,驱动发送SQL模板(带占位符?)到数据库,数据库进行解析、验证、优化、编译,生成执行计划。
· 后续多次调用时,只需发送参数值,数据库直接使用已编译的计划执行,大幅提升性能。
· 注意:某些驱动(如MySQL老版本)默认不开启预编译,需要URL参数useServerPrepStmts=true。
步骤4:执行SQL
执行方法表
方法 返回值 适用于
executeQuery() ResultSet SELECT
executeUpdate() int(影响行数) INSERT/UPDATE/DELETE,以及DDL(如CREATE)
execute() boolean(true表示有结果集) 不确定的SQL,或存储过程返回多个结果集
处理多个结果集(如存储过程)
```java
boolean isResultSet = stmt.execute(sql);
while (true) {
if (isResultSet) {
ResultSet rs = stmt.getResultSet();
// 处理rs
rs.close();
} else {
int updateCount = stmt.getUpdateCount();
if (updateCount == -1) break; // 没有更多结果
// 处理updateCount
}
isResultSet = stmt.getMoreResults(); // 移动到下一个结果集
}
```
步骤5:处理ResultSet
ResultSet核心方法
· 移动游标:next()、previous()、absolute(int)、relative(int)、first()、last()(需创建可滚动结果集)。
· 获取数据:getXxx(int columnIndex)、getXxx(String columnLabel),支持类型转换(如getInt读取VARCHAR数字列)。
· 获取元数据:ResultSetMetaData md = rs.getMetaData();,可获取列数、列名、列类型等。
可滚动、可更新结果集
创建Statement时指定:
```java
Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, // 或 TYPE_SCROLL_SENSITIVE
ResultSet.CONCUR_UPDATABLE // 允许更新
);
ResultSet rs = stmt.executeQuery("SELECT id, name FROM user");
rs.absolute(5); // 移动到第5行
rs.updateString("name", "newName");
rs.updateRow(); // 更新数据库
```
注意:可更新结果集要求查询包含主键列,且不涉及多表连接。实际生产用得不多,推荐直接用UPDATE语句。
步骤6:关闭资源
正确关闭的三种写法
- finally块(Java 6及以前)
```java
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = ...;
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
} catch (SQLException e) {
// 处理
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
```
- try-with-resources(Java 7+,推荐)
```java
try (Connection conn = DriverManager.getConnection(...);
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
// 使用资源
} // 自动关闭
```
- 封装工具类 + ThreadLocal(不推荐,易出错)
三、深入知识点扩展
- SQL注入原理与PreparedStatement防护
注入示例:
String sql = "SELECT * FROM user WHERE name = '" + userName + "'";
如果输入' OR '1'='1,最终SQL变成... WHERE name = '' OR '1'='1',永远为真。
PreparedStatement防护机制:
· 参数占位符?发送到数据库时,驱动会转义参数中的特殊字符(如引号、反斜杠)。
· 数据库将参数当作字面值处理,而不是SQL语法的一部分。
· 即使是字符串参数包含OR '1'='1,也会被当作普通字符串值,不会改变语义。
- 事务隔离级别与并发问题
4种隔离级别(从低到高)
隔离级别 脏读 不可重复读 幻读 性能
READ_UNCOMMITTED 是 是 是 最好
READ_COMMITTED (Oracle默认) 否 是 是 好
REPEATABLE_READ (MySQL默认) 否 否 是(InnoDB通过间隙锁避免) 中等
SERIALIZABLE 否 否 否 最差
· 脏读:读到未提交的数据。
· 不可重复读:同一事务内两次查询结果不同(因为其他事务更新了数据)。
· 幻读:同一事务内两次范围查询,结果行数不同(其他事务插入或删除了记录)。
设置事务隔离级别
```java
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
```
注意:必须在事务开始前(setAutoCommit(true)时或开启事务前)设置。
- 批处理深度优化
两种批处理方式
方式一:Statement批处理
```java
Statement stmt = conn.createStatement();
stmt.addBatch("INSERT INTO t VALUES(1)");
stmt.addBatch("UPDATE t SET x=2 WHERE id=1");
int\[\] results = stmt.executeBatch();
```
方式二:PreparedStatement批处理(更常用)
```java
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO log(msg) VALUES(?)");
for (int i = 0; i < 100000; i++) {
pstmt.setString(1, "log " + i);
pstmt.addBatch();
if (i % 1000 == 0) { // 每1000条提交一次,避免内存溢出
pstmt.executeBatch();
pstmt.clearBatch();
}
}
pstmt.executeBatch(); // 剩余
```
MySQL批处理性能关键参数
· rewriteBatchedStatements=true:将多条INSERT重写为一条INSERT INTO ... VALUES (...), (...), ...,性能可提升数十倍。
· useServerPrepStmts=false(默认):客户端预编译,批处理效果差;设为true可启用服务端预编译,但批处理时建议rewriteBatchedStatements为主。
与事务结合
```java
conn.setAutoCommit(false);
// 批处理执行...
conn.commit();
```
- 连接池原理与主流实现
为什么需要连接池?
· 创建Connection需要网络握手、认证、分配资源,耗时可达100ms+。
· 高并发下频繁创建关闭连接会严重降低吞吐量。
· 连接池维护一定数量的连接,复用它们,大幅减少延迟。
主流连接池对比
连接池 特点 性能 活跃度
HikariCP 极快,字节码精简,默认Spring Boot 2.x 最高 非常活跃
Druid 阿里开源,支持监控、SQL防火墙、日志 高 活跃
Tomcat JDBC Pool 兼容Tomcat,性能也不错 较高 维护中
C3P0 老牌,稳定但性能较差 低 已过时
HikariCP完整配置示例
```java
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("pass");
config.setMaximumPoolSize(10); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
config.setConnectionTimeout(30000); // 获取连接超时(ms)
config.setIdleTimeout(600000); // 空闲超时(ms)
config.setMaxLifetime(1800000); // 连接最大生命周期
config.setPoolName("MyHikariPool");
// 可选:连接测试
config.setConnectionTestQuery("SELECT 1");
HikariDataSource dataSource = new HikariDataSource(config);
// 使用时
try (Connection conn = dataSource.getConnection()) {
// ...
}
// 应用关闭时释放池
dataSource.close();
```
- 调用存储过程(CallableStatement)
MySQL存储过程示例:
```sql
CREATE PROCEDURE getUser(IN userId INT, OUT userName VARCHAR(50))
BEGIN
SELECT name INTO userName FROM user WHERE id = userId;
END;
```
Java调用:
```java
CallableStatement cstmt = conn.prepareCall("{call getUser(?, ?)}");
cstmt.setInt(1, 100); // 设置输入参数
cstmt.registerOutParameter(2, Types.VARCHAR); // 注册输出参数
boolean hadResults = cstmt.execute(); // 或 cstmt.executeUpdate()
String name = cstmt.getString(2); // 获取输出参数
// 如果有结果集,用cstmt.getResultSet()获取
cstmt.close();
```
- 处理BLOB/CLOB大数据
写入BLOB:
```java
String sql = "INSERT INTO files (name, content) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "photo.jpg");
try (InputStream in = new FileInputStream("photo.jpg")) {
pstmt.setBinaryStream(2, in, (int)new File("photo.jpg").length());
pstmt.executeUpdate();
}
```
读取BLOB:
```java
ResultSet rs = stmt.executeQuery("SELECT content FROM files WHERE id=1");
if (rs.next()) {
Blob blob = rs.getBlob("content");
InputStream in = blob.getBinaryStream();
// 写入文件或处理
blob.free(); // 释放BLOB占用的数据库资源
}
```
注意:对于非常大的数据(>10MB),建议使用setBinaryStream分段写入,避免内存溢出。
- 元数据的实际应用
动态生成简单的查询工具
```java
public void printTableData(String tableName) throws SQLException {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName)) {
ResultSetMetaData md = rs.getMetaData();
int columnCount = md.getColumnCount();
// 打印表头
for (int i = 1; i <= columnCount; i++) {
System.out.print(md.getColumnName(i) + "\t");
}
System.out.println();
// 打印数据
while (rs.next()) {
for (int i = 1; i <= columnCount; i++) {
System.out.print(rs.getString(i) + "\t");
}
System.out.println();
}
}
}
```
获取数据库所有表
```java
DatabaseMetaData meta = conn.getMetaData();
try (ResultSet rs = meta.getTables(null, null, "%", new String\[\]{"TABLE"})) {
while (rs.next()) {
System.out.println(rs.getString("TABLE_NAME"));
}
}
```
- 超时设置
层级 设置方法 作用
驱动连接超时 URL参数connectTimeout=30000 建立TCP连接的超时
驱动socket超时 socketTimeout=60000 执行SQL期间,从socket读取数据的超时
JDBC Statement超时 stmt.setQueryTimeout(10) 执行SQL的最大秒数(秒),超时抛出SQLTimeoutException
事务超时 使用TransactionManager或Spring @Transactional(timeout=10) 整个事务的最大执行时间
注意:setQueryTimeout对某些驱动有bug(如MySQL老版本),推荐结合socketTimeout使用。
- 批量操作与事务的权衡
· 小批量(每批几百条):自动提交模式(默认)每条SQL单独提交,性能差,推荐手动提交。
· 大批量(数万条):手动提交并在适当位置commit(),防止事务日志过大。
```java
conn.setAutoCommit(false);
int batchSize = 1000;
for (int i = 0; i < 100000; i++) {
pstmt.setString(1, "value" + i);
pstmt.addBatch();
if (i % batchSize == 0) {
pstmt.executeBatch();
conn.commit();
pstmt.clearBatch();
}
}
pstmt.executeBatch();
conn.commit();
```
- 处理JDBC异常
SQLException的子类(JDBC 4.0+)
· SQLNonTransientException:重试不会成功的错误(如SQL语法错误、表不存在)。
· SQLTransientException:临时错误,重试可能成功(如网络超时、死锁)。
· SQLRecoverableException(Oracle特有):连接失效但可通过重建连接恢复。
获取详细错误信息
```java
catch (SQLException e) {
System.err.println("SQLState: " + e.getSQLState());
System.err.println("ErrorCode: " + e.getErrorCode()); // 厂商特定代码
System.err.println("Message: " + e.getMessage());
for (Throwable t : e) { // SQLException支持迭代链
System.err.println("Cause: " + t);
}
}
四、性能调优最佳实践
-
使用连接池:避免频繁创建关闭物理连接。
-
使用PreparedStatement:预编译和防注入。
-
合理设置批处理大小:通常500~2000条/批。
-
只查询需要的列:不要SELECT *,减少网络传输和内存。
-
使用fetchSize:对于大结果集,设置合理的fetchSize避免一次性加载所有行。
```java
pstmt.setFetchSize(1000); // 每次从数据库抓取1000行
```
-
流式读取:对于超大结果集,可设置useCursorFetch=true和defaultFetchSize,但需结合数据库特性。
-
尽量使用索引:这是数据库层面的优化,但JDBC执行的SQL也要注意。
-
关闭自动提交:在事务中合并多个DML操作。
-
避免在循环中执行SQL:改用批处理或JOIN查询。
-
正确关闭资源:防止连接泄露。
五、与ORM框架对比及适用场景
特性 原生JDBC MyBatis Hibernate/JPA
控制力 完全控制SQL SQL写XML/注解 自动生成SQL
开发效率 低,需手动映射 中等,半自动 高,对象化操作
学习曲线 较平缓 中等 陡峭(需理解持久化上下文)
性能 最高(无反射开销) 高 中等(有缓存和代理开销)
适用场景 高性能需求、复杂SQL、批量处理 大多数企业应用 快速开发、简单CRUD
建议:
· 学习阶段、性能敏感模块(如批处理、报表)、需要精细控制SQL的场景,使用原生JDBC。
· 一般业务开发,使用MyBatis或JPA,它们底层都依赖JDBC。
六、常见错误及排查
错误现象 可能原因 解决方法
ClassNotFoundException 驱动JAR未在classpath 添加Maven依赖或手动放JAR
No suitable driver found URL格式错误,或驱动未注册 检查URL,JDBC 4.0前需Class.forName
Access denied for user 用户名或密码错误 验证数据库权限
Communications link failure 网络不通、防火墙、数据库未启动 ping数据库端口,检查bind-address
Data truncation 插入数据长度超过字段定义 检查字段长度或启用STRICT_TRANS_TABLES
Lock wait timeout exceeded 事务未提交导致锁等待 检查是否未commit或rollback
ResultSet closed 在Statement/Connection关闭后访问ResultSet 确保ResultSet在使用完毕前不被关闭
Prepared statement already closed 复用已关闭的PreparedStatement 使用连接池时,Statement随Connection归还而关闭,需重新获取
七、JDBC 4.x 新特性
· 自动加载驱动(已提及)。
· try-with-resources支持(Java 7)。
· RowSet:可离线的结果集,支持JavaBeans属性。
· DataSource接口:标准连接池工厂,JNDI绑定。
· QueryTimeout 和 setNetworkTimeout 细粒度超时控制。
· JDBC 4.3(Java 9+):增加了sharding相关支持,以及largeUpdateCount。
八、实战:一个完整的CRUD工具类(简化版)
```java
public class JdbcUtils {
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/db");
config.setUsername("root");
config.setPassword("pass");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static int update(String sql, Object... params) {
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
setParameters(pstmt, params);
return pstmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static <T> List<T> query(String sql, RowMapper<T> mapper, Object... params) {
List<T> list = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
setParameters(pstmt, params);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
list.add(mapper.mapRow(rs));
}
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return list;
}
private static void setParameters(PreparedStatement pstmt, Object\[\] params) throws SQLException {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, paramsi);
}
}
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs) throws SQLException;
}
}
```
使用示例:
```java
String sql = "SELECT id, name FROM user WHERE age > ?";
List<User> users = JdbcUtils.query(sql, rs -> {
User u = new User();
u.setId(rs.getInt("id"));
u.setName(rs.getString("name"));
return u;
}, 18);