一、Spring JDBC 异常体系简介
当我们使用 Spring JDBC 进行数据访问时,大多数人关注的是 JdbcTemplate 如何简化数据库操作,却很少有人去深入理解异常体系。事实上,异常不仅仅是错误提示,它是系统健壮性、可维护性的重要一环。JDBC 原生的 SQLException
是受检异常,迫使你在每一层写 try-catch
,而 Spring 将其包装成 DataAccessException(运行时异常,无需代码处理)
,让代码更简洁,异常处理更集中。
二、异常处理体系源码分析
关键包与类一览
- 统一异常层(与数据访问无关的通用抽象)
org.springframework.dao.*
核心:DataAccessException
(抽象,继承自RuntimeException
) - JDBC 侧实现与转换器
org.springframework.jdbc.support.*
核心:SQLExceptionTranslator
(接口)SQLErrorCodeSQLExceptionTranslator
(按厂商errorCode
转换)SQLStateSQLExceptionTranslator
(按 SQLState 转换)SQLExceptionSubclassTranslator
(按 JDBC 异常子类转换)
DataAccessException
所有数据访问异常的统一父类,它的作用是将不同数据库、不同持久化技术抛出的异常(如 JDBC、Hibernate、JPA 等)进行统一封装和抽象。DataAccessException 源码在 org.springframework.dao 包中
package org.springframework.dao;import org.springframework.core.NestedRuntimeException;public abstract class DataAccessException extends NestedRuntimeException {// 构造方法,传入异常消息public DataAccessException(String msg) {super(msg);}// 构造方法,传入异常消息和底层异常public DataAccessException(String msg, Throwable cause) {super(msg, cause);}
}
常见子类及应用
Spring 根据异常分类进一步细化,如:
DataIntegrityViolationException
:违反数据完整性约束(唯一键、外键等)。DataAccessResourceFailureException
:数据库资源访问失败(连接失败等)。DuplicateKeyException
:唯一键冲突。CannotAcquireLockException
:锁无法获取。OptimisticLockingFailureException
:乐观锁失败。PermissionDeniedDataAccessException
:权限不足。
SQLExceptionTranslator
SQLExceptionTranslator 是 Spring JDBC 异常转换机制的核心接口,它的作用是将底层 SQLException 转换成统一的异常(即 DataAccessException 及其子类)
源码位置:org.springframework.jdbc.support.SQLExceptionTranslator,接口定义如下
package org.springframework.jdbc.support;import java.sql.SQLException;import org.springframework.dao.DataAccessException;
import org.springframework.lang.Nullable;@FunctionalInterface
public interface SQLExceptionTranslator {// 将 SQLException 转换成 DataAccessException@NullableDataAccessException translate(String task, @Nullable String sql, SQLException ex);
}
要点:
- 单一方法
translate
:接收参数:任务描述、SQL 语句、原始 SQLException。 - 返回值:DataAccessException(或其子类)。
主要实现类:
SQLErrorCodeSQLExceptionTranslator
基于数据库错误码映射SQLStateSQLExceptionTranslator
基于 SQLState 分类SQLExceptionSubclassTranslator
基于SQLException 子类类型
SQLErrorCodeSQLExceptionTranslator
SQLErrorCodeSQLExceptionTranslator作用是将数据库抛出的 SQLException 基于错误码(error code)映射到 Spring 统一的异常体系 DataAccessException
核心源码方法
@Override
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {SQLException sqlEx = ex;// 如果异常是 BatchUpdateException,并且内部还有嵌套异常(getNextException()),则取最内层的异常。if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {SQLException nestedSqlEx = sqlEx.getNextException();if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) {sqlEx = nestedSqlEx;}}// 先走开发者自定义逻辑,如果子类重写了 customTranslate() 方法,可以自定义转换逻辑。DataAccessException dae = customTranslate(task, sql, sqlEx);if (dae != null) {return dae;}// 获取数据库厂商错误码SQLErrorCodes sqlErrorCodes = getSqlErrorCodes();if (sqlErrorCodes != null) {// 如果配置了 SQLErrorCodes,并且定义了 customSqlExceptionTranslator,优先调用它。SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator();if (customTranslator != null) {DataAccessException customDex = customTranslator.translate(task, sql, sqlEx);if (customDex != null) {return customDex;}}}// 根据错误码映射if (sqlErrorCodes != null) {String errorCode;if (sqlErrorCodes.isUseSqlStateForTranslation()) {errorCode = sqlEx.getSQLState();}else {// 遍历 cause,找到 errorCode 非 0 的 SQLExceptionSQLException current = sqlEx;while (current.getErrorCode() == 0 && current.getCause() instanceof SQLException) {current = (SQLException) current.getCause();}errorCode = Integer.toString(current.getErrorCode());}if (errorCode != null) {// 如果在 SQLErrorCodes 中配置了 customTranslations(用户自定义映射),优先使用它CustomSQLErrorCodesTranslation[] customTranslations = sqlErrorCodes.getCustomTranslations();if (customTranslations != null) {for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) {if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 &&customTranslation.getExceptionClass() != null) {DataAccessException customException = createCustomException(task, sql, sqlEx, customTranslation.getExceptionClass());if (customException != null) {logTranslation(task, sql, sqlEx, true);return customException;}}}}// 按 Spring 预定义错误码分类翻译if (Arrays.binarySearch(sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new BadSqlGrammarException(task, (sql != null ? sql : ""), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new InvalidResultSetAccessException(task, (sql != null ? sql : ""), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new DuplicateKeyException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new DataIntegrityViolationException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new PermissionDeniedDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new DataAccessResourceFailureException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new TransientDataAccessResourceException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new CannotAcquireLockException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new DeadlockLoserDataAccessException(buildMessage(task, sql, sqlEx), sqlEx);}else if (Arrays.binarySearch(sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {logTranslation(task, sql, sqlEx, false);return new CannotSerializeTransactionException(buildMessage(task, sql, sqlEx), sqlEx);}}}// 如果没有匹配,返回 null,由 fallback 翻译器继续if (logger.isDebugEnabled()) {String codes;if (sqlErrorCodes != null && sqlErrorCodes.isUseSqlStateForTranslation()) {codes = "SQL state '" + sqlEx.getSQLState() + "', error code '" + sqlEx.getErrorCode();}else {codes = "Error code '" + sqlEx.getErrorCode() + "'";}logger.debug("Unable to translate SQLException with " + codes + ", will now try the fallback translator");}return null;
}
错误码配置来源
SQLErrorCodeSQLExceptionTranslator 使用 SQLErrorCodes 对象,该对象包含各种数据库的错误码映射。
配置文件:
org/springframework/jdbc/support/sql-error-codes.xml(Spring 内置),如MySQL部分如下:
<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes"><property name="databaseProductNames"><list><value>MySQL</value><value>MariaDB</value></list></property><property name="badSqlGrammarCodes"><value>1054,1064,1146</value></property><property name="duplicateKeyCodes"><value>1062</value></property><property name="dataIntegrityViolationCodes"><value>630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557</value></property><property name="dataAccessResourceFailureCodes"><value>1</value></property><property name="cannotAcquireLockCodes"><value>1205,3572</value></property><property name="deadlockLoserCodes"><value>1213</value></property>
</bean>
Spring 会根据 DataSource 的元数据(DatabaseMetaData.getDatabaseProductName()) 自动选择对应数据库的错误码配置。
SQLStateSQLExceptionTranslator
SQLStateSQLExceptionTranslator会根据 SQLException 的 SQLState(标准 SQL 错误码)来转换为 Spring的DataAccessException
SQLState
SQLState 是 JDBC 提供的标准错误码,用于标识数据库操作的错误或状态,一般由5 位字符串,前两位:表示错误类别(Class Code),后两/三位:表示具体错误子类(Subclass Code)
核心源码
@Override
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {// 提取 SQLException 的 SQLState(标准5位错误码),取前两位作为“异常类别”String sqlState = getSqlState(ex);if (sqlState != null && sqlState.length() >= 2) {String classCode = sqlState.substring(0, 2);if (logger.isDebugEnabled()) {logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'");}// 根据 SQLState 类匹配 Spring 异常if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) {return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);}else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) {return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);}else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) {return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);}else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) {return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);}else if (CONCURRENCY_FAILURE_CODES.contains(classCode)) {return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);}}// 超时处理,检查异常类名是否包含 “Timeout”,如果是则返回 QueryTimeoutExceptionif (ex.getClass().getName().contains("Timeout")) {return new QueryTimeoutException(buildMessage(task, sql, ex), ex);}// 如果没有匹配到任何类别,最终可能被封装为 UncategorizedSQLExceptionreturn null;
}
SQLExceptionSubclassTranslator
主要作用是将标准的 java.sql.SQLException 及其子类异常转换为 Spring 自己定义的.DataAccessException异常体系。
核心源码
@Override
@Nullable
protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) {if (ex instanceof SQLTransientException) {// 处理“瞬时异常”(Transient):这类异常可能在稍后重试时成功。// 例如:数据库死锁、查询超时等。// 根据更具体的子类进行精细翻译if (ex instanceof SQLTransientConnectionException) {//连接相关的瞬时异常,例如连接池暂时无法获取连接return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex);}else if (ex instanceof SQLTransactionRollbackException) { // 事务回滚异常,例如死锁return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex);}else if (ex instanceof SQLTimeoutException) { // 查询执行超时return new QueryTimeoutException(buildMessage(task, sql, ex), ex);}}// 处理“非瞬时异常”(Non-Transient):这类异常通常不是重试能解决的,是更根本性的报错。else if (ex instanceof SQLNonTransientException) {if (ex instanceof SQLNonTransientConnectionException) {// 连接相关的非瞬时异常,例如无法建立数据库连接、用户名密码错误return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex);}//数据问题,例如数据类型错误、数据超出长度else if (ex instanceof SQLDataException) {return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);}// 完整性约束违反else if (ex instanceof SQLIntegrityConstraintViolationException) {return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex);}// 授权失败,例如权限不足else if (ex instanceof SQLInvalidAuthorizationSpecException) {return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex);}else if (ex instanceof SQLSyntaxErrorException) {// 无效的SQL语法return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex);}else if (ex instanceof SQLFeatureNotSupportedException) {return new InvalidDataAccessApiUsageException(buildMessage(task, sql, ex), ex);}}// 处理“可恢复异常”:这类异常介于瞬态和非瞬态之间,应用程序可能能够从中恢复。// 例如:连接意外中断后可能重新连接成功。else if (ex instanceof SQLRecoverableException) {return new RecoverableDataAccessException(buildMessage(task, sql, ex), ex);}// 如果传入的SQLException不属于任何已知的标准子类,则返回null。return null;
}
逻辑总结
基于 instanceof 对 SQLException 进行类型检查,将其分为三大类:
SQLTransientException
:瞬时异常。通常意味着操作可以稍后重试并可能成功(如死锁、超时)。SQLNonTransientException
:非瞬时异常。意味着存在根本性问题,重试无法解决(如语法错误、违反约束)。SQLRecoverableException
:可恢复异常。一种中间状态,应用程序或许能恢复(如连接中断后重连)。
调用顺序
- SQLErrorCodeSQLExceptionTranslator默认优先使用的转换器,它依赖于 sql-error-codes.xml 配置文件,将特定数据库的错误代码映射到最具体DataAccessException 子类
- SQLExceptionSubclassTranslator,按 JDBC 标准子类
- SQLStateSQLExceptionTranslator,按 SQLState 前两位
在每个Translator的构造方法中代码中可以看到,通过 setFallbackTranslator 把链路串起来:
SQLErrorCodeSQLExceptionTranslator
public SQLErrorCodeSQLExceptionTranslator() {setFallbackTranslator(new SQLExceptionSubclassTranslator());
}
SQLExceptionSubclassTranslator
public SQLExceptionSubclassTranslator() {setFallbackTranslator(new SQLStateSQLExceptionTranslator());
}