Spring 数据访问异常体系:SQLException 到 DataAccessException

3 阅读33分钟

概述

在之前的系列文章中,我们已深入探索了 Spring 核心容器的基石——IoC 容器及其强大的 BeanPostProcessor 扩展点机制,也领略了 Spring MVC 如何通过 HandlerMappingHandlerAdapter 等组件优雅地处理 Web 请求。我们同样剖析了 Spring Boot 自动化配置的魔法,并见证了模板方法模式在 JdbcTemplate 中的经典应用。这些前置知识,共同构成了理解 Spring 生态的“骨架”。

现在,我们将目光转向一个更为“隐性”却至关重要的基础设施——数据访问异常体系。它就像一根贯穿应用各个层次的透明管道,悄无声息地将 JDBC、Hibernate、MyBatis 等底层持久化技术的专属“方言”(异常),统一翻译为 Spring 定义的“通用语言”(DataAccessException)。这一转换,使得上层的业务逻辑层得以与底层持久化技术彻底解耦,成为整个 Spring 数据访问模块不可或缺的粘合剂。

总结性引言

在 Java 持久化的基石 JDBC 中,所有操作异常都被定义为一个宽泛的受检异常——SQLException。开发者必须显式捕获或抛出它,并从中解析出特定数据库厂商定义的错误码和 SQL State,才能判断是语法错误、连接超时还是死锁。这带来了三个核心痛点:强制受检导致的代码污染数据库厂商差异导致的移植锁定、以及 SQLException 信息不足导致的异常处理粗糙

Spring 的设计哲学旗帜鲜明地反对在业务代码中处理这类技术细节。它通过 SQLExceptionTranslator 这一策略接口,智能地将 SQLException 翻译为统一的、非受检的 DataAccessException 层次结构。这项工程决策不仅将开发者从繁琐的 try-catch 中解放出来,更通过 NonTransientTransient 等异常分类,为声明式事务的回滚策略提供了语义基础。本文将深入解剖这一体系的核心架构,从 SQLErrorCodeSQLExceptionTranslator 的源码级错误码映射,到 PersistenceExceptionTranslationPostProcessor 如何利用 AOP 与 BeanPostProcessor 实现无感知的异常翻译,层层递进,揭示 Spring 是如何构建起一个坚不可摧的持久化中间层。

核心要点

  • 统一异常层次DataAccessException 继承自 NestedRuntimeException,其三个核心分支(NonTransientTransientRecoverable)构成了对数据访问故障的完整语义分类。
  • 智能错误码映射SQLErrorCodeSQLExceptionTranslator 是异常翻译的大脑,它通过加载并解析 sql-error-codes.xml,将各数据库厂商特有且混乱的错误码,精确映射到语义清晰的 DataAccessException 子类。
  • ORM 翻译无缝集成PersistenceExceptionTranslator 接口为 Hibernate、JPA 等 ORM 框架提供了统一的集成契约,而 PersistenceExceptionTranslationPostProcessor 则利用 AOP 技术,为所有 @Repository 组件自动织入异常翻译逻辑。
  • 设计模式的集大成者:模板方法模式(JdbcTemplate 中的异常捕获与翻译)、策略模式(不同的 SQLExceptionTranslator 实现)、BeanPostProcessor 扩展点(自动代理创建)在此体系中完美融合。
  • 与事务的隐性协同:异常的 Transient(暂时性)与 NonTransient(非暂时性)分类,为 Spring 事务管理器的回滚决策提供了关键但与代码解耦的语义提示。

文章组织架构图

下图以流程图形式清晰展示了本文的8大核心模块及其层级递进关系:

flowchart TD
    subgraph S1 ["认知建立"]
        n1["1. DataAccessException 统一体系总览"]
        n2["2. SQLException 的翻译引擎:SQLExceptionTranslator 及其实现"]
    end

    subgraph S2 ["核心技术剖析"]
        n3["3. 错误码映射的幕后英雄:SQLErrorCodeSQLExceptionTranslator"]
        n4["4. ORM 框架的整合桥梁:PersistenceExceptionTranslator"]
        n5["5. 自动翻译的魔法:PersistenceExceptionTranslationPostProcessor"]
        n6["6. 与 JdbcTemplate 的深入协同"]
    end

    subgraph S3 ["实战与升华"]
        n7["7. 生产事故排查专题"]
        n8["8. 面试高频专题"]
    end

    n1 --> n2 --> n3 --> n4 --> n5 --> n6 --> n7 --> n8

    classDef topic fill:#f8f9fa,stroke:#333,stroke-width:2px,rx:5,color:#333;
    class S1 topic;
    class S2 topic;
    class S3 topic;
    class n1 topic;
    class n2 topic;
    class n3 topic;
    class n4 topic;
    class n5 topic;
    class n6 topic;
    class n7 topic;
    class n8 topic;

架构图说明

  • 总览说明:全文8个模块遵循“是什么 → 如何实现 → 如何集成 → 如何应用 → 如何排错与应试”的认知逻辑。从异常体系的价值与架构总览开始,逐步深入到两大核心翻译引擎,再到 ORM 的无缝整合与 AOP 的自动化代理,最后以 JdbcTemplate 的协同实战、生产事故排查和面试高频题作为闭环。
  • 逐模块说明
    • 模块 1-2 建立基础认知:模块1宏观俯瞰 DataAccessException 的统一架构与分类哲学;模块2聚焦翻译引擎接口 SQLExceptionTranslator,并介绍其两大核心策略。
    • 模块 3-4 进入技术深水区:模块3是全文核心,深入源码剖析 SQLErrorCodeSQLExceptionTranslator 如何利用 sql-error-codes.xml 进行智能错误码映射;模块4则展示如何通过 PersistenceExceptionTranslator 接口将 Hibernate/MyBatis 等 ORM 框架纳入统一体系。
    • 模块 5 展现扩展点与 AOP 的完美结合:此模块是联系前文核心容器知识的关键,详细解析 PersistenceExceptionTranslationPostProcessor 如何作为一个 BeanPostProcessor,利用 AOP 代理为 @Repository Bean 自动添加异常翻译能力。
    • 模块 6 回归经典实战:剖析 JdbcTemplate 模板方法内部如何与翻译器协同,展示该体系在 Spring 内部最经典的应用。
    • 模块 7-8 面向现实与应试:通过两个生产事故复盘,将理论落地为排错能力;通过12道高频面试题,系统化巩固知识体系。
  • 关键结论Spring 的数据访问异常体系绝非仅仅是一组异常类,它是“设计模式”与“核心容器扩展点”的集大成者。它通过“翻译”这一核心动作,斩断了业务逻辑与数据访问技术间的耦合,是实现持久层技术无关性、提升代码可测试性和可维护性的基石,深刻体现了 Spring 优雅抽象与工程实用的设计哲学。

1. DataAccessException 统一体系总览

1.1 直击原生 JDBC 异常的痛点

在直接使用 JDBC 编程的年代,以下代码是每个开发者的梦魇:

// JDK 原生 JDBC 编程示例
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement("INSERT INTO users (name) VALUES (?)")) {
    ps.setString(1, "test");
    ps.executeUpdate();
} catch (SQLException e) {
    // 噩梦开始:如何理解这个异常?
    // 是需要重试的暂时性故障(如死锁),还是开发者犯下的不可恢复错误(如语法错误)?
    // 它的错误码是 1062 (MySQL) 还是 23505 (PostgreSQL)? 这完全绑定了数据库产品。
    // 开发者被迫在此处耦合大量特定数据库的错误码判断逻辑。
    e.printStackTrace();
}

SQLException 作为几乎所有 JDBC 操作的受检异常,携带了两个关键信息:

  • SQL State:基于 SQL-92 标准的状态码(如 42000 表示语法错误),理论上跨数据库,但实践中各厂商实现并不完全一致,且粒度很粗。
  • Error Code:数据库厂商自定义的错误码,如 MySQL 的 1062(重复键)、Oracle 的 1(唯一约束违反)。这些编码提供了精确的错误原因,但完全锁定了数据库平台。

这种设计导致了两个顽疾:

  1. 强制受检(Checked Exception):强制业务层捕获或声明 SQLException,将纯技术性的异常泄露到业务代码,污染了业务逻辑的清晰性,违背了关注点分离原则。
  2. 技术锁定:任何试图基于错误码的判断(例如判断是否为主键冲突),都将代码与特定数据库产品永久绑定,使得数据库移植成为一场重构灾难。

1.2 DataAccessException:非受检的统一抽象

Spring 的设计理念是“消灭非必要的受检异常”。其核心论断是:绝大多数数据访问异常都是不可恢复的,业务代码无力处理,因此应该是非受检的。DataAccessException 作为 Spring 数据访问异常层次的根,继承自 NestedRuntimeException,是一个RuntimeException。这本身就完成了一次巨大的解放。

// 摘自 org.springframework.core.NestedRuntimeException
package org.springframework.core;

public abstract class NestedRuntimeException extends RuntimeException {
    //... 持有 cause 的构造器
    public NestedRuntimeException(String msg, Throwable cause) {
        super(msg, cause);
    }
}
// 摘自 org.springframework.dao.DataAccessException
package org.springframework.dao;

public abstract class DataAccessException extends NestedRuntimeException {
    public DataAccessException(String msg) {
        super(msg);
    }
    public DataAccessException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

通过继承 NestedRuntimeExceptionDataAccessException 保留了异常根源(root cause)信息,这对于排查问题至关重要,同时它又是一个RuntimeException,解放了业务代码。

1.3 三大分支:异常分类的哲学

Spring 对异常的划分不仅仅是为了“包装”,更是为了赋予语义。它通过三层抽象对数据访问故障进行了哲学层面的分类:

  1. NonTransientDataAccessException(非暂时性异常)

    • 语义:操作重试必然失败的异常。根本原因在于数据或 SQL 本身,除非进行代码或数据层面的修复,否则同一操作永远无法成功。
    • 决策:对于此类异常,任何重试都是徒劳的。事务应该坚决回滚
    • 典型子类BadSqlGrammarException(SQL语法错误)、DataIntegrityViolationException(数据完整性违规)、DuplicateKeyException(主键/唯一键冲突)。
  2. TransientDataAccessException(暂时性异常)

    • 语义:操作重试可能成功的异常。根本原因在于瞬时的系统状态,而非永久的故障。
    • 决策:对于此类异常,可以设计重试策略。在事务管理中,有时也暗示着操作可能已经在数据库端成功,但响应返回时失败(并发场景)。
    • 典型子类QueryTimeoutException(查询超时)、TransientDataAccessResourceException(资源暂时不可用,如连接获取超时)。
  3. RecoverableDataAccessException(可恢复性异常)

    • 语义:介于以上两者之间,通常指应用通过某些特定操作(如回调、备用方案)可以恢复的异常。它在体系中的位置略显特殊,更多是表达一种“可干预”的语义。
    • 典型子类RecoverableDataAccessException 的子类较少,如面向死锁的 DeadlockLoserDataAccessException

1.4 DataAccessException 体系核心类图

下面的类图展示了 DataAccessException 的关键继承层次,揭示了其清晰的分类逻辑:

classDiagram
    class RuntimeException
    class NestedRuntimeException {
        +NestedRuntimeException(String msg, Throwable cause)
    }
    class DataAccessException {
        <<abstract>>
        +DataAccessException(String msg)
        +DataAccessException(String msg, Throwable cause)
    }
    class NonTransientDataAccessException {
        <<abstract>>
    }
    class TransientDataAccessException {
        <<abstract>>
    }
    class RecoverableDataAccessException {
        <<abstract>>
    }
    class BadSqlGrammarException
    class DataIntegrityViolationException
    class DuplicateKeyException
    class QueryTimeoutException
    class CannotAcquireLockException

    RuntimeException <|-- NestedRuntimeException
    NestedRuntimeException <|-- DataAccessException

    DataAccessException <|-- NonTransientDataAccessException
    DataAccessException <|-- TransientDataAccessException
    DataAccessException <|-- RecoverableDataAccessException

    NonTransientDataAccessException <|-- BadSqlGrammarException
    NonTransientDataAccessException <|-- DataIntegrityViolationException
    DataIntegrityViolationException <|-- DuplicateKeyException

    TransientDataAccessException <|-- QueryTimeoutException
    TransientDataAccessException <|-- CannotAcquireLockException

类图说明

  • 层级结构:此图清晰地展示了 DataAccessExceptionRuntimeException 开始的三级继承结构:运行时异常 -> 嵌套运行时异常 -> 数据访问异常根类。
  • 分类分支:三大分支 NonTransientTransientRecoverable 作为抽象类,构成了整个体系的核心骨架,每个分支下挂接了具体的异常实现。
  • 异常定位:如 DuplicateKeyException,通过继承 DataIntegrityViolationException,准确表明其“数据完整性违规”且“非暂时性”的双重特性,语义极其精确。
  • 关键设计:该体系的精妙之处在于其精准的分类学,它让 catch 语句既能捕获非常具体的异常(如 DuplicateKeyException),也能捕获更宽泛的分类(如 NonTransientDataAccessException),提供了极大灵活性。

2. SQLException 的翻译引擎:SQLExceptionTranslator 及其实现

2.1 翻译策略接口:SQLExceptionTranslator

Spring 将“翻译”这一行为抽象为一个核心策略接口,这是整个异常体系运转的起点。

// 摘自 org.springframework.jdbc.support.SQLExceptionTranslator
package org.springframework.jdbc.support;

import java.sql.SQLException;
import org.springframework.dao.DataAccessException;

@FunctionalInterface
public interface SQLExceptionTranslator {
    // 将给定的 SQLException 翻译为 Spring 的 DataAccessException
    // task: 可读的任务描述,如 “PreparedStatement.executeUpdate”
    // sql: 引发异常的 SQL 语句,可为 null
    // ex: 原生 SQLException
    DataAccessException translate(String task, String sql, SQLException ex);
}

该接口的设计意图非常明确:输入 SQLException,输出 DataAccessExceptiontasksql 参数用于提供丰富的上下文信息,帮助定位问题。

2.2 两大核心实现:正确的分工与降级

Spring 为 SQLExceptionTranslator 提供了两个主要实现,它们之间形成了一个完美的分工与降级策略:

  1. SQLErrorCodeSQLExceptionTranslator

    • 职责:基于数据库厂商的错误码进行精确翻译。
    • 机制:这是翻译的“大脑”,负责加载并解析 sql-error-codes.xml 配置文件,该文件维护了常见数据库错误码到 Spring 异常的映射关系。
    • 地位主翻译器,只有在能确定数据库产品类型时才使用,并提供最准确的翻译。
  2. SQLStateSQLExceptionTranslator

    • 职责:基于标准的 SQL-92 SQL State 进行通用翻译。
    • 机制:遵循 SQL 标准中对 SQL State 的分类规则(如 23xxx 类通常表示完整性约束违反,42xxx 表示语法错误)。
    • 地位兜底翻译器。当无法识别数据库(如未知数据源)、SQLErrorCodeSQLExceptionTranslator 翻译失败,或应用指定时,作为降级方案使用。其翻译粒度较粗,无法做到 DuplicateKeyException 这种精确程度。

下面我们首先分析作为兜底策略的 SQLStateSQLExceptionTranslator,下一章再深入剖析核心的 SQLErrorCodeSQLExceptionTranslator

2.3 SQLStateSQLExceptionTranslator:基于标准的降级策略

SQLStateSQLExceptionTranslator 的工作原理是解析 SQLException.getSQLState(),并将其前两位作为分类码,映射到粗粒度的 DataAccessException 子类。

// 摘自 org.springframework.jdbc.support.SQLStateSQLExceptionTranslator
package org.springframework.jdbc.support;

public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator {

    @Override
    protected DataAccessException doTranslate(String task, String sql, SQLException ex) {
        String sqlState = ex.getSQLState();
        if (sqlState != null && sqlState.length() >= 2) {
            String classCode = sqlState.substring(0, 2);
            // 根据标准 SQL State 分类码进行映射
            if ("00".equals(classCode)) { // 成功状态,不应是异常
                return null;
            } else if ("08".equals(classCode)) { // 连接异常 -> 暂时性资源异常
                return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);
            } else if ("22".equals(classCode) || "23".equals(classCode) || ... ) {
                // 22: 数据异常, 23: 完整性约束违反 -> 非暂时性数据完整性异常
                return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
            } else if ("42".equals(classCode)) { // 语法错误或访问规则违反 -> 语法错误异常
                return new BadSqlGrammarException(task, sql, ex);
            }
            // ... 其他映射
        }
        // 若无法识别,返回一个通用的回退异常
        return new UncategorizedSQLException(task, sql, ex);
    }
}

源码解读

  • doTranslate 方法:这是模板方法 translate 最终调用的钩子方法。它首先从 SQLException 中获取 SQL State。
  • 前两位分类:提取 SQL State 的前两位字符作为分类码,如 08 代表连接异常。
  • 通用映射:映射逻辑是标准化的,不区分数据库。例如,所有以 08 开头的 SQL State 都被视为连接失败,对应抛出 TransientDataAccessResourceException
  • 局限性:由于 SQL State 的通用性,此类翻译器无法区分 DuplicateKeyException 和其他数据完整性违规,它们都被笼统地归为 DataIntegrityViolationException。这正是它作为兜底而非主力翻译器的原因。

3. 错误码映射的幕后英雄:SQLErrorCodeSQLExceptionTranslator

这是全文最核心的模块,我们将深入源码,解剖 Spring 如何利用特定数据库的错误码,实现精确到子类的智能异常翻译。

3.1 配置之源:sql-error-codes.xml

SQLErrorCodeSQLExceptionTranslator 的魔法源于一个 XML 配置文件——sql-error-codes.xml。该文件位于 spring-jdbc 模块的 org/springframework/jdbc/support/ 包下。它定义了常见数据库(如 H2、MySQL、PostgreSQL、Oracle 等)的错误码与 DataAccessException 子类的映射关系。以下是一个简化示例片段:

<!-- 摘自 spring-jdbc 的 sql-error-codes.xml -->
<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
        <value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
    </property>
    <property name="dataIntegrityViolationCodes">
        <value>22001,22003,22012,22025,23000,23001,23505</value>
    </property>
    <property name="duplicateKeyCodes">
        <value>23505</value> <!-- 23505 是 H2 的唯一键冲突错误码 -->
    </property>
</bean>

<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
        <value>1054,1064,1146</value>
    </property>
    <property name="duplicateKeyCodes">
        <value>1062</value> <!-- 1062 是 MySQL 的唯一键冲突错误码 -->
    </property>
</bean>

该配置文件通过 SQLErrorCodes Bean 来承载每个数据库的配置。SQLErrorCodes 是一个包含多个 Set<String> 属性的 POJO,用于存储各种类别的错误码。

3.2 翻译大脑的运转:源码剖析 SQLErrorCodeSQLExceptionTranslator

// 摘自 org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExceptionTranslator {

    // 持有特定数据库的 SQLErrorCodes 配置
    private SQLErrorCodes sqlErrorCodes;

    // 构造函数之一:接受 DataSource 作为参数,用于自动探测数据库产品名
    public SQLErrorCodeSQLExceptionTranslator(DataSource dataSource) {
        this(JdbcUtils.extractDatabaseMetaData(dataSource, "getDatabaseProductName"));
    }

    // 核心翻译方法
    @Override
    protected DataAccessException doTranslate(String task, String sql, SQLException ex) {
        // 1. 获取具体的错误码
        int errorCode = ex.getErrorCode();

        // 2. 进入自定义翻译逻辑,如通过 CustomSQLExceptionTranslator 进行更具体的匹配
        if (this.sqlErrorCodes != null) {
            CustomSQLExceptionTranslator customTranslator = this.sqlErrorCodes.getCustomSqlExceptionTranslator();
            if (customTranslator != null) {
                DataAccessException dae = customTranslator.translate(task, sql, ex);
                if (dae != null) {
                    return dae;
                }
            }
        }

        // 3. 尝试用配置的 SQLErrorCodes 进行匹配
        if (this.sqlErrorCodes != null) {
            // 核心匹配算法
            if (matchInSet(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode)) {
                return new BadSqlGrammarException(task, sql, ex);
            } else if (matchInSet(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode)) {
                return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);
            } else if (matchInSet(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode)) {
                return new DuplicateKeyException(buildMessage(task, sql, ex), ex);
            } else if (matchInSet(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode)) {
                return new CannotAcquireLockException(buildMessage(task, sql, ex), ex);
            } 
            // ... 更多 else if 分支,匹配死锁、超时等
        }

        // 4. 无法匹配时的特殊处理:检查是否可能是网络断开、死锁等
        if (ex instanceof SQLRecoverableException) {
            // ... 
        }

        // 5. 所有手段用尽,返回 uncategorized 异常
        return null; // 返回null表示此翻译器无法处理,由其父类(或调用者)继续尝试下一个Translator
    }

    // 简单的匹配工具方法:在逗号分隔的错误码列表中匹配给定的错误码
    private boolean matchInSet(Set<String> codeSet, int errorCode) {
        return codeSet != null && codeSet.contains(Integer.toString(errorCode));
    }
}

源码深度解读

  1. SQLErrorCodes 获取与持有:翻译器通过 DataSource 的元数据自动提取数据库产品名(如 MySQL, PostgreSQL),然后在 Spring 容器或静态缓存中找到对应的 SQLErrorCodes 实例。
  2. doTranslate 的匹配优先链
    • 第一步:自定义翻译器。优先调用用户通过 SQLErrorCodes.setCustomSqlExceptionTranslator() 注册的自定义翻译逻辑,提供了最高的扩展性。
    • 第二步:精确错误码匹配。这是核心流程。通过一系列 if-else 判断,依次将 SQLException 的错误码与 SQLErrorCodes 中各类错误码集合(duplicateKeyCodes, badSqlGrammarCodes 等)进行匹配。匹配成功,则立即构造并返回对应语义的 DataAccessException 子类。
    • 第三步:基于异常类型的试探。例如,检查 SQLException 是否是 SQLRecoverableException 或其子类,以辅助判断网络断开等瞬时故障。
    • 第四步:返回null。若当前翻译器无法识别,则返回 null。其父类 AbstractFallbackSQLExceptionTranslator 会依次调用持有的其他翻译器(例如兜底的 SQLStateSQLExceptionTranslator)。

3.3 翻译流程序列图

以下序列图直观地展示了当一个 SQLException 被抛出时,SQLErrorCodeSQLExceptionTranslator 内部完整的翻译决策流程。

sequenceDiagram
    participant Caller as 调用者 (JdbcTemplate)
    participant SQLErrTrans as SQLErrorCodeSQLExceptionTranslator
    participant Cache as SQLErrorCodes 缓存
    participant CustomTrans as 自定义 Translator (如果有)

    Caller->>SQLErrTrans: translate(task, sql, ex)
    SQLErrTrans->>SQLErrTrans: 获取 ex.getErrorCode()
    SQLErrTrans->>Cache: 获取 SQLErrorCodes 实例
    alt 存在 CustomSQLExceptionTranslator
        SQLErrTrans->>CustomTrans: translate(task, sql, ex)
        alt 自定义翻译成功
            CustomTrans-->>SQLErrTrans: 返回自定义 DataAccessException
            SQLErrTrans-->>Caller: 返回异常
        end
    end
    SQLErrTrans->>SQLErrTrans: matchInSet(duplicateKeyCodes, errorCode)
    alt 匹配 DuplicateKey
        SQLErrTrans-->>Caller: 返回 new DuplicateKeyException(...)
    else 匹配 BadSqlGrammar
        SQLErrTrans-->>Caller: 返回 new BadSqlGrammarException(...)
    else 匹配其他预定义错误码...
        SQLErrTrans-->>Caller: 返回对应的 DataAccessException 子类
    else 所有错误码均不匹配
        SQLErrTrans->>SQLErrTrans: 检查是否为 SQLRecoverableException 等...
        alt 是特定可恢复/瞬时异常
            SQLErrTrans-->>Caller: 返回相应异常(如...CannotAcquireLockException)
        else 否
            SQLErrTrans-->>Caller: 返回 null (表示无法翻译)
        end
    end

序列图说明

  • 参与者角色JdbcTemplate 发起翻译请求;SQLErrorCodeSQLExceptionTranslator 作为翻译编排者;SQLErrorCodes 缓存提供映射数据;自定义翻译器是可选的优先处理组件。
  • 决策流:流程严格遵循“自定义优先 -> 精确错误码匹配 -> 类型试探 -> 放弃”的顺序。这种设计既保证了框架的通用定义,也为特殊需求预留了最高优先级的扩展入口。
  • 关键交互:与 SQLErrorCodes 缓存的交互是轻量级的,数据在启动时已初始化。核心的 matchInSet 调用是一个高效的字符串集合查找操作。
  • 结果处理:方法返回 null 的设计非常巧妙,允许 AbstractFallbackSQLExceptionTranslator 继续尝试下一个翻译器,完美实现了责任链模式。

3.4 自定义扩展:覆盖和增强错误码映射

Spring 的异常体系极具扩展性。你可以通过创建自定义的 sql-error-codes.xml 并将其放在 classpath 根目录下来覆盖默认配置,或通过定义一个 CustomSQLExceptionTranslator Bean 来增强翻译逻辑。 示例:为特定错误码添加自定义异常

  1. 定义一个自定义翻译器类:
    package com.example.demosqlexception;
    
    import org.springframework.jdbc.support.CustomSQLExceptionTranslator;
    import org.springframework.dao.DataAccessException;
    import java.sql.SQLException;
    
    public class MyCustomTranslator implements CustomSQLExceptionTranslator {
        @Override
        public DataAccessException translate(String task, String sql, SQLException ex) {
            if (ex.getErrorCode() == 99999) {
                // 为错误码 99999 返回一个自定义异常
                return new BusinessLogicViolationException("业务逻辑校验失败: task='" + task + "'", ex);
            }
            return null; // 返回 null,交给下一个匹配环节
        }
    }
    
  2. 配置 SQLErrorCodes 以使用该自定义翻译器(通常在 Spring XML 或 @Bean 中定义):
    @Configuration
    public class JdbcConfig {
        @Bean
        public SQLErrorCodes sqlerrorCodes(DataSource dataSource) {
             SQLErrorCodes errorCodes = new SQLErrorCodes();
             errorCodes.setDataSource(dataSource);
             errorCodes.setCustomSqlExceptionTranslator(new MyCustomTranslator());
             return errorCodes;
        }
    }
    

这种“先辈覆盖”的机制,让开发者无需修改框架源码即可精确控制异常转换行为。


4. ORM 框架的整合桥梁:PersistenceExceptionTranslator

对于 Hibernate、JPA、MyBatis 等更高抽象层次的持久化框架,它们不再抛出 SQLException,而是其各自的特定异常(如 HibernateException, PersistenceException)。Spring 通过定义一个新的、更通用的翻译接口 PersistenceExceptionTranslator 来接纳它们。

4.1 通用翻译契约:PersistenceExceptionTranslator

// 摘自 org.springframework.dao.support.PersistenceExceptionTranslator
package org.springframework.dao.support;

import org.springframework.dao.DataAccessException;

/**
 * 由持久化框架(如 Hibernate, JPA)实现的接口,用于将它们的原生异常翻译为 Spring 的 DataAccessException。
 */
@FunctionalInterface
public interface PersistenceExceptionTranslator {
    DataAccessException translateExceptionIfPossible(RuntimeException ex);
}

这个接口的语义非常直接:接收一个 RuntimeException(ORM框架的异常通常是运行时异常),然后尝试将其转换为 Spring 的 DataAccessException。如果无法转换,则返回 null

4.2 Hibernate 的整合:HibernateExceptionTranslator

HibernateExceptionTranslator 实现了 PersistenceExceptionTranslator 接口,它结构性地将 Hibernate 的原生异常转换为 Spring 的对应异常。

// 摘自 org.springframework.orm.hibernate5.HibernateExceptionTranslator
public class HibernateExceptionTranslator implements PersistenceExceptionTranslator {
    @Override
    public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
        if (ex instanceof HibernateException) {
            // 使用 JDBC 异常翻译器作为兜底,提取底层的 SQLException
            return this.jdbcExceptionTranslator.translateExceptionIfPossible(ex);
        }
        return null;
    }
}

实际上,Spring 更常用 SessionFactoryUtils.convertHibernateAccessException 作为主要的转换工具,它会分析 HibernateException 及其众多子类(如 ConstraintViolationException),并返回对应的 DataAccessException 子类。其内部实现机制与 SQLErrorCodeSQLExceptionTranslator 类似,也是基于大量的 instanceof 检查。

4.3 MyBatis 的整合:MyBatisExceptionTranslator

在 MyBatis-Spring 整合中,SqlSessionTemplate 在执行操作时会捕获 MyBatis 的 PersistenceExceptionSQLException。它内部同样持有一个 SQLExceptionTranslator 实例。其转换逻辑核心在 SqlSessionUtilsExceptionTranslaterUtil 中,本质上也是调用 SQLErrorCodeSQLExceptionTranslator 来完成。

// MyBatis-Spring 中异常翻译的简化概念伪代码
// 摘自 org.mybatis.spring.SqlSessionTemplate
public class SqlSessionTemplate implements SqlSession {
    //...
    private final SQLExceptionTranslator exceptionTranslator;

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        try {
            return this.sqlSession.selectOne(statement, parameter);
        } catch (Exception e) {
            // 调用 Spring 的翻译器
            throw translateException(e);
        }
    }

    private RuntimeException translateException(Exception e) {
        // 内部的转换工具类,最终会解包 MyBatis 异常,找到 SQLException
        // 然后调用持有的 SQLExceptionTranslator 进行翻译
        RuntimeException translated = ExceptionTranslaterUtil.translate(e, this.exceptionTranslator);
        return translated == null ? new UncategorizedDataAccessException(...) : translated;
    }
}

无论 ORM 框架如何封装,Spring 总能通过 PersistenceExceptionTranslator 这根桥梁,将形形色色的原生异常,最终统一到 DataAccessException 的旗帜下。


5. 自动翻译的魔法:PersistenceExceptionTranslationPostProcessor

如果只有手动调用的翻译器,开发者在每个 DAO 方法中仍需编写 try-catch,这种侵入性不是 Spring 想要的。完美结合核心容器 AOP 及 BeanPostProcessor 扩展点的自动翻译机制应运而生。

5.1 @Repository 注解:不仅仅是语义标记

@Repository 注解是对 @Component 的特化。在 Spring 生态中,它扮演了两个显性角色和一个隐性角色:

  • 标记数据访问组件:作为 @Component 的模板,提供语义清晰性。
  • 触发平台原生异常翻译:为 PersistenceExceptionTranslationPostProcessor 提供了识别目标 Bean 的切点。
  • (在早期 Spring 中)提供数据访问异常的特殊处理逻辑雏形,现已演化为上述自动化机制。

5.2 幕后英雄:PersistenceExceptionTranslationPostProcessor 与 AOP

PersistenceExceptionTranslationPostProcessor 是整个自动化魔法链的最后一环,它完全体现了 BeanPostProcessor 扩展点的精髓。

// 摘自 org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor
public class PersistenceExceptionTranslationPostProcessor
        extends AbstractAdvisingBeanPostProcessor implements InitializingBean {

    // 存放所有能在应用中使用的 PersistenceExceptionTranslator
    private Collection<PersistenceExceptionTranslator> persistenceExceptionTranslators;

    @Override
    public void afterPropertiesSet() {
        // 在 Bean 初始化后,找到容器中所有的 PersistenceExceptionTranslator
        this.persistenceExceptionTranslators = 
           this.beanFactory.getBeansOfType(PersistenceExceptionTranslator.class, true, false).values();
        // 创建一个 Advisor
        this.advisor = new PersistenceExceptionTranslationAdvisor(this.persistenceExceptionTranslators);
    }

    // 实现 BeanPostProcessor 的核心方法
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // 检查 Bean 的类上是否有 @Repository 注解
        if (isEligibleForTranslation(bean, beanName)) {
            // 使用父类 AbstractAdvisingBeanPostProcessor 的逻辑创建一个 AOP 代理
            bean = super.postProcessAfterInitialization(bean, beanName);
        }
        return bean;
    }

    // 判断 Bean 是否满足翻译条件:即类上是否有 @Repository 注解
    protected boolean isEligibleForTranslation(Object bean, String beanName) {
        return AnnotationUtils.findAnnotation(bean.getClass(), Repository.class) != null;
    }
}

源码深度解读

  • BeanPostProcessor 本性:它本质上是一个 BeanPostProcessor。在 postProcessAfterInitialization 阶段,它拦截所有创建好的 Bean。
  • 切点判断:通过 isEligibleForTranslation 方法,它使用 AnnotationUtils.findAnnotation 来检查当前 Bean 的类是否标注了 @Repository。这就是 @Repository 发挥选择器作用的地方。
  • AOP 代理创建:一个符合条件的 Bean 被发现,它便在 Bean 的后处理阶段,通过 Advisor 对该 Bean 创建一个 AOP 动态代理。这个 Advisor 的核心是一个 PersistenceExceptionTranslationAdvisor,其内部封装了一个 PersistenceExceptionTranslationInterceptor
  • 拦截器工作:当外部调用经过代理到达 @Repository Bean 的方法时,这个拦截器会将整个方法调用包裹在一个 try-catch(RuntimeException e) 块中。一旦捕获到任何 RuntimeException,它就会遍历持有的所有 PersistenceExceptionTranslator,尝试翻译这个异常。如果任何一个翻译器返回了非 nullDataAccessException,代理就会抛出这个翻译后的异常;否则,原始异常继续向上抛出。

5.3 BeanPostProcessor 创建代理的序列图

下面的序列图完整展示了从容器启动到自动翻译生效的全过程:

sequenceDiagram
    participant Container as Spring IoC 容器
    participant PP as PersistenceExceptionTranslationPostProcessor (implements BeanPostProcessor)
    participant UserRepo as UserRepository (@Repository)
    participant Proxy as AOP 代理对象
    participant Translator as PersistenceExceptionTranslator (如 HibernateExceptionTranslator)

    Container->>UserRepo: 1. 实例化原始 Bean
    Container->>PP: 2. postProcessAfterInitialization(bean, beanName)
    PP->>PP: 3. isEligibleForTranslation(bean): 检查是否有 @Repository 注解
    alt 是目标 Bean
        PP->>PP: 4. 获取 Advisor (内嵌 PersistenceExceptionTranslationInterceptor)
        PP-->>Container: 5. 返回被代理后的 Bean (Proxy)
        Note over Container, Proxy: 此时容器中持有的是 Proxy
    else 否
        PP-->>Container: 返回原始 Bean
    end

    Note over UserRepo, Proxy: --- 运行时,外部调用 ---
    Client->>Proxy: 6. 调用 findById() 方法
    Proxy->>PP: 调用 PersistenceExceptionTranslationInterceptor.invoke()
    Note over Proxy,UserRepo: 7. 拦截器包裹 try-catch
    Proxy->>UserRepo: 8. 调用原始 Bean 的 findById()
    UserRepo->>Database: 9. 执行数据库操作
    Database-->>UserRepo: 10. 抛出 HibernateException
    UserRepo-->>Proxy: 11. 抛出 HibernateException
    Note over Proxy: 12. 拦截器捕获 HibernateException
    Proxy->>Translator: 13. translateExceptionIfPossible(HibernateException)
    Translator-->>Proxy: 14. 返回 DataIntegrityViolationException
    Note over Proxy: 15. 拦截器抛出翻译后的异常
    Proxy-->>Client: 16. 抛出 DataIntegrityViolationException

序列图说明

  • 启动时的织入:步骤1至5清晰展示了BeanPostProcessor在启动时对目标Bean进行“偷梁换柱”的过程。这是Spring AOP与IoC结合的典型模式。
  • 运行时的拦截:步骤6至16展示了代理对象如何工作。关键点在于,PersistenceExceptionTranslationInterceptor 在步骤12静默捕获了原生异常,并在步骤13-14利用Translator完成了转换,最终在步骤16将符合Spring体系的新异常抛出。
  • 完全的透明性:整个异常翻译过程对业务调用者(Client)和UserRepository内部实现都是透明的。UserRepository仍然可以按照ORM框架的原生 API 编程并抛出其原生异常,而调用者接收到的却是标准的 DataAccessException
  • 关键扩展点PersistenceExceptionTranslationPostProcessor 完美示范了如何利用 BeanPostProcessor 这一扩展点,在不修改目标类任何代码的情况下,为其动态添加横切性关注点(异常翻译),这是Spring开放-闭合原则的绝佳体现。

5.4 内联示例:验证 @Repository 的自动翻译

我们可以通过一个简单的示例来验证这个过程。假设我们有一个基于Hibernate的UserRepository

// UserRepository.java
@Repository // 这个注解是触发器
public class UserRepository {
    @PersistenceContext
    private EntityManager em;

    // 该方法会因主键冲突而抛出 Hibernate 的 ConstraintViolationException
    public void createUser(User user) {
        em.persist(user);
        // 假设 flush 被立即执行并引发异常
        em.flush();
    }
}

在测试中,当调用userRepository.createUser(sameUser)两次时,外部捕获到的将不再是Hibernate的ConstraintViolationException,而是Spring的DataIntegrityViolationException。更进一步,对于主键冲突,你甚至可以捕获到 DuplicateKeyException。此机制完全由PersistenceExceptionTranslationPostProcessor驱动。


6. 与 JdbcTemplate 的深入协同

JdbcTemplateSQLExceptionTranslator 最经典的应用场景。它将模板方法模式的威力与异常翻译机制完美融合,构建了一个健壮且易用的数据库操作工具。

6.1 异常捕获与翻译的协作序列图

sequenceDiagram
    participant Service as UserService
    participant JdbcTmpl as JdbcTemplate
    participant ExeStmt as execute(StatementCallback)
    participant Trans as SQLErrorCodeSQLExceptionTranslator

    Service->>JdbcTmpl: update(sql, args)
    JdbcTmpl->>ExeStmt: execute(new UpdateStatementCallback())
    ExeStmt->>Database: 执行 PreparedStatement.executeUpdate()
    Database-->>ExeStmt: 抛出 SQLException (ErrorCode: 1062)
    Note over ExeStmt: 模板方法 catch (SQLException ex) 块
    ExeStmt->>JdbcTmpl: translateException(task, sql, ex)
    JdbcTmpl->>Trans: translate(task, sql, ex)
    Trans->>Trans: doTranslate(...) 解析错误码1062
    Trans-->>JdbcTmpl: 返回 new DuplicateKeyException(...)
    Note over JdbcTmpl: 将翻译后的非受检异常抛出
    JdbcTmpl-->>Service: 抛出 DuplicateKeyException

序列图说明

  • 模板方法的控制反转JdbcTemplate 的核心方法(如 execute)定义了固定流程:获取连接、创建语句、执行、清理资源,而将变化的部分(executeUpdate)交给 StatementCallback 实现。
  • 集中的异常处理点:在 execute 方法的 JDBC 样板代码 中,有一个集中的 catch (SQLException ex) 代码块。这是所有 JDBC 异常的汇合点。
  • 翻译的触发:在此 catch 块内,JdbcTemplate 立即调用自身持有的 SQLExceptionTranslator(默认为 SQLErrorCodeSQLExceptionTranslator)的 translate 方法,并将 task(如 "PreparedStatement.executeUpdate")和 sql 传递进去。
  • 结果返回:翻译后的 DataAccessException(如 DuplicateKeyException)随后被抛出。对于上层调用者 UserService 而言,它完全看不到 SQLException 的踪迹,只需处理 Spring 定义的统一异常。

6.2 源码耦合点分析

// 摘自 org.springframework.jdbc.core.JdbcTemplate
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    // 默认构造器会初始化一个默认的翻译器
    private volatile SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(...);

    // execute 核心方法简化版
    @Override
    public <T> T execute(StatementCallback<T> action) throws DataAccessException {
        Assert.notNull(action, "Callback object must not be null");
        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        Statement stmt = null;
        try {
            stmt = con.createStatement();
            applyStatementSettings(stmt);
            return action.doInStatement(stmt);
        }
        catch (SQLException ex) {
            // 关键步骤:在这里调用翻译器,并直接将其抛出,不再需要调用者处理
            throw translateException("StatementCallback", getSql(action), ex);
        }
        catch (RuntimeException | Error ex) {
            // 处理其他运行时异常和错误
            throw ex;
        }
        finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }

    // 封装翻译调用
    protected DataAccessException translateException(String task, String sql, SQLException ex) {
        return getExceptionTranslator().translate(task, sql, ex);
    }
}

源码解读

  • JdbcTemplate 通过 translateException 方法封装了对翻译器的调用。
  • execute 方法的 catch (SQLException ex) 块中,该调用是强制且唯一的处理逻辑。这保证了所有从 JdbcTemplate 流出的异常都是 DataAccessException
  • 翻译后的异常是 RuntimeException,因此 JdbcTemplate 接口中的方法无需声明 throws DataAccessException,这完全符合 Spring 消灭受检异常的哲学。

7. 生产事故排查专题

理论必须能解释和解决实践中的问题。以下是两个典型的因对 Spring 异常体系理解不足而引发的线上事故。

7.1 事故一:DuplicateKeyException 被吞没,导致业务逻辑错乱

场景描述:某电商系统(使用 Spring Boot + MyBatis + MySQL)的用户注册流程,服务层 UserServiceImpl.register(User user) 方法调用 userMapper.insert(user)。该 SQL 可能因用户名或手机号重复而引发 DuplicateKeyException。业务逻辑期望在发生重复时返回特定错误码,告知用户“用户名已存在”。

代码(错误版本)

@Service
public class UserServiceImpl {
    @Autowired
    private UserMapper userMapper;

    @Transactional
    public Result register(User user) {
        try {
            userMapper.insert(user); // 用户名重复时,Spring 会抛出 DuplicateKeyException
            return Result.success("注册成功");
        } catch (DataAccessException e) { // 泛化的捕获
            // 事故点:此处捕获了 DataAccessException,但未区分具体的子类。
            // 开发者错误地认为所有 DataAccessException 都是“系统繁忙”,
            // 进而执行了“降级插入临时表并返回注册成功”的错误逻辑。
            insertToTemporaryTable(user);
            return Result.success("注册请求已提交,请稍后...");
        }
    }
}

事故复盘

  1. 错误的异常粒度:开发者使用过于泛化的 catch (DataAccessException e) 来捕获所有数据访问异常,忽略了 Spring 异常体系的丰富层次。
  2. 混淆故障语义:由于未区分 DataAccessException 的子类,将“唯一键冲突”这种业务上可预期的、不可恢复的异常(NonTransient),与“连接超时”等可重试的、系统性的异常(Transient)等同看待。
  3. 业务决策错误:基于错误的异常归类,业务层做出了“降级插入”的错误决策。用户看到“注册成功”的提示,但实际上其账户并未创建。这导致了严重的客诉和业务数据混乱。

正确的处理方式

@Transactional
public Result register(User user) {
    try {
        userMapper.insert(user);
        return Result.success("注册成功");
    } catch (DuplicateKeyException e) { // 精确捕获业务预期的重复异常
        // 解析异常的详细信息,确认是用户名还是手机号重复
        return Result.fail("用户名或手机号已存在");
    } catch (DataAccessException e) { // 处理其他真正的系统级数据访问异常
        log.error("系统数据访问异常,请稍后重试", e);
        return Result.fail("系统繁忙,请稍后重试");
    }
}

教训:在 Spring 框架下进行数据访问编程时,catch 子句的粒度应尽量的精细,优先捕获并处理 DataAccessException 的语义化子类。这本身就是 Spring 精心设计此异常层次的初衷。

7.2 事故二:更换数据库后,死锁不触发重试机制

场景描述:一个会计系统从 MySQL 迁移到 PostgreSQL。系统中有一个重要的转账服务,为了防止资源争抢,设计了基于 CannotAcquireLockException 的自动重试逻辑。该逻辑在 MySQL 上运行良好,但迁移到 PostgreSQL 后,重试机制完全失效。

原理分析

  • 在 MySQL(默认 InnoDB 引擎)中,事务等待锁超时会抛出 SQLException,其错误码为 1205。Spring 的 sql-error-codes.xml 中,MySQL 的 cannotAcquireLockCodes 集合包含了 1205。因此,Spring 能正确将其翻译为 CannotAcquireLockException
  • 在 PostgreSQL 中,锁等待超时导致的异常对应于错误码 55P03lock_not_available)。但是,在旧版本的 spring-jdbc 依赖中,PostgreSQL 的 cannotAcquireLockCodes 配置里缺失55P03 这个错误码。
  • 因此,当 PostgreSQL 发生锁等待超时时,Spring 无法将其精确翻译为 CannotAcquireLockException,而是将其降级翻译为 TransientDataAccessResourceException 或更通用的 UncategorizedDataAccessException
  • 由于转账服务的重试逻辑只捕获了 CannotAcquireLockException,导致这个异常被漏掉,进而被通篇的错误处理逻辑捕获,直接标记转账失败,而没有进行任何重试。

解决方案

  1. 升级依赖:首先检查 spring-jdbc 版本,新版本通常已修复此类映射缺失。
  2. 自定义映射(根除方案):创建自定义的 sql-error-codes.xml 文件,为所用数据库补充或覆盖错误码映射,确保未来不再出现类似问题。
    <!-- 自定义 sql-error-codes.xml,放在 classpath 根目录 -->
    <bean id="PostgreSQL" class="org.springframework.jdbc.support.SQLErrorCodes">
        <!-- 补充 PostgreSQL 的死锁和锁等待错误码 -->
        <property name="cannotAcquireLockCodes">
            <value>55P03, ...</value>
        </property>
        <property name="deadlockLoserCodes">
            <value>40P01</value>
        </property>
    </bean>
    

教训:依赖于特定数据库错误码的异常翻译,必须考虑迁移场景。**在项目初期或更换基础设施后,应专门测试各类预期的数据访问异常是否都能被正确翻译为预想的 DataAccessException 子类 **。自定义 sql-error-codes.xml 是实现跨数据库一致异常行为的进阶必修课。


8. 面试高频专题

以下问题旨在系统性地检验对 Spring 数据访问异常体系的理解,而非死记硬背。

  1. Spring 为什么要封装 JDBC 的 SQLException

    • :主要三个目的:① 将受检异常转为非受检异常,解放业务代码;② 将散乱的数据库厂商错误码统一为语义化的异常层次,实现平台解耦;③ 通过NestedRuntimeException保留异常根因,并提供标准化的分类(Non/Transient),为事务管理等上层服务提供决策依据。
  2. DataAccessException 体系的结构是怎样的?

    • :它继承自NestedRuntimeException,是RuntimeException的子类。体系的核心是三个抽象分支:NonTransientDataAccessException(不可重试)、TransientDataAccessException(可重试)和RecoverableDataAccessException(可恢复)。每个分支下包含大量具体异常,如DataIntegrityViolationExceptionQueryTimeoutException等,形成三层树状结构。
  3. SQLErrorCodeSQLExceptionTranslator 是如何工作的?错误码从哪里来?

    • :它首先通过DataSource获取数据库产品名,然后加载对应的SQLErrorCodes对象。该对象的数据源是spring-jdbc中的sql-error-codes.xml配置文件,其中定义了各种数据库(H2, MySQL, PostgreSQL等)的错误码到Spring异常子类的映射。翻译时,它会获取SQLException.getErrorCode(),并通过matchInSet方法在配置的映射集中查找,找到匹配则实例化对应的DataAccessException
  4. 如何自定义异常翻译规则?

    • :有两种主要方式:① 覆盖:在 classpath 根目录下创建自定义的 sql-error-codes.xml,可完全或部分覆盖默认的错误码映射。② 增强/优先:实现 CustomSQLExceptionTranslator 接口,编写自己的翻译逻辑,并通过配置 SQLErrorCodessetCustomSqlExceptionTranslator 方法注册,它将获得最高匹配优先级。
  5. @Repository 注解结合 PersistenceExceptionTranslationPostProcessor 有什么作用?

    • PersistenceExceptionTranslationPostProcessor 是一个 BeanPostProcessor。它检查所有初始化后的 Bean,如果 Bean 的类上标注了 @Repository 注解,它就会为这个 Bean 动态创建一个 AOP 代理。该代理中的拦截器会捕获方法调用中的任何 RuntimeException,并利用 PersistenceExceptionTranslator 将其翻译为 Spring 的 DataAccessException
  6. 在 MyBatis 或 Hibernate 中,Spring 是如何统一异常处理的?

    • :Spring 定义了 PersistenceExceptionTranslator 接口。Hibernate 提供了 HibernateExceptionTranslator 实现此接口,将 HibernateException 转换;MyBatis-Spring 在 SqlSessionTemplate 内部,通过捕获 MyBatis 的异常并调用持有的 SQLExceptionTranslator 来完成转换,其作用原理类似。它们都遵循“将专有异常翻译为通用 DataAccessException”这一核心契约。
  7. DataAccessException 和普通 RuntimeException 的区别是什么?

    • :其父类 NestedRuntimeException 提供了内置的异常链管理能力。更重要的是,DataAccessException 的丰富子类体系赋予了异常明确的语义,如 DuplicateKeyException 能精确表达故障原因,TransientDataAccessException 能暗示操作可重试。这远非一个普通的 RuntimeException 可比。
  8. TransientDataAccessException 是什么意思?何时用到?

    • :表示“暂时性”的数据访问异常,即该操作在当前状态下失败,但在稍后重试可能成功。典型场景包括死锁、事务回滚导致的状态失效、查询超时、连接获取超时等。上层应用可以基于此异常类型来实施自动重试策略。
  9. 为什么在 Spring 中很少见到 try-catch 处理 SQLException

    • :因为 Spring 的数据访问组件(如 JdbcTemplateSqlSessionTemplate)及自 @Repository 代理机制,已经将所有 SQLException 及其等价物翻译成了非受检的 DataAccessException。开发者从根源上就不必再与受检异常打交道,只需根据业务需要捕获 DataAccessException 层次中对自己有意义的结点。
  10. JdbcTemplate 如何处理异常?

    • JdbcTemplate 在核心模板方法(如 execute)中,使用集中式的 catch (SQLException ex) 代码块捕获所有 JDBC 受检异常。然后,它调用内部持有的 SQLExceptionTranslator 实例(默认 SQLErrorCodeSQLExceptionTranslator)的 translate 方法,将 SQLException 转换为某种 DataAccessException 子类并重新抛出。
  11. PersistenceExceptionTranslationPostProcessorBeanPostProcessor 吗?它做了什么?

    • :是的,它直接实现了 BeanPostProcessor 接口。它的核心工作在 postProcessAfterInitialization 方法中完成,可以概括为:“找到所有 @Repository 组件,并用一个具有异常翻译拦截能力的 AOP 代理替换它。”
  12. (系统设计题)设计一个统一数据访问中间件,要求能屏蔽底层 MySQL、PostgreSQL 和 MongoDB 的差异,对上层业务只抛出统一的非受检异常。请参考Spring的DataAccessException体系,给出关键接口和异常转换链路的伪代码。

    • 方案概要
      1. 定义统一异常层次:创建 UnifiedDataException 抽象类(继承自 RuntimeException),并建立类似 TemporaryExceptionPermanentExceptionQuerySyntaxExceptionUniqueConstraintException 等子类体系。
      2. 定义 ExceptionTranslator<T extends Throwable> 策略接口:为不同的数据访问技术(SQL与NoSQL)设计各自的异常翻译器。
        interface ExceptionTranslator<T extends Throwable> {
            UnifiedDataException translate(T nativeException);
        }
        // SQL 翻译器
        class SqlExceptionTranslator implements ExceptionTranslator<SQLException> {
            // 内部同样维护一份错误码映射表
            private Map<String, Class<? extends UnifiedDataException>> codeMappings;
            // ...
            public UnifiedDataException translate(SQLException ex) { /* 映射逻辑 */ }
        }
        // MongoDB 翻译器
        class MongoExceptionTranslator implements ExceptionTranslator<MongoException> {
            public UnifiedDataException translate(MongoException ex) {
                if (ex instanceof MongoDuplicateKeyException) return new UniqueConstraintException(...);
                // ... 其他映射
            }
        }
        
      3. 定义 UnifiedTemplate 模板类:类似于 JdbcTemplate。各个具体实现(如 JdbcUnifiedTemplateMongoUnifiedTemplate)在其核心执行方法中捕获原生异常,并调用对应的 ExceptionTranslator 进行翻译。
      4. 自动化翻译(可选):通过 AOP 拦截所有标有 @DataAccess 注解的组件,根据配置的 ExceptionTranslator 链自动翻译异常,这与 PersistenceExceptionTranslationPostProcessor 的实现思路如出一辙。
      5. 伪代码翻译链路
        // 在 JDBC Template 的伪代码中
        try {
            stmt.executeUpdate(sql);
        } catch (SQLException ex) {
            throw getSqlTranslator().translate(ex);
        }
        
        // 在 MongoDB Template 的伪代码中
        try {
            collection.insertOne(document);
        } catch (MongoException ex) {
            throw getMongoTranslator().translate(ex);
        }
        
      • 结论:通过定义固定的异常契约和翻译器策略,我们可以斩断业务调用的技术依赖,使得无论底层数据库如何切换,上层捕获处理的始终是 UniqueConstraintException 等具有明确业务语义的统一异常。

DataAccessException 体系速查表

抽象分支异常子类常见触发原因语义/建议
NonTransient (不可恢复)BadSqlGrammarException错误的表名、列名、SQL关键字拼写错误等。语法错误,必须修复SQL语句。
DataIntegrityViolationException非空字段为空、外键约束、类型错误等。数据完整性违规,需校验数据。
DuplicateKeyException主键或唯一键冲突。DataIntegrityViolationException的子类,可精确捕获处理。
Transient (暂时性/可重试)QueryTimeoutException查询执行超过预设的超时时间。可尝试优化SQL或增加超时阈值,或直接重试。
TransientDataAccessResourceException连接池耗尽、获取连接超时等。资源暂时不可用,可重试。
CannotAcquireLockException尝试获取锁但失败(如MySQL的1205,PG的55P03)。表示当前的并发状态,可重试事务。
Recoverable (可恢复)DeadlockLoserDataAccessException事务因在死锁中成为牺牲品而回滚。数据库自动解除死锁后的产物,可安全重试事务。
通用UncategorizedSQLException任何被翻译器捕获但无法精确映射的JDBC/SQL异常。潜在问题,应排查错误码并考虑添加到映射表。

延伸阅读

  • Spring Framework 官方文档Data Access 一章,尤其是关于异常翻译的部分。
  • 《Spring 揭秘》:王福强著,对 Spring 的各个模块,包括事务和持久化有透彻的讲解。
  • Spring 官方源码spring-jdbcspring-tx 模块,是深入理解该体系的最佳路径。