千家信息网

Spring转换SQLException并埋入扩展点的工作过程

发表于:2025-11-15 作者:千家信息网编辑
千家信息网最后更新 2025年11月15日,本篇内容介绍了"Spring转换SQLException并埋入扩展点的工作过程"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家
千家信息网最后更新 2025年11月15日Spring转换SQLException并埋入扩展点的工作过程

本篇内容介绍了"Spring转换SQLException并埋入扩展点的工作过程"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

SQLException 中字段 SQLState 和 vendorCode

我不知道这样理解是否正确:

SQLState 的来源是对 SQL 标准的履约。尽管 SQL 数据库的厂商有很多,只要它们都尊重 SQL 标准和 JDBC 的规范,那么不同厂商在同样的错误上必然返回同样的 SQLState。

vendorCode 很魔幻,它对应的 getter 方法名为 getErrorCode。但字段名里的 vendor 和它的 getter 方法上的注解已经将出卖了它的意义。vendorCode 或者说 errorCode 并不是 SQL 标准的内容,不同数据库厂商可能对同样的错误返回不同的 errorCode。

SQLErrorCodeSQLExceptionTranslator 是如何注册为 Bean 的

我本来猜测默认的转换器 org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator 在 SpringBoot 环境下,在引入 spring-boot-starter-jdbc 依赖后会在某个 *AutoConfiguration 中被一个带有 @Bean 的方法注册,这个方法同时可能还将也注册为 Bean 的 SQLErrorCodes 通过 setter 注入。

然而后者,即默认的 SQLErrorCodes 的注册为 Bean 的地方我找到了, org\springframework\jdbc\support\sql-error-codes.xml,在这个里面注册并注入了一系列属性的。然而我对 SQLErrorCodeSQLExceptionTranslator 和它的父类借助IDEA的 Alt + F7 检索被使用的地方并没发现上一段说的注册它为 Bean 的方法。如果有老哥知道它在哪儿成为 Bean 加入上下文环境的告诉我一声。我先把假设当事实用着了。

批处理产生的异常只关注尾部吗

请看正文的第一段。

正文

org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator,这是 Spring 默认提供的转换各种 SQLException 异常为Spring 内置的统一的异常类型的转换器。本文主要通过它,准确地说是它的 protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) 方法来看一看它整个的处理思路以及其中给用户流出的扩展点。

第一段

         SQLException sqlEx = ex;                if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) {                        SQLException nestedSqlEx = sqlEx.getNextException();                        if (nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null) {                                sqlEx = nestedSqlEx;                        }                }

上面是 doTranslate 方法入门第一段,它做的事情很简单,起手先判断这个异常是不是 BatchUpdateException 类型的从名字上猜,这个异常只会在批处理时抛出。而之后的sqlEx.getNextException(),我们追进源码去看,有这些重要相关代码:

    /**     * Retrieves the exception chained to this     * SQLException object by setNextException(SQLException ex).     *     * @return the next SQLException object in the chain;     *         null if there are none     * @see #setNextException     */    public SQLException getNextException() {        return (next);    }    /**     * Adds an SQLException object to the end of the chain.     *     * @param ex the new exception that will be added to the end of     *            the SQLException chain     * @see #getNextException     */    public void setNextException(SQLException ex) {        SQLException current = this;        for(;;) {            SQLException next=current.next;            if (next != null) {                current = next;                continue;            }            if (nextUpdater.compareAndSet(current,null,ex)) {                return;            }            current=current.next;        }    }

一阅读 setNextException 就知道,玩链表的老手了,next 一定永远指向链表最末元素。

那在看回转换器的代码,那么它做的事情就是:取出异常链的最末尾异常,然后判断 nestedSqlEx.getErrorCode() > 0 || nestedSqlEx.getSQLState() != null,如果通过,则 sqlEx 就置为这个最末尾异常。而阅读之后的代码后能知道,sqlEx 就是会被 Spring 转换处理的目标。

也就是说只要末尾的SQLException是个正常的异常,Spring 就只关心末尾的异常了。这是为什么?

另外,为什么 SQLException 要搞一个异常链呢?顶层 Exception 不是已经设置了 cause 这样一个机制来实现异常套娃吗?

第二段 第一个扩展点

              // First, try custom translation from overridden method.                DataAccessException dae = customTranslate(task, sql, sqlEx);                if (dae != null) {                        return dae;                }

customTranslate 这个方法光听名字就很有扩展点的感觉,跳转过去看下它的代码:

   @Nullable        protected DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlEx) {                return null;        }

第一个埋入的扩展点出现了。这个方法的存在,使得我们可以通过继承 SQLErrorCodeSQLExceptionTranslator 类进行扩展,装入自己的私活。我猜测注册 SQLErrorCodeSQLExceptionTranslator 到环境中的那个 Bean 方法应该是有 @ConditionalOnMissingBean 在的,我们手动将自己实现的继承 SQLErrorCodeSQLExceptionTranslator 的类注册为 Bean 后它自己就不会再注册了,从而实现偷换转换器夹带私活。

第三段 第二和第三个扩展点

           // Next, try the custom SQLException translator, if available.                SQLErrorCodes sqlErrorCodes = getSqlErrorCodes();                if (sqlErrorCodes != null) {                        SQLExceptionTranslator customTranslator = sqlErrorCodes.getCustomSqlExceptionTranslator();                        if (customTranslator != null) {                                DataAccessException customDex = customTranslator.translate(task, sql, sqlEx);                                if (customDex != null) {                                        return customDex;                                }                        }                }

SQLErrorCodes 就是之前在 org\springframework\jdbc\support\sql-error-codes.xml 注册的 Bean 的类。而在那个 xml 文件的开头,人家明确说了:

- Default SQL error codes for well-known databases.- Can be overridden by definitions in a "sql-error-codes.xml" file- in the root of the class path.

如果你在自己的项目的 class path 下写一个 sql-error-codes.xml ,那么 Spring 默认提供的就会被覆盖。

注意 sqlErrorCodes.getCustomSqlExceptionTranslator()。这一步从一个 Bean 中取出它的一个成员,而这个成员是完全有 getter 显然也有 setter,也就意味着它可以在 SQLErrorCodes 注册为 Bean 的同时的一个成员 Bean 注入。

结合 sql-error-codes.xml 头部的注释,于是有:用户先写一个夹带自己私活的 SQLExceptionTranslator 接口的实现类。然后自己在项目 class path 下新建一个 sql-error-codes.xml,复制 Spring 已经提供的内容。再然后,在 xml 里将自己的私活 SQLExceptionTranslator 实现类注册为 Bean,并在复制过来的注册 SQLErrorCodes 为 Bean 的内容里添加一条 customSqlExceptionTranslator 的成员的注入,用的当然是自己的那个私活 Bean。这样就完成了扩展。

其实这里还有第三个扩展点。注意第一行的 getSqlErrorCodes(); ,有这个 getter 也有 setter 还有 sqlErrorCodes 的成员,这里 SQLErrorCodes 也是后注入给 ``SQLErrorCodeSQLExceptionTranslator的组件。那我们也可以自定义一个SQLErrorCodes的实现类然后注册为 Bean,取代默认的。那SQLErrorCodes` 都彻底换了个,夹带私活肯定没问题了。

第四段 准备工作

第四段也是最后一段,这段比较长,段内部还需分片看。

          // Check SQLErrorCodes with corresponding error code, if available.                if (sqlErrorCodes != null) {                        String errorCode;                        if (sqlErrorCodes.isUseSqlStateForTranslation()) {                                errorCode = sqlEx.getSQLState();                        }                        else {                                // Try to find SQLException with actual error code, looping through the causes.                                // E.g. applicable to java.sql.DataTruncation as of JDK 1.6.                                SQLException current = sqlEx;                                while (current.getErrorCode() == 0 && current.getCause() instanceof SQLException) {                                        current = (SQLException) current.getCause();                                }                                errorCode = Integer.toString(current.getErrorCode());                        }

这一段是做了一些准备工作。

        /**         * Set this property to true for databases that do not provide an error code         * but that do provide SQL State (this includes PostgreSQL).         */        public void setUseSqlStateForTranslation(boolean useStateCodeForTranslation) {                this.useSqlStateForTranslation = useStateCodeForTranslation;        }        public boolean isUseSqlStateForTranslation() {                return this.useSqlStateForTranslation;        }

setUseSqlStateForTranslation 方法的注释我们可以推测,isUseSqlStateForTranslation() 返回 true 时,数据库厂商是那种 JDBC 执行出错不返回 errorCode 只返回 SQLState 的,两者都返回的这里应该返回 false (两者都不返回的那不是正常的 JDBC 实现)。在此基础上继续回去理解代码,那这里就是获取待转化的异常中最有价值的可以表明自己错误的异常的对应的错误码置给 errorCode。而后续操作都基于这个 errorCode

第四段 第四个扩展点

                  if (errorCode != null) {                                // Look for defined custom translations first.                                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;                                                        }                                                }                                        }                                }

这个第四个扩展点,就是可以注入到 SQLErrorCodes 中的 CustomSQLErrorCodesTranslation[],细看 CustomSQLErrorCodesTranslation 这个类:

/** * JavaBean for holding custom JDBC error codes translation for a particular * database. The "exceptionClass" property defines which exception will be * thrown for the list of error codes specified in the errorCodes property. * * @author Thomas Risberg * @since 1.1 * @see SQLErrorCodeSQLExceptionTranslator */public class CustomSQLErrorCodesTranslation {        private String[] errorCodes = new String[0];        @Nullable        private Class exceptionClass;

我没复制粘贴完,因为这些已经够了,看注释:

The "exceptionClass" property defines which exception will be thrown for the list of error codes specified in the errorCodes property.

回到第四段对 CustomSQLErrorCodesTranslation[] 的使用:

                                 for (CustomSQLErrorCodesTranslation customTranslation : customTranslations) {                                                if (Arrays.binarySearch(customTranslation.getErrorCodes(), errorCode) >= 0 &&                                                                customTranslation.getExceptionClass() != null)

CustomSQLErrorCodesTranslation[] 中的每一个 customTranslations,匹配当前的 errorCode 是不是在它的处理范围内(即它的 private String[] errorCodes 这个数组类型的成员中有和 errorCode 相同的值),如果有,且它 private Class exceptionClass 成员不为 null,就说明该将当前异常转化为 exceptionClass 类的异常。

话说我一开始写这篇文章的动机,就是极客时间-玩转Spring全家桶-了解Spring的JDBC抽象异常里讲,而我想具体搞明白。而课程视频就是在自定义的``sql-error-codes.xml中注入自定义的CustomSQLErrorCodesTranslation[]`来完成扩展的。

第四段 扫尾

                               // Next, look for grouped error codes.                                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);                                }                                //后面还有大同小异逻辑一致的几个就不复制粘贴了

这一段的大体逻辑和上一节相同,已经到了最后,没有了扩展空间。

getBadSqlGrammarCodes()getInvalidResultSetAccessCodes() 虽说也能通过自定义的 sql-error-codes.xml 去改,但没必要了,因为它们返回的异常的类型都是写死的,这块地方其实是 Spring 给经典的 SQL 错误的自留地,我们就不要动了。写自己的 sql-error-codes.xml 时复制粘贴下这块东西就好,如下是 Spring 原生的 sql-error-codes.xml 中对应 MySQL 数据库的内容:

                                                                            MySQL                                MariaDB                                                                                1054,1064,1146                                                        1062                                                        630,839,840,893,1169,1215,1216,1217,1364,1451,1452,1557                                                        1                                                        1205,3572                                                        1213                        

后面还有一些代码就是 Spring 处理不过来的 SQLException 做一些日志记录,不值得多说了。

扩展点总结

第一个扩展点是 protected 的方法,用户可以通过继承后在此方法中添加自己的实现的实现后将自己的实现类注册为 Bean,再结合 Spring 为默认的实现注册 Bean 时的 @ConditionalOnMissingBean 限制,达成了扩展点。

随后的几个扩展点的本质都是为默认实现的 Bean 中留下可注入的成员,用户通过实现特定接口并将其注册为 Bean,结合成员注入将带着自己实现逻辑的 Bean 注入后将自己的私活带入。

"Spring转换SQLException并埋入扩展点的工作过程"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

0