跳转至

异常处理

本文你会学到

  • 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

SQLWarningSQLException 的子类,表示数据库操作产生的非致命警告。与 SQLException 不同,警告**不会中断程序执行**,也不会被 try-catch 捕获,必须主动调用 getWarnings() 获取。

ConnectionStatementResultSet 三个接口都提供了 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 代码或自定义异常转换逻辑时,了解这套分类体系有助于实现更精确的错误处理策略。