异常处理
本文你会学到:
SQLException 提供的三种诊断信息(getMessage / getSQLState / getErrorCode)
- 异常链和因果链的遍历方式
- 通过
SQLState 类别码区分致命与非致命错误
SQLWarning 的获取和处理
- JDBC 4.0 异常子类体系(瞬时 vs 非瞬时)
🤔 为什么需要了解 SQLException
JDBC 操作可能因多种原因失败:数据库服务不可达、认证失败、SQL 语法错误、约束违反、连接超时等。SQLException 不仅携带了错误描述信息,还提供了 SQLState(标准化的 5 字符错误码)、ErrorCode(数据库厂商私有码)以及异常链,使得开发者可以精确诊断问题根因,并根据错误类型采取不同的恢复策略。
📋 出错了能拿到哪些诊断信息?
错误描述与 SQLState
SQLException 提供三个核心诊断方法:
| 方法 |
返回类型 |
说明 |
getMessage() |
String |
人类可读的错误描述 |
getSQLState() |
String |
5 字符的 SQLState 错误码,遵循 X/Open 标准约定 |
getErrorCode() |
int |
数据库厂商私有的错误码(如 MySQL 的错误编号) |
SQLState 的前 2 位是类别码,标识错误的大类,后 3 位是子类码。同一个 SQLState 在不同数据库之间具有一致的语义,因此可用于编写与数据库无关的错误处理逻辑。
异常链遍历
数据库操作失败时,底层可能产生多个关联异常(例如驱动层异常 + 数据库层异常)。SQLException 通过 getNextException() 将它们串联成链,开发者需逐一遍历才能获取完整的错误信息。
| 异常链遍历:getNextException() |
|---|
| /**
* 演示 SQLException 异常链遍历
* <p>
* 一个 SQLException 可能关联多个异常,通过 getNextException() 逐个获取
*/
@Test
@DisplayName("异常链遍历")
void testExceptionChain() {
// 查询不存在的表,触发 SQLException
try (Statement stmt = conn.createStatement()) {
stmt.executeQuery("SELECT * FROM non_existent_table");
fail("应该抛出 SQLException");
} catch (SQLException e) {
// 遍历异常链,打印每个异常的信息
System.out.println("=== 异常链遍历 ===");
System.out.println("异常总数: " + countExceptions(e));
SQLException current = e;
int index = 1;
while (current != null) {
System.out.println("异常 #" + index + ":");
System.out.println(" Message: " + current.getMessage());
System.out.println(" SQLState: " + current.getSQLState());
System.out.println(" ErrorCode: " + current.getErrorCode());
current = current.getNextException();
index++;
}
// 验证至少有一个异常
assertNotNull(e.getMessage());
}
}
/**
* 递归统计异常链中的异常总数
*/
private int countExceptions(SQLException e) {
int count = 1;
SQLException next = e.getNextException();
while (next != null) {
count++;
next = next.getNextException();
}
return count;
}
|
因果链遍历
除了 getNextException(),SQLException 也支持 Java 标准的 getCause() 因果链。两者的区别:
| 方法 |
所属体系 |
用途 |
getNextException() |
JDBC 特有 |
链接同类型的多层 SQLException,如驱动异常 + 数据库返回的异常 |
getCause() |
Java 标准异常链 |
initCause() 设置,通常包装底层 IOException 等非 SQLException |
在实际排查中,建议**两种链都遍历**,确保不遗漏任何诊断信息。
🔍 怎样区分致命与非致命错误?——SQLState 过滤
通过 SQLState 类别码可以区分错误严重程度,对非致命错误执行特殊处理(如自动建表、跳过已存在的索引等),而非直接向上抛出异常。
| 根据 SQLState 过滤非致命异常 |
|---|
| /**
* 演示通过 SQLState 过滤异常类型
* <p>
* SQLState 是一个 5 字符字符串,遵循 X/Open SQL 约定:
* - 42S02:表或视图不存在
* - 23000:违反完整性约束(如主键冲突)
* - 08001:无法建立连接
*/
@Test
@DisplayName("SQLState 过滤")
void testSqlStateFilter() {
// 尝试删除不存在的表
try (Statement stmt = conn.createStatement()) {
stmt.execute("DROP TABLE non_existent_table");
fail("应该抛出 SQLException");
} catch (SQLException e) {
String sqlState = e.getSQLState();
System.out.println("=== SQLState 过滤 ===");
System.out.println("SQLState: " + sqlState);
System.out.println("Message: " + e.getMessage());
if ("42S02".equals(sqlState)) {
// 表不存在的错误码,属于预期的非致命错误
System.out.println("[预期错误] 表不存在,可以安全忽略或自动创建表");
} else {
// 其他 SQLState 表示非预期错误,需要关注
System.out.println("[意外错误] 未预期的 SQLState: " + sqlState);
fail("意外错误: " + e.getMessage());
}
// 验证 SQLState 以 "42" 开头(语法错误或访问规则类错误)
assertTrue(sqlState.startsWith("42"),
"表不存在错误的 SQLState 应以 '42' 开头");
}
}
|
常见 SQLState 类别码:
| 类别码 |
含义 |
典型场景 |
42 |
语法错误或访问规则错误 |
表/视图不存在(42S02)、列不存在 |
08 |
连接异常 |
数据库服务不可达(08001)、连接超时 |
23 |
完整性约束违反 |
主键冲突(23000)、外键约束 |
25 |
事务状态无效 |
事务回滚后继续操作(25000) |
HY |
驱动/连接实现错误 |
驱动不支持的操作(如 H2 特有的 HY000) |
编码建议
判断 SQLState 时优先使用 startsWith("42") 按类别匹配,而非精确匹配完整的 5 字符代码。这样同一类别下的不同子错误都能被覆盖,同时保持代码的数据库兼容性。
⚠️ 操作成功了但有隐患?——SQLWarning
SQLWarning 是 SQLException 的子类,表示数据库操作产生的非致命警告。与 SQLException 不同,警告**不会中断程序执行**,也不会被 try-catch 捕获,必须主动调用 getWarnings() 获取。
Connection、Statement、ResultSet 三个接口都提供了 getWarnings() 和 clearWarnings() 方法,分别对应不同层级的警告信息。警告也支持链式遍历(getNextWarning()),用法与异常链一致。
最常见的警告类型是 DataTruncation(数据截断警告),表示读取或写入数据时发生了精度丢失或截断。
| 获取 Connection/Statement/ResultSet 上的 SQLWarning |
|---|
| /**
* 演示 SQLWarning 的获取与处理
* <p>
* SQLWarning 是 SQLException 的子类,表示数据库操作产生的警告信息。
* 与 SQLException 不同,警告不会中断程序执行,需要主动调用 getWarnings() 获取。
* H2 内存数据库通常不产生警告,这里主要演示 API 的使用方式。
*/
@Test
@DisplayName("SQLWarning 获取")
void testSqlWarning() throws SQLException {
System.out.println("=== SQLWarning 获取 ===");
// 1. Connection 级别的警告
SQLWarning connWarning = conn.getWarnings();
if (connWarning != null) {
System.out.println("Connection 警告: " + connWarning.getMessage());
// 遍历警告链(与异常链类似)
SQLWarning w = connWarning;
while (w != null) {
System.out.println(" Warning: " + w.getMessage()
+ ", SQLState: " + w.getSQLState());
w = w.getNextWarning();
}
} else {
System.out.println("Connection 无警告");
}
// 2. Statement 级别的警告
try (Statement stmt = conn.createStatement()) {
stmt.execute("SELECT * FROM exception_test");
SQLWarning stmtWarning = stmt.getWarnings();
if (stmtWarning != null) {
System.out.println("Statement 警告: " + stmtWarning.getMessage());
} else {
System.out.println("Statement 无警告");
}
// 3. ResultSet 级别的警告
try (ResultSet rs = stmt.getResultSet()) {
SQLWarning rsWarning = rs.getWarnings();
if (rsWarning != null) {
System.out.println("ResultSet 警告: " + rsWarning.getMessage());
} else {
System.out.println("ResultSet 无警告");
}
}
}
// 4. clearWarnings() 演示:清除所有已记录的警告
conn.clearWarnings();
System.out.println("已调用 conn.clearWarnings(),Connection 警告已清除");
assertNull(conn.getWarnings(), "清除后不应有警告");
// 验证基本功能正常:确认测试表数据可查询
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM exception_test")) {
assertTrue(rs.next());
assertEquals(1, rs.getInt(1), "测试表应有 1 条数据");
}
}
|
🌳 哪些异常值得重试?——异常子类体系
JDBC 4.0(Java 6)引入了更细粒度的异常子类,按错误性质分为两大分支:
graph TD
SQLException --> SQLNonTransientException
SQLException --> SQLTransientException
SQLNonTransientException --> SQLSyntaxErrorException
SQLNonTransientException --> SQLIntegrityConstraintViolationException
SQLNonTransientException --> SQLDataException
SQLTransientException --> SQLTimeoutException
SQLTransientException --> SQLTransactionRollbackException
SQLTransientException --> SQLTransientConnectionException
SQLException --> BatchUpdateException
classDef base fill:transparent,stroke:#e3b341,color:#adbac7,stroke-width:2px
classDef nonTrans fill:transparent,stroke:#f47067,color:#adbac7,stroke-width:1px
classDef trans fill:transparent,stroke:#539bf5,color:#adbac7,stroke-width:1px
classDef batch fill:transparent,stroke:#57ab5a,color:#adbac7,stroke-width:1px
class SQLException base
class SQLNonTransientException,SQLSyntaxErrorException,SQLIntegrityConstraintViolationException,SQLDataException nonTrans
class SQLTransientException,SQLTimeoutException,SQLTransactionRollbackException,SQLTransientConnectionException trans
class BatchUpdateException batch
| 分支 |
含义 |
说明 |
SQLNonTransientException |
非瞬时异常 |
重试无意义,必须修复问题(如 SQL 语法错误、约束违反) |
SQLTransientException |
瞬时异常 |
可能通过重试恢复(如连接超时、锁等待超时) |
BatchUpdateException |
批处理异常 |
继承自 SQLException,额外携带 getUpdateCounts() 表示每条语句的执行结果 |
框架已处理的场景
在 Spring Boot 等框架中,这些子类通常已被统一转换,开发者较少直接与它们打交道。但在编写原生 JDBC 代码或自定义异常转换逻辑时,了解这套分类体系有助于实现更精确的错误处理策略。